Navigation

So far so good. The point is that usually, you won’t get enough by assembling
cubes out-of-the-box. You will want to customize them, have a personal look and
feel, add your own data model and so on. Or maybe start from scratch?

So let’s get a bit deeper and start coding our own cube. In our case, we want
to customize the blog we created to add more features to it.

First, notice that if you’ve installed CubicWeb using Debian packages, you will
need the additional cubicweb-dev package to get the commands necessary to
CubicWeb development. All cubicweb-ctl commands are described in details in
cubicweb-ctl tool.

Once your CubicWeb development environment is set up, you can create a new
cube:

cubicweb-ctlnewcubemyblog

This will create in the cubes directory (/path/to/grshell/cubes for source
installation, /usr/share/cubicweb/cubes for Debian packages installation)
a directory named blog reflecting the structure described in
Standard structure for a cube.

For packages installation, you can still create new cubes in your home directory
using the following configuration. Let’s say you want to develop your new cubes
in ~src/cubes, then set the following environment variables:

CW_CUBES_PATH=~/src/cubes

and then create your new cube using:

cubicweb-ctlnewcube--directory=~/src/cubesmyblog

Note

We previously used myblog as the name of our instance. We’re now creating
a cube with the same name. Both are different things. We’ll now try to
specify when we talk about one or another, but keep in mind this difference.

A simple set of metadata about your cube are stored in the __pkginfo__.py
file. In our case, we want to extend the blog cube, so we have to tell that our
cube depends on this cube, by modifying the __depends__ dictionary in that
file:

__depends__={'cubicweb':'>= 3.10.7','cubicweb-blog':None}

where the None means we do not depends on a particular version of the cube.

The first step is the import from the yams package necessary classes to build
the schema.

This file defines the following:

a Community has a title and a description as attributes

the name is a string that is required and can’t be longer than 50 characters

the description is a string that is not constrained and may contains rich
content such as HTML or Restructured text.

a Community may be linked to a Blog using the community_blog relation

* means a community may be linked to 0 to N blog, ? means a blog may
be linked to 0 to 1 community. For completeness, remember that you can also
use + for 1 to N, and 1 for single, mandatory relation (e.g. one to one);

this is a composite relation where Community (e.g. the subject of the
relation) is the composite. That means that if you delete a community, its
blog will be deleted as well.

Of course, there are a lot of other data types and things such as constraints,
permissions, etc, that may be defined in the schema, but those won’t be covered
in this tutorial.

Notice that our schema refers to the Blog entity type which is not defined
here. But we know this type is available since we depend on the blog cube
which is defining it.

Now the problem is that we created an instance using the blog cube, not our
myblog cube, so if we don’t do anything there is no way that we’ll see anything
changing in the instance.

One easy way, as we’ve no really valuable data in the instance would be to trash and recreated it:

cubicweb-ctlstopmyblog# or Ctrl-C in the terminal running the server in debug modecubicweb-ctldeletemyblogcubicweb-ctlcreatemyblogcubicweb-ctlstart-Dmyblog

Another way is to add our cube to the instance using the cubicweb-ctl shell
facility. It’s a python shell connected to the instance with some special
commands available to manipulate it (the same as you’ll have in migration
scripts, which are not covered in this tutorial). In that case, we’re interested
in the add_cube command:

The add_cube command is enough since it automatically updates our
application to the cube’s schema. There are plenty of other migration
commands of a more finer grain. They are described in Migration

As explained, leave the shell by typing Ctrl-D. If you restart the instance and
take another look at the schema, you’ll see that changes to the data model have
actually been applied (meaning database schema updates and all necessary stuff
has been done).

If you follow the ‘info’ link in the user pop-up menu, you’ll also see that the
instance is using blog and myblog cubes.

You can now add some communities, link them to blog, etc… You’ll see that the
framework provides default views for this entity type (we have not yet defined any
view for it!), and also that the blog primary view will show the community it’s
linked to if any. All this thanks to the model driven interface provided by the
framework.

You’ll then be able to redefine each of them according to your needs
and preferences. We’ll now see how to do such thing.

CubicWeb provides a lot of standard views in directory
cubicweb/web/views/. We already talked about ‘primary’ and ‘list’ views,
which are views which apply to one ore more entities.

A view is defined by a python class which includes:

an identifier: all objects used to build the user interface in CubicWeb are
recorded in a registry and this identifier will be used as a key in that
registry. There may be multiple views for the same identifier.

a selector, which is a kind of filter telling how well a view suit to a
particular context. When looking for a particular view (e.g. given an
identifier), CubicWeb computes for each available view with that identifier
a score which is returned by the selector. Then the view with the highest
score is used. The standard library of predicates is in
cubicweb.predicates.

A view has a set of methods inherited from the cubicweb.view.View class,
though you usually don’t derive directly from this class but from one of its more
specific child class.

Last but not least, CubicWeb provides a set of default views accepting any kind
of entities.

Want a proof? Create a community as you’ve already done for other entity types
through the index page, you’ll then see something like that:

If you notice the weird messages that appear in the page: those are messages
generated for the new data model, which have no translation yet. To fix that,
we’ll have to use dedicated cubicweb-ctl commands:

You’ll then be able to redefine each of them according to your needs and
preferences. So let’s see how to do such thing.

The layout is the general organization of the pages in the site. Views that generate
the layout are sometimes referred to as ‘templates’. They are implemented in the
framework in the module cubicweb.web.views.basetemplates. By overriding
classes in this module, you can customize whatever part you wish of the default
layout.

But notice that CubicWeb provides many other ways to customize the
interface, thanks to actions and components (which you can individually
(de)activate, control their location, customize their look…) as well as
“simple” CSS customization. You should first try to achieve your goal using such
fine grained parametrization rather then overriding a whole template, which usually
embeds customisation access points that you may loose in the process.

But for the sake of example, let’s say we want to change the generic page
footer… We can simply add to the module views of our cube,
e.g. cubes/myblog/views.py, the code below:

fromcubicweb.web.viewsimportbasetemplatesclassMyHTMLPageFooter(basetemplates.HTMLPageFooter):deffooter_content(self):self.w(u'This website has been created with <a href="http://cubicweb.org">CubicWeb</a>.')defregistration_callback(vreg):vreg.register_all(globals().values(),__name__,(MyHTMLPageFooter,))vreg.register_and_replace(MyHTMLPageFooter,basetemplates.HTMLPageFooter)

Our class inherits from the default page footer to ease getting things right,
but this is not mandatory.

When we want to write something to the output stream, we simply call self.w,
which must be passed a unicode string.

The latest function is the most exotic stuff. The point is that without it, you
would get an error at display time because the framework wouldn’t be able to
choose which footer to use between HTMLPageFooter and
MyHTMLPageFooter, since both have the same selector, hence the same
score… In this case, we want our footer to replace the default one, so we have
to define a registration_callback() function to control object
registration: the first instruction tells to register everything in the module
but the MyHTMLPageFooter class, then the second to register it instead
of HTMLPageFooter. Without this function, everything in the module is
registered blindly.

Note

When a view is modified while running in debug mode, it is not required to
restart the instance server. Save the Python file and reload the page in your
web browser to view the changes.

The ‘primary’ view (i.e. any view with the identifier set to ‘primary’) is the one used to
display all the information about a single entity. The standard primary view is one
of the most sophisticated views of all. It has several customisation points, but
its power comes with uicfg, allowing you to control it without having to
subclass it.

However this is a bit off-topic for this first tutorial. Let’s say we simply want a
custom primary view for my Community entity type, using directly the view
interface without trying to benefit from the default implementation (you should
do that though if you’re rewriting reusable cubes; everything is described in more
details in The Primary View).

So… Some code! That we’ll put again in the module views of our cube.

fromcubicweb.predicatesimportis_instancefromcubicweb.web.viewsimportprimaryclassCommunityPrimaryView(primary.PrimaryView):__select__=is_instance('Community')defcell_call(self,row,col):entity=self.cw_rset.get_entity(row,col)self.w(u'<h1>Welcome to the "%s" community</h1>'%entity.printable_value('name'))ifentity.description:self.w(u'<p>%s</p>'%entity.printable_value('description'))

What’s going on here?

Our class inherits from the default primary view, here mainly to get the correct
view identifier, since we don’t use any of its features.

We set on it a selector telling that it only applies when trying to display
some entity of the Community type. This is enough to get an higher score than
the default view for entities of this type.

View applying to entities usually have to define cell_call as entry point,
and are given row and col arguments tell to which entity in the result set
the view is applied. We can then get this entity from the result set
(self.cw_rset) by using the get_entity method.

To ease thing, we access our entity’s attribute for display using its
printable_value method, which will handle formatting and escaping when
necessary. As you can see, you can also access attributes by their name on the
entity to get the raw value.

You can now reload the page of the community we just created and see the changes.

We’ve seen here a lot of thing you’ll have to deal with to write views in
CubicWeb. The good news is that this is almost everything that is used to
build higher level layers.

Note

As things get complicated and the volume of code in your cube increases, you can
of course still split your views module into a python package with subpackages.

CubicWeb provides an ORM to easily programmaticaly manipulate
entities (just like the one we have fetched earlier by calling
get_entity on a result set). By default, entity
types are instances of the AnyEntity class, which holds a set of
predefined methods as well as property automatically generated for
attributes/relations of the type it represents.

You can redefine each entity to provide additional methods or whatever you want
to help you write your application. Customizing an entity requires that your
entity:

inherits from cubicweb.entities.AnyEntity or any subclass

defines a __regid__ linked to the corresponding data type of your schema

You may then want to add your own methods, override default implementation of some
method, etc…

fromcubicweb.entitiesimportAnyEntity,fetch_configclassCommunity(AnyEntity):"""customized class for Community entities"""__regid__='Community'fetch_attrs,cw_fetch_order=fetch_config(['name'])defdc_title(self):returnself.namedefdisplay_cw_logo(self):return'CubicWeb'inself.description

In this example:

we used convenience fetch_config() function to tell which attributes
should be prefetched by the ORM when looking for some related entities of this
type, and how they should be ordered

we overrode the standard dc_title method, used in various place in the interface
to display the entity (though in this case the default implementation would
have had the same result)

we implemented here a method display_cw_logo() which tests if the blog
entry title contains ‘CW’. It can then be used when you’re writing code
involving ‘Community’ entities in your views, hooks, etc. For instance, you can
modify your previous views as follows:

classCommunityPrimaryView(primary.PrimaryView):__select__=is_instance('Community')defcell_call(self,row,col):entity=self.cw_rset.get_entity(row,col)self.w(u'<h1>Welcome to the "%s" community</h1>'%entity.printable_value('name'))ifentity.display_cw_logo():self.w(u'<img src="https://docs.cubicweb.org/_static/logo-cubicweb-small.svg"/>')ifentity.description:self.w(u'<p>%s</p>'%entity.printable_value('description'))

Then each community whose description contains ‘CW’ is shown with the CubicWeb
logo in front of it.

Note

As for view, you don’t have to restart your instance when modifying some entity
classes while your server is running in debug mode, the code will be
automatically reloaded.

One of the goal of the CubicWeb framework was to have truly reusable
components. To do so, they must both behave nicely when plugged into the
application and be easily customisable, from the data model to the user
interface. And I think the result is pretty successful, thanks to system such as
the selection mechanism and the choice to write views as python code which allows
to build our page using true object oriented programming techniques, that no
template language provides.

A library of standard cubes is available from CubicWeb Forge, to address a
lot of common concerns such has manipulating people, files, things to do, etc. In
our community blog case, we could be interested for instance in functionalities
provided by the comment and tag cubes. The former provides threaded
discussion functionalities, the latter a simple tag mechanism to classify content.
Let’s say we want to try those. We will first modify our cube’s __pkginfo__.py
file:

So in the case above we activated comments on BlogEntry entities and tags on
both Community and BlogEntry. Various views from both comment and tag
cubes will then be automatically displayed when one of those relations is
supported.

As you can see, we now have a box displaying tags and a section proposing to add
a comment and displaying existing one below the post. All this without changing
anything in our views, thanks to the design of generic views provided by the
framework. Though if we take a look at a community, we won’t see the tags box!
That’s because by default this box try to locate itself in the left column within
the white frame, and this column is handled by the primary view we
hijacked. Let’s change our view to make it more extensible, by keeping both our
custom rendering but also extension points provided by the default
implementation.

classCommunityPrimaryView(primary.PrimaryView):__select__=is_instance('Community')defrender_entity_title(self,entity):self.w(u'<h1>Welcome to the "%s" community</h1>'%entity.printable_value('name'))defrender_entity_attributes(self,entity):ifentity.display_cw_logo():self.w(u'<img src="https://docs.cubicweb.org/_static/logo-cubicweb-small.svg"/>')ifentity.description:self.w(u'<p>%s</p>'%entity.printable_value('description'))

It appears now properly:

You can control part of the interface independently from each others, piece by
piece. Really.