Grok Developer’s Notes¶
This document is a developer’s overview of Grok. It is not intended to be a beginner’s tutorial. It’s also not a reference. It gives a succinct an overview of what’s there in Grok, with a brief idea on how to use it, so you can try it out and learn more about it. This includes rules, common APIs and patterns.
Models¶
A Grok-based application is composed out of one or more models. We also call these content objects, or just objects. The objects are just Python objects, instantiated from a class. Models can be stored in the object database (ZODB), created by an object relational mapper, or created on the fly by your code.
Grok comes with two kinds of models: grok.Model
and
grok.Container
. grok.Model
is the most basic one and doesn’t
really do much for you. You can subclass from grok.Model
, like
this:
class Document(grok.Model):
pass
The main thing subclassing from grok.Model
does is make it
possible (but not required) to store instances in the ZODB.
You can also subclass from grok.Container
, like this:
class Folder(grok.Container):
pass
A container is like a model, but also acts much like a Python
dictionary. The main difference with Python dictionaries is that its
methods, like keys
and items
, are iterator-like. They also do
more, like send events, but we can forget about that for now.
In order to be able to install an application, you need to mix in
grok.Application
into a class:
class Application(grok.Application, grok.Container):
pass
Instances of this class can now be installable in the Grok web UI.
Let’s make a structure with some folders and documents:
app = Application()
app['a'] = a = Container()
a['b'] = Document()
a['c'] = Container()
a['c']['d'] = Document()
Grok publishes these objects to the web: this is called object publishing. What this means in essence is that objects can be addressed with URLs. When you access a URL of a Grok application with your web browser, Grok uses this URL to find an object.
An example: if app
were installed under
http://localhost:8080/app
, the following URLs will exist in your
application:
http://localhost:8080/app
http://localhost:8080/app/a
http://localhost:8080/app/a/b
http://localhost:8080/app/a/c
http://localhost:8080/app/a/c/d
__parent__
and __name__
¶
Models in Grok are automatically supplied with a __parent__
and a
__name__
attribute.
__parent__
points to object this object is in. If the object is in a container, this is the container.__name__
is the name this object has in a URL (and in its container, if it is in a container).
These attributes are used for navigation through content space, and
Grok also uses them to construct URLs automatically (see below). The
__parent__
and __name__
attributes are automatically added to
an object when it is placed in a container, or when it is being
traversed through using traverse
.
Custom traversing¶
Grok resolves URLs to objects by traversing through the containers
and models in question. What if you want to customize the way this
traversal works? Perhaps you want to traverse through objects you
create yourself, or objects created by an object relational
mapper. Grok offers a handy way to do so: the traverse
method on
models.
A math example: imagine you want to create an application that represents integer numbers, and you want to traverse to each individual number, like this:
http://localhost:8080/integers/0
http://localhost:8080/integers/1
http://localhost:8080/integers/2
http://localhost:8080/integers/3
...
and so on. How would we implement this? We cannot create a container and fill it with all integers possible, as there are an infinite number of them. Okay, so we are in a math example, so let’s be exact: this is true if we ignore memory limitations and URL length limitations. Storing all possible integers in a container is just not practical.
We use the traverse
method:
class ParticularInteger(grok.Model):
def __init__(self, number):
self.number = number
class Integers(grok.Application, grok.Model):
def traverse(self, name):
try:
value = int(name)
except ValueError:
return None # not an integer
return ParticularInteger(value)
Now all URLs for numbers are available. What’s more, other URLs like this are not:
http://localhost:8080/integers/foo
The traverse
method works by trying to convert the path element
that comes in as name
to an integer. If it fails, we return
None
, telling Grok that the traverse()
method didn’t find the
object asked for. Grok then falls back on default behavior, which in
this case would mean a 404 Not Found
error.
Traversable attributes¶
In some cases, you want to traverse to attributes or methods of your
grok.Model
. This can be done easily using the grok.traversable
directive:
class Mammoth(grok.Model):
grok.traversable('trunk')
trunk = Trunk()
class MammothView(grok.View):
grok.context(Mammoth)
def render(self):
return "I'm a mammoth!"
Now, if traversing to http://localhost/mammoth/trunk , a Trunk() object will be exposed at that URL.
Views¶
Now that we have models and can build structures of them, we will need to look at ways to actually present them to the user: views. So what is a view? A view is a class that represents a model in some way. It creates a user interface of some sort (typically HTML) for a model. A single model can have more than one view. It looks like this:
class Index(grok.View):
grok.context(Application)
def render(self):
return "This is the application"
The grok.context
bit in the class is an example of using a Grok
directive. If you use grok.context
on a view class, it connects
the view to the class we give it. So in this case, Index
is a view
for Application
. Note that if there is only a single model in the
module and you want your view to be associated with it, you can leave
out grok.context
and the view will be associated with that model
by default. Many directives have such default behavior, allowing you
to leave them out of your code if you organize your code in a certain
way.
The default view for a model is called index
. You can specify
index
at the end of the URL, like this:
http://localhost:8080/app/index
What happens when you go to this URL is that Grok instantiates the
Index
class, creating a Index
instance. View instances have
a number of attributes by default:
context
, the model instance that the view is presenting.
request
, the current web request.
response
, an object representing the response sent to theuser. Used less often.
index
views are special, as it’s also fine not to add index
at
the end, because the name index
is the default:
http://localhost:8080/app
You can also create views with different names:
class Edit(grok.View):
grok.context(Application)
def render(self):
return "This is the edit screen for the application"
Now you can go to this URL:
http://localhost:8080/app/edit
The name of the view is the name of the view class, lowercased. This
is the default behavior: you can override this using the grok.name
directive:
class SomeImpossiblyLongClassName(grok.View):
grok.context(Application)
grok.name('edit')
def render(self):
return "This is the edit screen for the application"
Templates¶
In the previous examples, we used the render
method to determine
what you actually see on a web page. For most views we don’t want to
do that: we want to use a template to prepare presentation. Using a
template with a view is easy. First create a directory
<name>_templates
, where <name>
is the the module that contains
the views. So, if you are developing in a module app.py
, you need
to create a subdirectory app_templates
for templates in the same
directory as the app.py
module.
You can then add templates to that directory with the same name as the
view class name (lowercase), with the .pt
extension
appended. These templates follow the Zope Page Template (ZPT) rules,
though Grok can also be extended to support other template languages.
You could for instance have this view:
class Index(grok.View):
grok.context(Application)
and a file index.pt
in the module’s templates directory containing
template code.
These are the defaults. If for some reason you want the name of the
template directory not to be based on the name of module, you can
manually set the name of the template directory used by a module by
using the grok.templatedir
directive in the module. If you want
the name of the template not to be based on the name of the class, you
use the grok.template
directive in the view class.
The template can access attributes and methods on the view through the
special view
name available in the template. The template can
access attributes and methods on the model through the special
context
name available in the template. The template has the
following special names available:
* ``view`` - the view that this template is associated with
context
- the model that is being viewedrequest
- the current request objectstatic
- to make URLs to static content made available by this module
and any names you also make available using the namespace
method.
static content¶
A typical web page references one or more CSS files, javascript files and images: static content that is part of the layout.
To make available static content to your template create a directory
in your package called static
. Put .css
files, .js
files,
image and whatever else is needed in there.
You can now refer to these static files in your template using the
special name static
, like this (ZPT example):
<img tal:attributes="src static/my_image.png" />
This will automatically create a URL to the place where Grok published that image.
You can create subdirectories in static
and refer to them as you’d
expect:
<image tal:attributes="src static/images/some_image.gif" />
update
¶
You can define an update
method in a view to prepare a view just
before it is accessed. You can use this to process information in the
request (URL parameters or form variables) or in the context, and set
attributes on the view that can be used in the template:
def update(self):
self.total = int(self.request.form['a']) + int(self.request.form['b'])
The template now has access to view.total
.
You can define parameters in the update view. These will be automatically bound to parameters (or form values) in the request:
def update(self, a, b):
self.total = int(a) + int(b)
namespace
¶
If you just want a variable to become available in the top-level of
your template (much like view
and model
), you can also define
the namespace
method on the view:
def namespace(self):
return {'foo': "Some value"}
You can now refer to foo
in your template and have available to
this value.
the url
method¶
Views have a special method called url()
that can be used to
create URLs to objects. The url
method takes zero, one or two
arguments and an additional optional keyword argument ‘data’ that
is converted into a CGI query string appended to the URL:
* self.url() - URL to this view.
self.url(object) - URL to the provided object.
- self.url(u”name”) - URL to the context object, with
/name
appended, to point to a view or subobject of the context.
- self.url(u”name”) - URL to the context object, with
- self.url(object, u”name”) - URL to the provided object, with
/name
appended, to point to a view or subobject of the provided object.
- self.url(object, u”name”, data={‘name’:’Peter’, ‘age’:28})
URL to the provided object, with
/name
appended with ‘?name=Peter&age=28’ at the end.
- self.url(data={‘name’:u’Andrxe9’, ‘age:int’:28}) - URL to the provided
object with ‘?name=Andre%C3%A9’&age%3Aint=28’.
From the view, this is accessed through self.url()
. From the
template, this method can be accessed using view.url()
.
the application_url
method¶
When using views it is sometimes desirable to be able to construct a
URL to the application object. application_url
is a quick way to
do it. It takes a single optional argument, name, which is the name
of a view of the application.
the redirect
method¶
The redirect
method on views can be used to redirect the browser
to another URL. Example:
def render(self):
self.redirect(self.url(self.context.__parent__))
# return empty body as we are going to redirect anyway
return ''
__parent__
and __name__
on views¶
Like models, views also get supplied with a __parent__
and
__name__
object when they are instantiated for a particular model.
__parent__
points to the model being viewed (and is the same as
context
, which should normally be used).
__name__
is the name of the view in the URL.
The @@
thing¶
Supposing you have a view called edit
, whenever you write this:
http://localhost:8080/app/edit
you can also write this:
http://localhost:8080/app/@@edit
Why the ugly @@
syntax? Imagine that app
is a container, and
that your user interface lets the user add objects to it with a name
of their own choosing. The user could decide to add an object called
index
. In that case Grok wouldn’t know whether the
http://localhost:8080/app/index
index is to get to a view or a
subobject. @@
tells the system to look up a view definitely. If
@@
is not provided, subobjects take precedence over views in case
of name collision.
Request¶
Some useful things to know about the request object (accessible as an attribute on the view):
Information on the request
object can be accessed using mapping
access (request[`foo`]
). You can access request form variables and
cookies and headers (including environment variables).
To access form variables in particular use: request.form['foo']
.
To access cookies in particular use: request.cookies['foo']
.
To access headers (and environment variables) in particular use:
request.headers['foo']
. You can also use request.getHeader()
,
with the header name as the argument, and an optional second default
argument.
Instead of the mapping access, the get
methods work as well, as on
normal Python dictionaries.
More can be found in the IHTTPRequest
interface documentation
in zope.publisher.interfaces.http
.
Response¶
Some useful things to know about the response object (accessible as an attribute on the view):
setStatus(name, reason)
sets the HTTP status code. The argument
may either be an integer representing the status code (such as 200
or 400
), or a string (OK
, NotFound
). The optional second
argument can be be used to pass the human-readable representation
(Not Found
).
setHeader(name, value)
can be used to set HTTP response headers. The first
argument is the header name, the second the value.
addHeader(name, value)
can be used to add a HTTP header, while
retaining any previously set headers with the same name.
setCookie(name, value, **kw)
can be used to set a cookie. The first
argument is the cookie name, the second the value. Optional keyword
arguments can be used to set up further cookie properties (such as
max_age
and expires
).
expireCookie(name, **kw)
can be used to immediately expire a
cookie.
More can be found in the IHTTPResponse
interface documentation
in zope.publisher.interfaces.http
.
Adapters¶
An adapter is much like a view, but is aimed towards developers, not end users. It presents an interface to an object, but an interface for developers, not an user interface for end-users.
The section on adapters will of necessity be rather abstract. Feel free to skip it until you want to know what is going on up with interfaces and adapters - it is an important foundation to Grok, but one you do not know much about when you get started.
An adapter can be used to add new methods to an object without changing the object. To demonstrate the principle, we will construct adapters entirely by hand first. At the end we will show how Groks helps in constructing adapters and using them.
Imagine we are developing a content management system and we want to
get information about the size (in, say, bytes, approximately) of
content objects stored in our CMS, for instance in order to display it
in our UI or to calculate the total size of all objects in a
container. The simplest approach would be to add a size()
method
to all our content objects:
class Document(grok.Model):
def __init__(self, text):
self.text = text
def size(self):
return len(self.text.encode('UTF-8'))
class Image(grok.Model):
def __init__(self, data):
self.data = data
def size(self):
return len(self.data)
class Container(grok.Container):
def size(self):
total = 0
for obj in self.values():
total += obj.size()
return total
For simple cases this is fine, but for larger applications this can
become a problem. Our Document
model needs a size
method, and
does our Image
model, and our Container
, and our News Item
model, and so on. Given the requirements of a typical CMS, content
objects would soon end up with a very large number of methods, for all
sorts of functionality, from getting the size of objects to offering a
commenting facility. It would be nicer to separate things out and keep
the underlying models clean.
To do this, we can use the adaptation pattern. As said, we will do it
by hand at first. An adapter is an object that adds an API to another
object (typically stored as the context
attribute of the
adapter):
class DocumentSized(object):
def __init__(self, context):
self.context = context
def size(self):
return len(self.context.text.encode('UTF-8'))
We would use it like this:
DocumentSized(document).size()
We could extend this same adapter to work for different kinds of content objects, but that isn’t very extensible when new adapters need to be made:
class Sized(object):
def __init__(self, context):
self.context = context
def size(self):
if isinstance(self.context, Document):
return len(self.context.text.encode('UTF-8'))
elif isinstance(self.context, Image):
return len(self.context.data)
elif isintance(self.context, Container):
total = 0
for obj in self.context.values():
total += Sized(obj).size()
return total
Instead, we can create a smart sized
factory that does this
switch-on-type behavior instead, keeping our adapters clean:
class DocumentSized(object):
def __init__(self, context):
self.context = context
def size(self):
return len(self.context.text.encode('UTF-8'))
class ImageSized(object):
def __init__(self, context):
self.context = context
def size(self):
return len(self.context.data)
class ContainerSized(object):
def __init__(self, context):
self.context = context
def size(self):
total = 0
for obj in self.context.values():
total += sized(obj).size()
return total
def sized(context):
if isinstance(context, Document):
return DocumentedSized(context)
elif isinstance(context, Image):
return ImageSized(context)
elif isinstance(context, Container):
return ContainerSized(context)
We can now call sized
for a content object and get an object back
that implements the “sized API”:
s = sized(my_content_object)
print s.size()
It’s good to spell out the APIs of your application explicitly, as
documentation so that other developers can work with them and also
implement them for their own content objects. Grok lets you do this
using an interface specification, using the zope.interface
package:
from zope.interface import Interface
class ISized(Interface):
def size():
"Return the size of the object"
We can now make this ISized
interface into the adapter factory
(like sized
above), without actually having to implement it
directly. Let’s do that now by subclassing from grok.Adapter
and
using a few grok directives:
class DocumentSized(grok.Adapter):
grok.context(Document)
grok.provides(ISized)
def size(self):
return len(self.context.text.encode('UTF-8'))
class ImageSized(grok.Adapter):
grok.context(Image)
grok.provides(ISized)
def size(self):
return len(self.context.data)
class ContainerSized(grok.Adapter):
grok.context(Container)
grok.provides(ISized)
def size(self):
total = 0
for obj in self.context.values():
total += ISized(obj).size()
return total
We can now use ISized
like we used sized
above:
s = ISized(my_content_object)
print s.size()
When new content objects were to be created for this CMS, ISized
adapters can be registered for them anywhere. Using this pattern,
existing objects implemented by someone else can be made to conform
with the ISized
API without having to modify them.
grok.context
works as for views. It is useful to point it to any
class however, not just that of models. grok.provides
has to be
pointed to an interface (the interface that the adapter adapts to).
Interfaces¶
Classes can also be made to implement an interface. This means that instances of that class provide that interface:
from zope.interface import Interface, Attribute
class IAnimal(Interface):
name = Attribute("The name of the animal")
def makeSound():
"The sound the animal makes."
class Cow(object):
grok.implements(IAnimal)
def __init__(self, name):
self.name = name
def makeSound(self):
return "Mooo"
We can ask the interface machinery whether an object provides an interface:
>>> cow = Cow()
>>> IAnimal.providedBy(cow)
True
If you use an interface to adapt an object, and this object already provides the interface, you get back the object itself:
>>> IAnimal(cow) is cow
True
grok.context
can always point to an interface instead of a class
directly. This indirection can be useful to make a view or adapter
work for a whole set of classes that all implement the same interface.
ComponentLookupError
¶
What if an adapter cannot be found for a particular object? Perhaps no
adapter has been registered for a particular object or a particular
interface. The system will raise a ComponentLookupError
:
>>> ISized(cow)
Traceback (most recent call last):
...
ComponentLookupError
If you want to catch this exception, you can import it from
zope.component.interfaces
:
from zope.component.interfaces import ComponentLookupError
Named adapters¶
It is possible to give an adapter a name, making it a named
adapter. This way it is possible to have more than one adapter
registered for a single object that all provide the same interface,
each with a different name. This feature is rarely used directly,
but internally it is used for views, as we will see later. The
grok.name()
directive can be used to give an adapter a name:
class Adapter(object):
grok.name('somename')
grok.context(SomeClass)
grok.provides(ISomeInterface)
Actually all adapters are named: by default the name of an adapter is the empty string.
You cannot call the interface directly to get a named adapter for an
object. Instead, you need to use the APIs provided by the
zope.component
package, in particular getAdapter
:
from zope import component
my_adapter = component.getAdapter(some_object, ISomeInterface,
name='somename')
getAdapter
can also be used to look up unnamed adapters, as an
alternative to using the interface directly:
myadapter = component.getAdapter(some_object, ISomeInterface)
Functions as adapters¶
Sometimes an adapter doesn’t need to be a full-fledged class;
registering the factory function itself is enough. We can do this with
the @grok.adapter
and @grok.implementer
decorators. This way
we can write simple adapters that don’t need to return a full-fledged
custom class instance but for instance some built-in Python object
like a string:
@grok.adapter(SomeClass)
@grok.implementer(ISomeInterface)
def some_interface_for_some_class(some_instance):
return str(some_instance)
You can now do the following:
some_instance = SomeClass()
s = ISomeInterface(some_instance)
print s
ISomeInterface
now behaves much like str
for SomeClass
.
Multi adapters¶
Another feature of adapters is that you can adapt multiple objects at once using a multi adapter. Again this feature is rarely used in practice, except internally to implement views and events.
You can construct a multi adapter by subclassing from
grok.MultiAdapter
:
class MyMultiAdapter(grok.MultiAdapter):
grok.adapts(SomeClass, AnotherClass)
grok.provides(ISomeInterface)
def __init__(some_instance, another_instance):
self.some_interface = some_instance
self.another_instance = another_instance
The multi-adapter receives as many arguments as what it was registered
for using grok.adapts
.
A multi adapter also cannot be looked up directly by calling the
interface. Instead, we need to use the zope.component
package
again:
from zope import component
my_multi_adapter = component.getMultiAdapter((some_object, another_object),
ISomeInterface)
getMultiAdapter
receives as the first argument a tuple with the
combination of objects to adapt.
It can also optionally be named using grok.name
and then looked up
using a name argument:
my_named_multi_adapter = component.getMultiAdapter(
(some_object, another_object), ISomeInterface, name="foo")
Views as adapters¶
A view in Grok is in fact a named multi adapter, providing the base
interface (Interface
). This means that a view in Grok can be
looked up in code by the following call:
from zope.interface import Interface
view = component.getMultiAdapter((object, request), Interface, name="index")
Since the default for the second argument is in fact Interface
, this
call can be shorted to this:
view = component.getMultiAdapter((object, request), name="index")
Being able to do this in code is sometimes useful. It is also what Grok does internally when it looks up a view.
Events¶
Grok lets you write handlers for events. Using event handlers you can hook into code that you do not control. Events allow decoupling: a framework can send events without worrying who is interested in it, and similarly you can send events to work with existing bits of framework that expects them. You can also define new types of events if you are designing a framework yourself.
You write an event handler by writing a function that subscribes to the event, and marking it with a python decorator:
@grok.subscribe(Document, grok.IObjectAddedEvent)
def handle(obj, event):
print "Object %s was added." % obj
Whenever an instance of a model of class Document
(or subclasses)
is added to a container, this code will be run. You can then take some
action. Any grok.Container
subclass will take care of sending
these events automatically. You can have as many subscribers for a
particular event as you like. The order in which they are run is not
guaranteed by the system, so cannot be relied on.
The event handler takes two arguments: the object for which the event was fired, and the event instance. The event instance has attributes, depending on the type of event.
Events defined by Grok¶
Here we describe the standard events defined by Grok. Described are the interfaces which you would use in a subscriber, and how you can send this event yourself. Other events may be defined by libraries or by you.
IObjectMovedEvent
¶
Will be fired whenever an object is moved from container to container, renamed, added or removed.
The event object has these attributes:
object
- the object being movedoldParent
- the parent (container) from which the object was movedor removed, or
None
if this object is newly added.
oldName
- the previous name of the object in its container,before renaming if renaming took place, or
None
if this object is newly added.
newParent
- the parent (container) to this object was moved oradded.
None
if this object was removed.
newName
- the name the object has in the new container, orNone
if this object was removed.
Containers take care of sending this event, but should you want to send it yourself, use:
grok.notify(grok.ObjectMovedEvent(obj, oldParent, oldName, newParent, newName))
IObjectAddedEvent
¶
Fired when an object is added to a container. Specialization of
IObjectMovedEvent
, and shares the attributes as described.
Containers take care of sending this event, but should you want to send it yourself, use:
grok.notify(grok.ObjectAddedEvent(obj))
or:
grok.notify(grok.ObjectAddedEvent(obj, newParent, newName))
IObjectRemovedEvent
¶
Fired when an object is removed from a container (and not re-added
elsewhere). Specialization of IObjectMovedEvent
, and shares the
attributes as described.
Containers take care of sending this event, but should you want to send it yourself, use:
grok.notify(grok.ObjectRemovedEvent(obj)
or:
grok.notify(grok.ObjectRemovedEvent(obj, oldparent, oldName))
IObjectModifiedEvent
¶
Fired when an object is modified by the system, such as when a form is saved. If you modify the object in code, the system won’t know about this, and you will have to remember to send it yourself.
This event has a single attribute, object
, which is the object
that was modified.
To send this event yourself, use:
grok.notify(grok.ObjectModifiedEvent(obj))
IContainerModifiedEvent
¶
A specialization of IObjectModifiedEvent
that fires when the
container was modified by adding something to it or removing from it.
Containers take care of sending this event, but if you want to send it yourself, use:
grok.notify(grok.ContainerModifiedEvent(obj))
IObjectCreatedEvent
¶
Fired when an object is created. When you create your own objects the
system won’t know about this, and you will have to remember to send it
yourself if you care about listing to IObjectCreatedEvent
. This is
fairly rare - usually you’re better of looking at
IObjectAddedEvent
if you can.
This event has a single attribute, object
, which is the object
that was created.
To send this event yourself:
grok.notify(grok.ObjectCreatedEvent(obj))
IObjectCopiedEvent
¶
Fired when an object was copied. It is a specialization of
IObjectCreatedEvent
that is fired by the system if you use the
zope.copypastemove
functionality.
Besides the object
attribute it shares with
IObjectCreattedEvent
, it has also has the original
attribute,
which was the object that iwas copied from.
To send this event yourself:
grok.notify(grok.ObjectCopiedEvent(copy, original))
IBeforeTraverseEvent
¶
Fired when the publisher is about to traverse into your object. This is useful to specify on your application object if you for instance want to set the default skin for your application.
Creating and sending your own events¶
If you are going to send an object that pertains to a particular object,
subclass zope.component.interfaces.ObjectEvent
:
from zope.component.interfaces import ObjectEvent
class MyEvent(ObjectEvent):
pass
You can then send it like this:
grok.notify(MyEvent(some_obj))
And listen for it like this:
@grok.subscribe(SomeClass, MyEvent)
def handle_my_event(obj, event):
pass
This subclassing from ObjectEvent
is not required; if your event
isn’t about an object, you can choose to design your event class
entirely yourself. See zope.sendmail
for the construction of mail sending
events for an example.
Interfaces for events¶
For documentation purposes it can be a good idea to to define an interface for your event. You can then also allow for multiple implementations of the same event interface. When you have an interface for your event, you can then listen for the interface in the subscribers as well:
from zope.interface import Interface
class IMyEvent(zope.component.interfaces.IObjectEvent):
"My special event"
class MyEvent(zope.component.interfaces.ObjectEvent):
grok.implements(IMyEvent)
@grok.subscribe(SomeClass, IMyEvent):
def handle_my_event(obj, event):
pass
More about interfaces¶
We have seen small examples of interfaces before, but here we will go a bit more into them, and why they are useful.
An interface is a description of the API of a class (or more rarely, module or object). Interfaces are useful because:
They are API documentation.
They can describe how a framework expects you to implement classes that fit into it.
The system can inspect the interfaces a particular object provides, and treat them as an abstract form of classes for registration purposes.
Interfaces make it possible to use a generic framework’s pluggability points with confidence: you can clearly see what you are supposed to implement to plug into it. You can define very generic frameworks yourself by defining them in terms of interfaces.
Some interface features¶
A summary of interface features we’ve seen:
To create an interface, subclass from
zope.interface.Interface
.To state that implementors of the interface must have a method, supply the method with arguments. Don’t use
self
as the first arguments, as this is an implementation detail not important to the interface. Instead, describe the methods as they look to the caller.To state that implementors of the interface must have an attribute, use:
some_attribute = zope.interface.Attribute("Description of attribute")
To state a class implements an interface, use
grok.implements
.Instances of a class are said to provide the interface that the class implements.
You can check whether an instance provides a certain interface by using
some_interface.providedBy
:IObjectEvent.providedBy(NonSubclassEvent(some_obj))
Interfaces and events¶
Let’s study interfaces some more in connection with
IObjectModifiedEvent
. The IObjectModifiedEvent
interface looks
like this:
class IObjectModifiedEvent(zope.component.interfaces.IObjectEvent):
"""An object has been modified"""
This refers us to the IObjectEvent
interface, which looks like
this:
from zope import interface
class IObjectEvent(interface.Interface):
"""An event related to an object.
"""
object = interface.Attribute("The subject of the event.")
We therefore know that if we implement IObjectModifiedEvent
, we
must supply a single attribute, object
.
The following event handler for instances of SomeClass
subscribes
to any event that provides IModifiedObjectEvent
:
@grok.subscribe(SomeClass, IObjectModifiedEvent):
def handle_event(obj, event):
"Called when there is an IObjectModifiedEvent for SomeClass instances."
This handler will be called not only for subclasses of the
grok.ObjectModifiedEvent
class, but also for other, otherwise
unrelated classes that implement IObjectEvent
, such as this one:
class NonSubclassObjectEvent(object):
grok.implements(IObjectEvent)
def __init__(self, object):
self.object = object
So far we have only used interfaces for the second argument of the
event handler registration, but the principle also works for the first
argument. For example, to handle IObjectModifiedEvent
events for
all kinds of containers, you can subscribe to
zope.container.interfaces.IContainer
objects:
@grok.subscribe(IContainer, IObjectModifiedEvent):
def handle_event(obj, event):
"Called whenever any container is modified"
zope.container.interfaces.IContainer
defines the abstract
container API that all containers must provide, no matter how they are
implemented internally.
Interfaces and adapters¶
The same principle also works for adapters and grok.context
. You
can use grok.context
with interfaces as well as with concrete
classes. To write an adapter that works for any kind of container, you
can write:
from zope.container.interfaces import IContainer
class SortedKeysAdapter(grok.Adapter):
grok.context(IContainer)
grok.provides(ISortedKeys)
def sortedKeys(self):
return sorted(self.context.keys())
Interfaces and views¶
The same principle can also be used with grok.context
in other
places, such as in views. This view is registered for all containers:
from zope.container.interfaces import IContainer
class Keys(grok.View):
grok.context(IContainer)
def render(self):
return ', '.join(ISortedKeysAdapter(self.context).sortedKeys())
The view keys
exists for all containers, no matter how they are
implement, where they are implemented or who implemented them, as long
as they provide IContainer
.
Using the fact Interface
is the base of all interfaces, you can
even register a view for all objects. This can be useful to register
ZPT macros, which will then be available on all contexts:
class Layout(grok.View):
grok.context(Interface)
with a template layout.pt
associated to it.
You can then use these macros in any page template anywhere by referring to them like this:
<html metal:use-macro="context/@@layout/macros/page">
Forms¶
Grok can autogenerate web forms from descriptions called schema. A
schema is a special kind of interface. We already saw Attribute
,
which can be used to specify that something that provides that
interface should have that attribute. The zope.schema
package adds
a lot more specific field descriptions. Here is an example of a
schema:
from zope.interface import Interface
from zope import schema
class ISpecies(Interface):
name = schema.TextLine(u"Animal species name")
scientific_name = schema.TextLine(u"Scientific name")
legs = schema.Int(u"Number of legs")
Let’s also look at a simple implementation of this interface:
class Species(grok.Model):
grok.implements(ISpecies)
Note how we aren’t even creating an __init__
to set the
attributes; we could, but we’ll see below that Grok’s applyData
can take care of this automatically.
The ISpecies
schema can be turned into a form. Grok does this by
looking up a widget for each schema field to display it. A widget is
very much like a view. Let’s look at a form for this schema:
class Species(grok.Form):
form_fields = grok.Fields(ISpecies)
@grok.action(u"Save form")
def handle_save(self, **data):
print data['name']
print data['scientific_name']
print data['legs']
What is going on here? Firstly we use a special base class called
grok.Form
. A form is a special kind of grok.View
, and
associates the same way (using grok.context
). A form expects two
things:
a
form_fields
attribute. Above we see the most common way to construct this attribute, usinggrok.Fields
on the interface.one or more actions. Actions are specified by using the
@grok.action
decorator. An action gets the fields filled in the form as keyword parameters, so**data
in this case. We could also have specified the arguments we expected specifically.
Form widgets translate the raw HTML form input to Python objects, such as (unicode) strings, integers and datetime objects, as specified by schema fields. The schema fields can then be used to validate this input further. Forms are self-submitting, and in case of a validation error the form can render them in-line next to the fields.
We’ll look at a lot of form features next.
grok.AddForm
¶
An add form is used to create a new object. Most forms are views of the object that they are representing, but an add form is typically associated a view of the container in which new objects are to be added. Let’s look at an example:
class SpeciesContainer(grok.Container):
pass
class Add(grok.AddForm):
grok.context(SpeciesContainer)
form_fields = grok.Fields(ISpecies)
@grok.action(u"Add species")
def add_species(self, **data):
# create a species instance
species = Species()
# assign the right attributes to fulfill ISpecies schema with
# the form data
self.applyData(species, **data)
# stores the instance into the SpeciesContainer
name = data['name']
self.context[name] = species
# redirect to the newly created object
self.redirect(self.url(species))
# we don't want to display anything, as we redirect
return ''
The user can now go to myspeciescontainer/add
to add a species,
where myspeciescontainer
is any instance of SpeciesContainer
.
grok.EditForm
¶
Now that we can create species objects, let’s create a form so you can
easily edit them. This is a view of the Species
model:
class Edit(grok.EditForm):
grok.context(Species)
form_fields = grok.Fields(ISpecies)
@grok.action(u"Edit species")
def edit_species(self, **data):
self.applyData(self.context, **data)
Forms are self-submitting, so this will show the edit form again. If you want to display another page, you can redirect the browser as we showed for the add form previously.
The user can now go to myspecies/edit
to edit the species.
grok.DisplayForm
¶
Sometimes you just want to display an object, and not actually edit it. If the object is schema-based, an easy way to do this is to use display forms. Let’s look at an example:
class Display(grok.DisplayForm):
grok.context(Species)
form_fields = grok.Fields(ISpecies)
The user can now go to myspecies/display
to look at the species.
Associating a template for a form¶
By default, Grok supplies some templates for forms. They work, but they are not very pretty and don’t fit into your application’s layout. You can instead use your own form rendering logic in a template you associate with the form just like you associate templates with views. You can also abstract form rendering logic you keep reusing into a ZPT macro. Below is an example of form rendering logic to help you get started. The example doesn’t have any consideration for layouting to make the logic clear. As a result, the form will be very ugly if you use this - you will want to use CSS or table HTML to layout things:
<!-- render the form tag -->
<form action="." tal:attributes="action request/URL" method="post"
class="edit-form" enctype="multipart/form-data">
<!-- render any validation errors on top -->
<ul class="errors" tal:condition="view/errors">
<li tal:repeat="error view/error_views">
<span tal:replace="structure error">Error Type</span>
</li>
</ul>
<!-- render the widgets -->
<tal:block repeat="widget view/widgets">
<label tal:attributes="for widget/name">
<!-- a * when the widget is required -->
<span class="required" tal:condition="widget/required">*</span>
<!-- the title of the field -->
<span i18n:translate="" tal:content="widget/label">label</span>
</label>
<!-- render the HTML widget -->
<div class="widget" tal:content="structure widget">
<input type="text" />
</div>
<!-- render any field specific validation error from a previous
form submit next to the field -->
<div class="error" tal:condition="widget/error">
<span tal:replace="structure widget/error">error</span>
</div>
</tal:block>
<!-- render all the action submit buttons -->
<span class="actionButtons" tal:condition="view/availableActions">
<input tal:repeat="action view/actions"
tal:replace="structure action/render" />
</span>
</form>
The template for a display form a lot simpler:
<tal:block repeat="widget view/widgets">
<tal:block content="widget/label" />
<input tal:replace="structure widget" />
</tal:block>
<!-- render all the action submit buttons -->
<span class="actionButtons" tal:condition="view/availableActions">
<input tal:repeat="action view/actions"
tal:replace="structure action/render" />
</span>