doc/book/en/tutorials/base/customizing-the-application.rst
changeset 10491 c67bcee93248
parent 10490 76ab3c71aff2
child 10492 68c13e0c0fc5
equal deleted inserted replaced
10490:76ab3c71aff2 10491:c67bcee93248
     1 .. -*- coding: utf-8 -*-
       
     2 
       
     3 .. _TutosBaseCustomizingTheApplication:
       
     4 
       
     5 Customizing your application
       
     6 ----------------------------
       
     7 
       
     8 So far so good. The point is that usually, you won't get enough by assembling
       
     9 cubes out-of-the-box. You will want to customize them, have a personal look and
       
    10 feel, add your own data model and so on. Or maybe start from scratch?
       
    11 
       
    12 So let's get a bit deeper and start coding our own cube. In our case, we want
       
    13 to customize the blog we created to add more features to it.
       
    14 
       
    15 
       
    16 Create your own cube
       
    17 ~~~~~~~~~~~~~~~~~~~~
       
    18 
       
    19 First, notice that if you've installed |cubicweb| using Debian packages, you will
       
    20 need the additional ``cubicweb-dev`` package to get the commands necessary to
       
    21 |cubicweb| development. All `cubicweb-ctl` commands are described in details in
       
    22 :ref:`cubicweb-ctl`.
       
    23 
       
    24 Once your |cubicweb| development environment is set up, you can create a new
       
    25 cube::
       
    26 
       
    27   cubicweb-ctl newcube myblog
       
    28 
       
    29 This will create in the cubes directory (:file:`/path/to/grshell/cubes` for source
       
    30 installation, :file:`/usr/share/cubicweb/cubes` for Debian packages installation)
       
    31 a directory named :file:`blog` reflecting the structure described in
       
    32 :ref:`cubelayout`.
       
    33 
       
    34 For packages installation, you can still create new cubes in your home directory
       
    35 using the following configuration. Let's say you want to develop your new cubes
       
    36 in `~src/cubes`, then set the following environment variables: ::
       
    37 
       
    38   CW_CUBES_PATH=~/src/cubes
       
    39 
       
    40 and then create your new cube using: ::
       
    41 
       
    42   cubicweb-ctl newcube --directory=~/src/cubes myblog
       
    43 
       
    44 .. Note::
       
    45 
       
    46    We previously used `myblog` as the name of our *instance*. We're now creating
       
    47    a *cube* with the same name. Both are different things. We'll now try to
       
    48    specify when we talk about one or another, but keep in mind this difference.
       
    49 
       
    50 
       
    51 Cube metadata
       
    52 ~~~~~~~~~~~~~
       
    53 
       
    54 A simple set of metadata about your cube are stored in the :file:`__pkginfo__.py`
       
    55 file. In our case, we want to extend the blog cube, so we have to tell that our
       
    56 cube depends on this cube, by modifying the ``__depends__`` dictionary in that
       
    57 file:
       
    58 
       
    59 .. sourcecode:: python
       
    60 
       
    61    __depends__ =  {'cubicweb': '>= 3.10.7',
       
    62                    'cubicweb-blog': None}
       
    63 
       
    64 where the ``None`` means we do not depends on a particular version of the cube.
       
    65 
       
    66 .. _TutosBaseCustomizingTheApplicationDataModel:
       
    67 
       
    68 Extending the data model
       
    69 ~~~~~~~~~~~~~~~~~~~~~~~~
       
    70 
       
    71 The data model or schema is the core of your |cubicweb| application.  It defines
       
    72 the type of content your application will handle. It is defined in the file
       
    73 :file:`schema.py` of the cube.
       
    74 
       
    75 
       
    76 Defining our model
       
    77 ******************
       
    78 
       
    79 For the sake of example, let's say we want a new entity type named `Community`
       
    80 with a name, a description. A `Community` will hold several blogs.
       
    81 
       
    82 .. sourcecode:: python
       
    83 
       
    84   from yams.buildobjs import EntityType, RelationDefinition, String, RichString
       
    85 
       
    86   class Community(EntityType):
       
    87       name = String(maxsize=50, required=True)
       
    88       description = RichString()
       
    89 
       
    90   class community_blog(RelationDefinition):
       
    91       subject = 'Community'
       
    92       object = 'Blog'
       
    93       cardinality = '*?'
       
    94       composite = 'subject'
       
    95 
       
    96 The first step is the import from the :mod:`yams` package necessary classes to build
       
    97 the schema.
       
    98 
       
    99 This file defines the following:
       
   100 
       
   101 * a `Community` has a title and a description as attributes
       
   102 
       
   103   - the name is a string that is required and can't be longer than 50 characters
       
   104 
       
   105   - the description is a string that is not constrained and may contains rich
       
   106     content such as HTML or Restructured text.
       
   107 
       
   108 * a `Community` may be linked to a `Blog` using the `community_blog` relation
       
   109 
       
   110   - ``*`` means a community may be linked to 0 to N blog, ``?`` means a blog may
       
   111     be linked to 0 to 1 community. For completeness, remember that you can also
       
   112     use ``+`` for 1 to N, and ``1`` for single, mandatory relation (e.g. one to one);
       
   113 
       
   114   - this is a composite relation where `Community` (e.g. the subject of the
       
   115     relation) is the composite. That means that if you delete a community, its
       
   116     blog will be deleted as well.
       
   117 
       
   118 Of course, there are a lot of other data types and things such as constraints,
       
   119 permissions, etc, that may be defined in the schema, but those won't be covered
       
   120 in this tutorial.
       
   121 
       
   122 Notice that our schema refers to the `Blog` entity type which is not defined
       
   123 here.  But we know this type is available since we depend on the `blog` cube
       
   124 which is defining it.
       
   125 
       
   126 
       
   127 Applying changes to the model into our instance
       
   128 ***********************************************
       
   129 
       
   130 Now the problem is that we created an instance using the `blog` cube, not our
       
   131 `myblog` cube, so if we don't do anything there is no way that we'll see anything
       
   132 changing in the instance.
       
   133 
       
   134 One easy way, as we've no really valuable data in the instance would be to trash and recreated it::
       
   135 
       
   136   cubicweb-ctl stop myblog # or Ctrl-C in the terminal running the server in debug mode
       
   137   cubicweb-ctl delete myblog
       
   138   cubicweb-ctl create myblog
       
   139   cubicweb-ctl start -D myblog
       
   140 
       
   141 Another way is to add our cube to the instance using the cubicweb-ctl shell
       
   142 facility. It's a python shell connected to the instance with some special
       
   143 commands available to manipulate it (the same as you'll have in migration
       
   144 scripts, which are not covered in this tutorial). In that case, we're interested
       
   145 in the `add_cube` command: ::
       
   146 
       
   147   $ cubicweb-ctl stop myblog # or Ctrl-C in the terminal running the server in debug mode
       
   148   $ cubicweb-ctl shell myblog
       
   149   entering the migration python shell
       
   150   just type migration commands or arbitrary python code and type ENTER to execute it
       
   151   type "exit" or Ctrl-D to quit the shell and resume operation
       
   152   >>> add_cube('myblog')
       
   153   >>>
       
   154   $ cubicweb-ctl start -D myblog
       
   155 
       
   156 The `add_cube` command is enough since it automatically updates our
       
   157 application to the cube's schema. There are plenty of other migration
       
   158 commands of a more finer grain. They are described in :ref:`migration`
       
   159 
       
   160 As explained, leave the shell by typing Ctrl-D. If you restart the instance and
       
   161 take another look at the schema, you'll see that changes to the data model have
       
   162 actually been applied (meaning database schema updates and all necessary stuff
       
   163 has been done).
       
   164 
       
   165 .. image:: ../../images/tutos-base_myblog-schema_en.png
       
   166    :alt: the instance schema after adding our cube
       
   167 
       
   168 If you follow the 'info' link in the user pop-up menu, you'll also see that the
       
   169 instance is using blog and myblog cubes.
       
   170 
       
   171 .. image:: ../../images/tutos-base_myblog-siteinfo_en.png
       
   172    :alt: the instance schema after adding our cube
       
   173 
       
   174 You can now add some communities, link them to blog, etc... You'll see that the
       
   175 framework provides default views for this entity type (we have not yet defined any
       
   176 view for it!), and also that the blog primary view will show the community it's
       
   177 linked to if any. All this thanks to the model driven interface provided by the
       
   178 framework.
       
   179 
       
   180 You'll then be able to redefine each of them according to your needs
       
   181 and preferences. We'll now see how to do such thing.
       
   182 
       
   183 .. _TutosBaseCustomizingTheApplicationCustomViews:
       
   184 
       
   185 Defining your views
       
   186 ~~~~~~~~~~~~~~~~~~~
       
   187 
       
   188 |cubicweb| provides a lot of standard views in directory
       
   189 :file:`cubicweb/web/views/`. We already talked about 'primary' and 'list' views,
       
   190 which are views which apply to one ore more entities.
       
   191 
       
   192 A view is defined by a python class which includes:
       
   193 
       
   194   - an identifier: all objects used to build the user interface in |cubicweb| are
       
   195     recorded in a registry and this identifier will be used as a key in that
       
   196     registry. There may be multiple views for the same identifier.
       
   197 
       
   198   - a *selector*, which is a kind of filter telling how well a view suit to a
       
   199     particular context. When looking for a particular view (e.g. given an
       
   200     identifier), |cubicweb| computes for each available view with that identifier
       
   201     a score which is returned by the selector. Then the view with the highest
       
   202     score is used. The standard library of predicates is in
       
   203     :mod:`cubicweb.predicates`.
       
   204 
       
   205 A view has a set of methods inherited from the :class:`cubicweb.view.View` class,
       
   206 though you usually don't derive directly from this class but from one of its more
       
   207 specific child class.
       
   208 
       
   209 Last but not least, |cubicweb| provides a set of default views accepting any kind
       
   210 of entities.
       
   211 
       
   212 Want a proof? Create a community as you've already done for other entity types
       
   213 through the index page, you'll then see something like that:
       
   214 
       
   215 .. image:: ../../images/tutos-base_myblog-community-default-primary_en.png
       
   216    :alt: the default primary view for our community entity type
       
   217 
       
   218 
       
   219 If you notice the weird messages that appear in the page: those are messages
       
   220 generated for the new data model, which have no translation yet. To fix that,
       
   221 we'll have to use dedicated `cubicweb-ctl` commands:
       
   222 
       
   223 .. sourcecode: bash
       
   224 
       
   225   cubicweb-ctl i18ncube myblog # build/update cube's message catalogs
       
   226   # then add translation into .po file into the cube's i18n directory
       
   227   cubicweb-ctl i18ninstance myblog # recompile instance's message catalogs
       
   228   cubicweb-ctl restart -D myblog # instance has to be restarted to consider new catalogs
       
   229 
       
   230 You'll then be able to redefine each of them according to your needs and
       
   231 preferences. So let's see how to do such thing.
       
   232 
       
   233 Changing the layout of the application
       
   234 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
       
   235 
       
   236 The layout is the general organization of the pages in the site. Views that generate
       
   237 the layout are sometimes referred to as 'templates'. They are implemented in the
       
   238 framework in the module :mod:`cubicweb.web.views.basetemplates`. By overriding
       
   239 classes in this module, you can customize whatever part you wish of the default
       
   240 layout.
       
   241 
       
   242 But notice that |cubicweb| provides many other ways to customize the
       
   243 interface, thanks to actions and components (which you can individually
       
   244 (de)activate, control their location, customize their look...) as well as
       
   245 "simple" CSS customization. You should first try to achieve your goal using such
       
   246 fine grained parametrization rather then overriding a whole template, which usually
       
   247 embeds customisation access points that you may loose in the process.
       
   248 
       
   249 But for the sake of example, let's say we want to change the generic page
       
   250 footer...  We can simply add to the module ``views`` of our cube,
       
   251 e.g. :file:`cubes/myblog/views.py`, the code below:
       
   252 
       
   253 .. sourcecode:: python
       
   254 
       
   255   from cubicweb.web.views import basetemplates
       
   256 
       
   257   class MyHTMLPageFooter(basetemplates.HTMLPageFooter):
       
   258 
       
   259       def footer_content(self):
       
   260 	  self.w(u'This website has been created with <a href="http://cubicweb.org">CubicWeb</a>.')
       
   261 
       
   262   def registration_callback(vreg):
       
   263       vreg.register_all(globals().values(), __name__, (MyHTMLPageFooter,))
       
   264       vreg.register_and_replace(MyHTMLPageFooter, basetemplates.HTMLPageFooter)
       
   265 
       
   266 
       
   267 * Our class inherits from the default page footer to ease getting things right,
       
   268   but this is not mandatory.
       
   269 
       
   270 * When we want to write something to the output stream, we simply call `self.w`,
       
   271   which *must be passed a unicode string*.
       
   272 
       
   273 * The latest function is the most exotic stuff. The point is that without it, you
       
   274   would get an error at display time because the framework wouldn't be able to
       
   275   choose which footer to use between :class:`HTMLPageFooter` and
       
   276   :class:`MyHTMLPageFooter`, since both have the same selector, hence the same
       
   277   score...  In this case, we want our footer to replace the default one, so we have
       
   278   to define a :func:`registration_callback` function to control object
       
   279   registration: the first instruction tells to register everything in the module
       
   280   but the :class:`MyHTMLPageFooter` class, then the second to register it instead
       
   281   of :class:`HTMLPageFooter`. Without this function, everything in the module is
       
   282   registered blindly.
       
   283 
       
   284 .. Note::
       
   285 
       
   286   When a view is modified while running in debug mode, it is not required to
       
   287   restart the instance server. Save the Python file and reload the page in your
       
   288   web browser to view the changes.
       
   289 
       
   290 We will now have this simple footer on every page of the site.
       
   291 
       
   292 
       
   293 Primary view customization
       
   294 ~~~~~~~~~~~~~~~~~~~~~~~~~~
       
   295 
       
   296 The 'primary' view (i.e. any view with the identifier set to 'primary') is the one used to
       
   297 display all the information about a single entity. The standard primary view is one
       
   298 of the most sophisticated views of all. It has several customisation points, but
       
   299 its power comes with `uicfg`, allowing you to control it without having to
       
   300 subclass it.
       
   301 
       
   302 However this is a bit off-topic for this first tutorial. Let's say we simply want a
       
   303 custom primary view for my `Community` entity type, using directly the view
       
   304 interface without trying to benefit from the default implementation (you should
       
   305 do that though if you're rewriting reusable cubes; everything is described in more
       
   306 details in :ref:`primary_view`).
       
   307 
       
   308 
       
   309 So... Some code! That we'll put again in the module ``views`` of our cube.
       
   310 
       
   311 .. sourcecode:: python
       
   312 
       
   313   from cubicweb.predicates import is_instance
       
   314   from cubicweb.web.views import primary
       
   315 
       
   316   class CommunityPrimaryView(primary.PrimaryView):
       
   317       __select__ = is_instance('Community')
       
   318 
       
   319       def cell_call(self, row, col):
       
   320           entity = self.cw_rset.get_entity(row, col)
       
   321           self.w(u'<h1>Welcome to the "%s" community</h1>' % entity.printable_value('name'))
       
   322           if entity.description:
       
   323               self.w(u'<p>%s</p>' % entity.printable_value('description'))
       
   324 
       
   325 What's going on here?
       
   326 
       
   327 * Our class inherits from the default primary view, here mainly to get the correct
       
   328   view identifier, since we don't use any of its features.
       
   329 
       
   330 * We set on it a selector telling that it only applies when trying to display
       
   331   some entity of the `Community` type. This is enough to get an higher score than
       
   332   the default view for entities of this type.
       
   333 
       
   334 * View applying to entities usually have to define `cell_call` as entry point,
       
   335   and are given `row` and `col` arguments tell to which entity in the result set
       
   336   the view is applied. We can then get this entity from the result set
       
   337   (`self.cw_rset`) by using the `get_entity` method.
       
   338 
       
   339 * To ease thing, we access our entity's attribute for display using its
       
   340   printable_value method, which will handle formatting and escaping when
       
   341   necessary. As you can see, you can also access attributes by their name on the
       
   342   entity to get the raw value.
       
   343 
       
   344 
       
   345 You can now reload the page of the community we just created and see the changes.
       
   346 
       
   347 .. image:: ../../images/tutos-base_myblog-community-custom-primary_en.png
       
   348    :alt: the custom primary view for our community entity type
       
   349 
       
   350 We've seen here a lot of thing you'll have to deal with to write views in
       
   351 |cubicweb|. The good news is that this is almost everything that is used to
       
   352 build higher level layers.
       
   353 
       
   354 .. Note::
       
   355 
       
   356   As things get complicated and the volume of code in your cube increases, you can
       
   357   of course still split your views module into a python package with subpackages.
       
   358 
       
   359 You can find more details about views and selectors in :ref:`Views`.
       
   360 
       
   361 
       
   362 Write entities to add logic in your data
       
   363 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
       
   364 
       
   365 |cubicweb| provides an ORM to easily programmaticaly manipulate
       
   366 entities (just like the one we have fetched earlier by calling
       
   367 `get_entity` on a result set). By default, entity
       
   368 types are instances of the :class:`AnyEntity` class, which holds a set of
       
   369 predefined methods as well as property automatically generated for
       
   370 attributes/relations of the type it represents.
       
   371 
       
   372 You can redefine each entity to provide additional methods or whatever you want
       
   373 to help you write your application. Customizing an entity requires that your
       
   374 entity:
       
   375 
       
   376 - inherits from :class:`cubicweb.entities.AnyEntity` or any subclass
       
   377 
       
   378 - defines a :attr:`__regid__` linked to the corresponding data type of your schema
       
   379 
       
   380 You may then want to add your own methods, override default implementation of some
       
   381 method, etc...
       
   382 
       
   383 .. sourcecode:: python
       
   384 
       
   385     from cubicweb.entities import AnyEntity, fetch_config
       
   386 
       
   387 
       
   388     class Community(AnyEntity):
       
   389         """customized class for Community entities"""
       
   390         __regid__ = 'Community'
       
   391 
       
   392         fetch_attrs, cw_fetch_order = fetch_config(['name'])
       
   393 
       
   394         def dc_title(self):
       
   395             return self.name
       
   396 
       
   397         def display_cw_logo(self):
       
   398             return 'CubicWeb' in self.description
       
   399 
       
   400 In this example:
       
   401 
       
   402 * we used convenience :func:`fetch_config` function to tell which attributes
       
   403   should be prefetched by the ORM when looking for some related entities of this
       
   404   type, and how they should be ordered
       
   405 
       
   406 * we overrode the standard `dc_title` method, used in various place in the interface
       
   407   to display the entity (though in this case the default implementation would
       
   408   have had the same result)
       
   409 
       
   410 * we implemented here a method :meth:`display_cw_logo` which tests if the blog
       
   411   entry title contains 'CW'.  It can then be used when you're writing code
       
   412   involving 'Community' entities in your views, hooks, etc. For instance, you can
       
   413   modify your previous views as follows:
       
   414 
       
   415 .. sourcecode:: python
       
   416 
       
   417 
       
   418   class CommunityPrimaryView(primary.PrimaryView):
       
   419       __select__ = is_instance('Community')
       
   420 
       
   421       def cell_call(self, row, col):
       
   422           entity = self.cw_rset.get_entity(row, col)
       
   423           self.w(u'<h1>Welcome to the "%s" community</h1>' % entity.printable_value('name'))
       
   424           if entity.display_cw_logo():
       
   425               self.w(u'<img src="http://www.cubicweb.org/doc/en/_static/cubicweb.png"/>')
       
   426           if entity.description:
       
   427               self.w(u'<p>%s</p>' % entity.printable_value('description'))
       
   428 
       
   429 Then each community whose description contains 'CW' is shown with the |cubicweb|
       
   430 logo in front of it.
       
   431 
       
   432 .. Note::
       
   433 
       
   434   As for view, you don't have to restart your instance when modifying some entity
       
   435   classes while your server is running in debug mode, the code will be
       
   436   automatically reloaded.
       
   437 
       
   438 
       
   439 Extending the application by using more cubes!
       
   440 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
       
   441 
       
   442 One of the goal of the |cubicweb| framework was to have truly reusable
       
   443 components. To do so, they must both behave nicely when plugged into the
       
   444 application and be easily customisable, from the data model to the user
       
   445 interface. And I think the result is pretty successful, thanks to system such as
       
   446 the selection mechanism and the choice to write views as python code which allows
       
   447 to build our page using true object oriented programming techniques, that no
       
   448 template language provides.
       
   449 
       
   450 
       
   451 A library of standard cubes is available from `CubicWeb Forge`_, to address a
       
   452 lot of common concerns such has manipulating people, files, things to do, etc. In
       
   453 our community blog case, we could be interested for instance in functionalities
       
   454 provided by the `comment` and `tag` cubes. The former provides threaded
       
   455 discussion functionalities, the latter a simple tag mechanism to classify content.
       
   456 Let's say we want to try those. We will first modify our cube's :file:`__pkginfo__.py`
       
   457 file:
       
   458 
       
   459 .. sourcecode:: python
       
   460 
       
   461    __depends__ =  {'cubicweb': '>= 3.10.7',
       
   462                    'cubicweb-blog': None,
       
   463                    'cubicweb-comment': None,
       
   464                    'cubicweb-tag': None}
       
   465 
       
   466 Now, we'll simply tell on which entity types we want to activate the 'comment'
       
   467 and 'tag' facilities by adding respectively the 'comments' and 'tags' relations on
       
   468 them in our schema (:file:`schema.py`).
       
   469 
       
   470 .. sourcecode:: python
       
   471 
       
   472   class comments(RelationDefinition):
       
   473       subject = 'Comment'
       
   474       object = 'BlogEntry'
       
   475       cardinality = '1*'
       
   476       composite = 'object'
       
   477 
       
   478   class tags(RelationDefinition):
       
   479       subject = 'Tag'
       
   480       object = ('Community', 'BlogEntry')
       
   481 
       
   482 
       
   483 So in the case above we activated comments on `BlogEntry` entities and tags on
       
   484 both `Community` and `BlogEntry`. Various views from both `comment` and `tag`
       
   485 cubes will then be automatically displayed when one of those relations is
       
   486 supported.
       
   487 
       
   488 Let's synchronize the data model as we've done earlier: ::
       
   489 
       
   490 
       
   491   $ cubicweb-ctl stop myblog
       
   492   $ cubicweb-ctl shell myblog
       
   493   entering the migration python shell
       
   494   just type migration commands or arbitrary python code and type ENTER to execute it
       
   495   type "exit" or Ctrl-D to quit the shell and resume operation
       
   496   >>> add_cubes(('comment', 'tag'))
       
   497   >>>
       
   498 
       
   499 Then restart the instance. Let's look at a blog entry:
       
   500 
       
   501 .. image:: ../../images/tutos-base_myblog-blogentry-taggable-commentable-primary_en.png
       
   502    :alt: the primary view for a blog entry with comments and tags activated
       
   503 
       
   504 As you can see, we now have a box displaying tags and a section proposing to add
       
   505 a comment and displaying existing one below the post. All this without changing
       
   506 anything in our views, thanks to the design of generic views provided by the
       
   507 framework. Though if we take a look at a community, we won't see the tags box!
       
   508 That's because by default this box try to locate itself in the left column within
       
   509 the white frame, and this column is handled by the primary view we
       
   510 hijacked. Let's change our view to make it more extensible, by keeping both our
       
   511 custom rendering but also extension points provided by the default
       
   512 implementation.
       
   513 
       
   514 
       
   515 .. sourcecode:: python
       
   516 
       
   517   class CommunityPrimaryView(primary.PrimaryView):
       
   518       __select__ = is_instance('Community')
       
   519 
       
   520       def render_entity_title(self, entity):
       
   521 	  self.w(u'<h1>Welcome to the "%s" community</h1>' % entity.printable_value('name'))
       
   522 
       
   523       def render_entity_attributes(self, entity):
       
   524 	  if entity.display_cw_logo():
       
   525 	      self.w(u'<img src="http://www.cubicweb.org/doc/en/_static/cubicweb.png"/>')
       
   526 	  if entity.description:
       
   527 	      self.w(u'<p>%s</p>' % entity.printable_value('description'))
       
   528 
       
   529 It appears now properly:
       
   530 
       
   531 .. image:: ../../images/tutos-base_myblog-community-taggable-primary_en.png
       
   532    :alt: the custom primary view for a community entry with tags activated
       
   533 
       
   534 You can control part of the interface independently from each others, piece by
       
   535 piece. Really.
       
   536 
       
   537 
       
   538 
       
   539 .. _`CubicWeb Forge`: http://www.cubicweb.org/project