doc/book/en/tutorials/advanced/part05_ui-advanced.rst
branchstable
changeset 6876 4b0b9d8207c5
child 6877 0e8fc441b38b
equal deleted inserted replaced
6875:a166b51d13f8 6876:4b0b9d8207c5
       
     1 Building my photos web site with |cubicweb| part V: let's make it even more user friendly
       
     2 =========================================================================================
       
     3 
       
     4 We'll now see how to benefit from features introduced in 3.9 and 3.10 releases of cubicweb
       
     5 
       
     6 Step 1: Tired of the default look?
       
     7 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
       
     8 
       
     9 Ok... Now our site has most desired features. But... I would like to make it look
       
    10 somewhat like *my* website, That's not cubicweb.org after all. Let's tackle this
       
    11 first!
       
    12 
       
    13 The first thing we can to is to change the logo. There are various way to achieve
       
    14 this. The easiest way is to put a `logo.png` file into the cube's `data`
       
    15 directory. As data files are looked at according to cubes order (cubicweb
       
    16 resources coming last). The first one being picked, the file will be selected
       
    17 instead of cubicweb's one.
       
    18 
       
    19 .. Note::
       
    20    As the location for static resources are cached, you'll have to restart
       
    21    your instance for this to be taken into account.
       
    22 
       
    23 Though there are some cases where you don't want to use a `logo.png` file.  For
       
    24 instance if it's a JPEG file. You can still change the logo by defining in the
       
    25 cube's `:file:`uiprops.py`` file:
       
    26 
       
    27 .. sourcecode:: python
       
    28 
       
    29    LOGO = data('logo.jpg')
       
    30 
       
    31 The uiprops machinery has been introduced in `CubicWeb 3.9`_. It's used to define
       
    32 some static file resources, such as the logo, default javascript / CSS files, as
       
    33 well as CSS properties (we'll see that later).
       
    34 
       
    35 .. Note::
       
    36    This file is imported specifically by |cubicweb|, with a predefined name space,
       
    37    containing for instance the `data` function, telling the file is somewhere
       
    38    in a cube or cubicweb's data directory.
       
    39 
       
    40    One side effect of this is that it can't be imported as a regular python
       
    41    module.
       
    42 
       
    43 The nice thing is that in debug mode, change to a :file:`uiprops.py` file are detected
       
    44 and then automatically reloaded.
       
    45 
       
    46 Now, as it's a photos web-site, I would like to have a photo of mine as background...
       
    47 After some trials I won't detail here, I've found a working recipe explained `here`_.
       
    48 All I've to do is to override some stuff of the default cubicweb user interface to
       
    49 apply it as explained.
       
    50 
       
    51 The first thing to to get the "<img/>" tag as first element after the "<body>"
       
    52 tag.  If you know a way to avoid this by simply specifying the image in the CSS,
       
    53 tell me!  The easiest way to do so is to override the `HTMLPageHeader` view,
       
    54 since that's the one that is directly called once the "<body>" has been written
       
    55 . How did I find this?  By looking in the `cubiweb.web.views.basetemplates`
       
    56 module, since I know that global page layouts sits there. I could also have
       
    57 grep the "body" tag in `cubicweb.web.views`... Finding this was the hardest
       
    58 part. Now all I need is to customize it to write that "img" tag, as below:
       
    59 
       
    60 .. sourcecode:: python
       
    61 
       
    62     class HTMLPageHeader(basetemplates.HTMLPageHeader):
       
    63 	# override this since it's the easier way to have our bg image
       
    64 	# as the first element following <body>
       
    65 	def call(self, **kwargs):
       
    66             self.w(u'<img id="bg-image" src="%sbackground.jpg" alt="background image"/>'
       
    67                    % self._cw.datadir_url)
       
    68 	    super(HTMLPageHeader, self).call(**kwargs)
       
    69 
       
    70 
       
    71     def registration_callback(vreg):
       
    72 	vreg.register_all(globals().values(), __name__, (HTMLPageHeader))
       
    73 	vreg.register_and_replace(HTMLPageHeader, basetemplates.HTMLPageHeader)
       
    74 
       
    75 
       
    76 Besides that, as you may I've guess, my background image is in a `backgroundjpg`
       
    77 file in the cube's `data` directory, there are still some things to explain to
       
    78 newcomers here though.
       
    79 
       
    80 * The `call` method is there the main access point of the view. It's called by
       
    81   the view's `render` method. That's not the onlyu access point for a view, but
       
    82   that will be detailed later.
       
    83 
       
    84 * Calling `self.w` write something to the output stream. Except for binary view
       
    85   (not generating text), it *must* be an Unicode string.
       
    86 
       
    87 * The proper way to get a file in `data` directory is to use the `datadir_url`
       
    88   attribute of the incoming request (e.g. `self._cw`).
       
    89 
       
    90 I won't explain again the `registration_callback` stuff, you should understand it
       
    91 know!  If not, go back to previous posts in the series :)
       
    92 
       
    93 Fine. Now all I've to do is to add a bit of CSS to get it behaves nicely (that's
       
    94 not yet the case at all). I'll put all this in a `cubes.sytweb.css` file, as usual
       
    95 in our `data` directory:
       
    96 
       
    97 .. sourcecode:: css
       
    98 
       
    99 
       
   100     /* fixed full screen background image
       
   101      * as explained on http://webdesign.about.com/od/css3/f/blfaqbgsize.htm
       
   102      *
       
   103      * syt update: set z-index=0 on the img instead of z-index=1 on div#page & co to
       
   104      * avoid pb with the user actions menu
       
   105      */
       
   106     img#bg-image {
       
   107 	position: fixed;
       
   108 	top: 0;
       
   109 	left: 0;
       
   110 	width: 100%;
       
   111 	height: 100%;
       
   112 	z-index: 0;
       
   113     }
       
   114 
       
   115     div#page, table#header, div#footer {
       
   116 	background: transparent;
       
   117 	position: relative;
       
   118     }
       
   119 
       
   120     /* add some space around the logo
       
   121      */
       
   122     img#logo {
       
   123 	padding: 5px 15px 0px 15px;
       
   124     }
       
   125 
       
   126     /* more dark font for metadata to have a chance to see them with the background
       
   127      *  image
       
   128      */
       
   129     div.metadata {
       
   130 	color: black;
       
   131     }
       
   132 
       
   133 You can see here stuff explained in the cited page, with only a slight modification
       
   134 explained in the comments, plus some additional rules to make thing somewhat cleaner:
       
   135 
       
   136 * a bit of padding around the logo
       
   137 
       
   138 * darker metadata which appears by default below the content (the white frame in the page)
       
   139 
       
   140 To get this CSS file used everywhere in the site, I've to modify the :file:`uiprops.py` file
       
   141 we've encountered above:
       
   142 
       
   143 .. sourcecode:: python
       
   144 
       
   145    STYLESHEETS = sheet['STYLESHEETS'] + [data('cubes.sytweb.css')]
       
   146 
       
   147 .. Note:
       
   148    `sheet` is another predefined variable containing values defined by
       
   149    already process `:file:`uiprops.py`` file, notably the cubicweb's one.
       
   150 
       
   151 Here we simply want our CSS additionally to cubicweb's base CSS files, so we
       
   152 redefine the `STYLESHEETS` variable to existing CSS (accessed through the `sheet`
       
   153 variable) with our one added. I could also have done:
       
   154 
       
   155 .. sourcecode:: python
       
   156 
       
   157    sheet['STYLESHEETS'].append(data('cubes.sytweb.css'))
       
   158 
       
   159 But this is less interesting since we don't see the overriding mechanism...
       
   160 
       
   161 At this point, the site should start looking good, the background image being
       
   162 resized to fit the screen.
       
   163 
       
   164 .. image:: ../../images/tutos-photowebsite_background-image.png
       
   165 
       
   166 The final touch: let's customize cubicweb's CSS to get less orange... By simply adding
       
   167 
       
   168 .. sourcecode:: python
       
   169 
       
   170   contextualBoxTitleBg = incontextBoxTitleBg = '#AAAAAA'
       
   171 
       
   172 and reloading the page we've just seen, we know have a nice greyed box instead of
       
   173 the orange one:
       
   174 
       
   175 .. image:: ../../images/tutos-photowebsite_grey-box.png
       
   176 
       
   177 This is because cubicweb's CSS include some variables which are
       
   178 expanded by values defined in uiprops file. In our case we controlled the
       
   179 properties of the CSS `background` property of boxes with CSS class
       
   180 `contextualBoxTitleBg` and `incontextBoxTitleBg`.
       
   181 
       
   182 
       
   183 Step 2: configuring boxes
       
   184 ~~~~~~~~~~~~~~~~~~~~~~~~~
       
   185 Boxes present to the user some ways to use the application. Lets first do a few tweaks:
       
   186 
       
   187 .. sourcecode:: python
       
   188 
       
   189   from cubicweb.selectors import none_rset
       
   190   from cubicweb.web.views import bookmark
       
   191   from cubes.zone import views as zone
       
   192   from cubes.tag import views as tag
       
   193 
       
   194   # change bookmarks box selector so it's only displayed on startup view
       
   195 gro  bookmark.BookmarksBox.__select__ = bookmark.BookmarksBox.__select__ & none_rset()
       
   196   # move zone box to the left instead of in the context frame and tweak its order
       
   197   zone.ZoneBox.context = 'left'
       
   198   zone.ZoneBox.order = 100
       
   199   # move tags box to the left instead of in the context frame and tweak its order
       
   200   tag.TagsBox.context = 'left'
       
   201   tag.TagsBox.order = 102
       
   202   # hide similarity box, not interested
       
   203   tag.SimilarityBox.visible = False
       
   204 
       
   205 The idea is to move all boxes in the left column, so we get more spaces for the
       
   206 photos.  Now, serious things: I want a box similar as the tags box but to handle
       
   207 the `Person displayed_on File` relation. We can do this simply by configuring a
       
   208 :class:`AjaxEditRelationCtxComponent` subclass as below:
       
   209 
       
   210 .. sourcecode:: python
       
   211 
       
   212     from logilab.common.decorators import monkeypatch
       
   213     from cubicweb import ValidationError
       
   214     from cubicweb.web import uicfg, component
       
   215     from cubicweb.web.views import basecontrollers
       
   216 
       
   217     # hide displayed_on relation using uicfg since it will be displayed by the box below
       
   218     uicfg.primaryview_section.tag_object_of(('*', 'displayed_on', '*'), 'hidden')
       
   219 
       
   220     class PersonBox(component.AjaxEditRelationCtxComponent):
       
   221 	__regid__ = 'sytweb.displayed-on-box'
       
   222 	# box position
       
   223 	order = 101
       
   224 	context = 'left'
       
   225 	# define relation to be handled
       
   226 	rtype = 'displayed_on'
       
   227 	role = 'object'
       
   228 	target_etype = 'Person'
       
   229 	# messages
       
   230 	added_msg = _('person has been added')
       
   231 	removed_msg = _('person has been removed')
       
   232 	# bind to js_* methods of the json controller
       
   233 	fname_vocabulary = 'unrelated_persons'
       
   234 	fname_validate = 'link_to_person'
       
   235 	fname_remove = 'unlink_person'
       
   236 
       
   237 
       
   238     @monkeypatch(basecontrollers.JSonController)
       
   239     @basecontrollers.jsonize
       
   240     def js_unrelated_persons(self, eid):
       
   241 	"""return tag unrelated to an entity"""
       
   242 	rql = "Any F + ' ' + S WHERE P surname S, P firstname F, X eid %(x)s, NOT P displayed_on X"
       
   243 	return [name for (name,) in self._cw.execute(rql, {'x' : eid})]
       
   244 
       
   245 
       
   246     @monkeypatch(basecontrollers.JSonController)
       
   247     def js_link_to_person(self, eid, people):
       
   248 	req = self._cw
       
   249 	for name in people:
       
   250 	    name = name.strip().title()
       
   251 	    if not name:
       
   252 		continue
       
   253 	    try:
       
   254 		firstname, surname = name.split(None, 1)
       
   255 	    except:
       
   256 		raise ValidationError(eid, {('displayed_on', 'object'): 'provide <first name> <surname>'})
       
   257 	    rset = req.execute('Person P WHERE '
       
   258 			       'P firstname %(firstname)s, P surname %(surname)s',
       
   259 			       locals())
       
   260 	    if rset:
       
   261 		person = rset.get_entity(0, 0)
       
   262 	    else:
       
   263 		person = req.create_entity('Person', firstname=firstname,
       
   264 						surname=surname)
       
   265 	    req.execute('SET P displayed_on X WHERE '
       
   266 			'P eid %(p)s, X eid %(x)s, NOT P displayed_on X',
       
   267 			{'p': person.eid, 'x' : eid})
       
   268 
       
   269     @monkeypatch(basecontrollers.JSonController)
       
   270     def js_unlink_person(self, eid, personeid):
       
   271 	self._cw.execute('DELETE P displayed_on X WHERE P eid %(p)s, X eid %(x)s',
       
   272 			 {'p': personeid, 'x': eid})
       
   273 
       
   274 
       
   275 You basically subclass to configure by some class attributes. The `fname_*`
       
   276 attributes gives name of methods that should be defined on the json control to
       
   277 make the AJAX part of the widget working: one to get the vocabulary, one to add a
       
   278 relation and another to delete a relation. Those methods must start by a `ks_`
       
   279 prefix and are added to the controller using the `@monkeypatch` decorator.Here
       
   280 the most complicated is the one to add a relation, since it tries to see if the
       
   281 person already exists, and else automatically create it by supposing the user
       
   282 entered "firstname surname".
       
   283 
       
   284 Let's see how it looks like on a file primary view:
       
   285 
       
   286 .. image:: ../../images/tutos-photowebsite_boxes.png
       
   287 
       
   288 Great, it's now as easy for me to link my pictures to people than to tag them.
       
   289 Also, visitors get a consistent display of those two informations.
       
   290 
       
   291 .. note:
       
   292   The ui component system has been refactored in `CubicWeb 3.10`_, which also
       
   293   introduced the :class:`AjaxEditRelationCtxComponent` class.
       
   294 
       
   295 
       
   296 Step 3: configuring facets
       
   297 ~~~~~~~~~~~~~~~~~~~~~~~~~~
       
   298 
       
   299 The last feature we'll add today is facet configuration. If you access to the
       
   300 '/file' url, you'll see a set of 'facet' appearing in the left column. Facets
       
   301 provide an intuitive way to build a query incrementally, by proposing to the user
       
   302 various way to restrict result set. For instance cubicweb propose a facet to
       
   303 restrict according to who's created an entity; the tag cube a facet to restrict
       
   304 according to tags. I want to propose similarly a facet to restrict according to
       
   305 people displayed on the picture. To do so, there are various classes in the
       
   306 :mod:`cubicweb.web.facet` module which you've simple to configure using class
       
   307 attributes as we've done for the box. In our case, we'll define a subclass of
       
   308 :class:`RelationFacet`:
       
   309 
       
   310 .. sourcecode:: python
       
   311 
       
   312     from cubicweb.web import facet
       
   313 
       
   314     class DisplayedOnFacet(facet.RelationFacet):
       
   315 	__regid__ = 'displayed_on-facet'
       
   316 	# relation to be displayed
       
   317 	rtype = 'displayed_on'
       
   318 	role = 'object'
       
   319 	# view to use to display persons
       
   320 	label_vid = 'combobox'
       
   321 
       
   322 Let's say we also want a filter according to the `visibility` attribute. this is
       
   323 even more simple, by inheriting from the :class:`AttributeFacet` class:
       
   324 
       
   325 .. sourcecode:: python
       
   326 
       
   327     class VisibilityFacet(facet.AttributeFacet):
       
   328 	__regid__ = 'visibility-facet'
       
   329 	rtype = 'visibility'
       
   330 
       
   331 Now if I search some pictures on my site, I get the following facets available:
       
   332 
       
   333 .. image:: ../../images/tutos-photowebsite_facets.png
       
   334 
       
   335 .. Note:
       
   336 
       
   337   Facets which have no choice to propose (i.e. one or less elements of
       
   338   vocabulary) are not displayed. That's may be why you don't see yours.
       
   339 
       
   340 
       
   341 Conclusion
       
   342 ~~~~~~~~~~
       
   343 
       
   344 We started to see the power behind the infrastructure provided by the
       
   345 framework. Both on the pure ui (CSS, javascript) stuff as on the python side
       
   346 (high level generic classes for components, including boxes and facets). We now
       
   347 have, by a few lines of code, a full-featured web site with a personnalized look.
       
   348 
       
   349 Of course we'll probably want more as the time goes, but we can now start
       
   350 concentrate on making good pictures, publishing albums and sharing them with
       
   351 friends...
       
   352 
       
   353 
       
   354 
       
   355 .. _`CubicWeb 3.10`: https://www.cubicweb.org/blogentry/1330518
       
   356 .. _`CubicWeb 3.9`: http://www.cubicweb.org/blogentry/1179899
       
   357 .. _`here`: http://webdesign.about.com/od/css3/f/blfaqbgsize.htm