doc/book/en/tutorials/advanced/part04_ui-base.rst
changeset 10491 c67bcee93248
parent 10490 76ab3c71aff2
child 10492 68c13e0c0fc5
equal deleted inserted replaced
10490:76ab3c71aff2 10491:c67bcee93248
     1 Let's make it more user friendly
       
     2 ================================
       
     3 
       
     4 
       
     5 Step 1: let's improve site's usability for our visitors
       
     6 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
       
     7 
       
     8 The first thing I've noticed is that people to whom I send links to photos with
       
     9 some login/password authentication get lost, because they don't grasp they have
       
    10 to login by clicking on the 'authenticate' link. That's much probably because
       
    11 they only get a 404 when trying to access an unauthorized folder, and the site
       
    12 doesn't make clear that 1. you're not authenticated, 2. you could get more
       
    13 content by authenticating yourself.
       
    14 
       
    15 So, to improve this situation, I decided that I should:
       
    16 
       
    17 * make a login box appears for anonymous, so they see at a first glance a place
       
    18   to put the login / password information I provided
       
    19 
       
    20 * customize the 404 page, proposing to login to anonymous.
       
    21 
       
    22 Here is the code, samples from my cube's `views.py` file:
       
    23 
       
    24 .. sourcecode:: python
       
    25 
       
    26     from cubicweb.predicates import is_instance
       
    27     from cubicweb.web import component
       
    28     from cubicweb.web.views import error
       
    29     from cubicweb.predicates import anonymous_user
       
    30 
       
    31     class FourOhFour(error.FourOhFour):
       
    32 	__select__ = error.FourOhFour.__select__ & anonymous_user()
       
    33 
       
    34 	def call(self):
       
    35 	    self.w(u"<h1>%s</h1>" % self._cw._('this resource does not exist'))
       
    36 	    self.w(u"<p>%s</p>" % self._cw._('have you tried to login?'))
       
    37 
       
    38 
       
    39     class LoginBox(component.CtxComponent):
       
    40 	"""display a box containing links to all startup views"""
       
    41 	__regid__ = 'sytweb.loginbox'
       
    42 	__select__ = component.CtxComponent.__select__ & anonymous_user()
       
    43 
       
    44 	title = _('Authenticate yourself')
       
    45 	order = 70
       
    46 
       
    47 	def render_body(self, w):
       
    48 	    cw = self._cw
       
    49 	    form = cw.vreg['forms'].select('logform', cw)
       
    50 	    form.render(w=w, table_class='', display_progress_div=False)
       
    51 
       
    52 The first class provides a new specific implementation of the default page you
       
    53 get on 404 error, to display an adapted message to anonymous user.
       
    54 
       
    55 .. Note::
       
    56 
       
    57   Thanks to the selection mecanism, it will be selected for anoymous user,
       
    58   since the additional `anonymous_user()` selector gives it a higher score than
       
    59   the default, and not for authenticated since this selector will return 0 in
       
    60   such case (hence the object won't be selectable)
       
    61 
       
    62 The second class defines a simple box, that will be displayed by default with
       
    63 boxes in the left column, thanks to default :class:`component.CtxComponent`
       
    64 selector. The HTML is written to match default CubicWeb boxes style. The code
       
    65 fetch the actual login form and render it.
       
    66 
       
    67 
       
    68 .. figure:: ../../images/tutos-photowebsite_login-box.png
       
    69    :alt: login box / 404 screenshot
       
    70 
       
    71    The login box and the custom 404 page for an anonymous visitor (translated in french)
       
    72 
       
    73 
       
    74 Step 2: providing a custom index page
       
    75 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
       
    76 
       
    77 Another thing we can easily do to improve the site is... A nicer index page
       
    78 (e.g. the first page you get when accessing the web site)! The default one is
       
    79 quite intimidating (that should change in a near future). I will provide a much
       
    80 simpler index page that simply list available folders (e.g. photo albums in that
       
    81 site).
       
    82 
       
    83 .. sourcecode:: python
       
    84 
       
    85     from cubicweb.web.views import startup
       
    86 
       
    87     class IndexView(startup.IndexView):
       
    88 	def call(self, **kwargs):
       
    89 	    self.w(u'<div>\n')
       
    90 	    if self._cw.cnx.anonymous_connection:
       
    91 		self.w(u'<h4>%s</h4>\n' % self._cw._('Public Albums'))
       
    92 	    else:
       
    93 		self.w(u'<h4>%s</h4>\n' % self._cw._('Albums for %s') % self._cw.user.login)
       
    94 	    self._cw.vreg['views'].select('tree', self._cw).render(w=self.w)
       
    95 	    self.w(u'</div>\n')
       
    96 
       
    97     def registration_callback(vreg):
       
    98 	vreg.register_all(globals().values(), __name__, (IndexView,))
       
    99 	vreg.register_and_replace(IndexView, startup.IndexView)
       
   100 
       
   101 As you can see, we override the default index view found in
       
   102 `cubicweb.web.views.startup`, geting back nothing but its identifier and selector
       
   103 since we override the top level view's `call` method.
       
   104 
       
   105 .. Note::
       
   106 
       
   107   in that case, we want our index view to **replace** the existing one. To do so
       
   108   we've to implements the `registration_callback` function, in which we tell to
       
   109   register everything in the module *but* our IndexView, then we register it
       
   110   instead of the former index view.
       
   111 
       
   112 Also, we added a title that tries to make it more evident that the visitor is
       
   113 authenticated, or not. Hopefuly people will get it now!
       
   114 
       
   115 
       
   116 .. figure:: ../../images/tutos-photowebsite_index-before.png
       
   117    :alt: default index page screenshot
       
   118 
       
   119    The default index page
       
   120 
       
   121 .. figure:: ../../images/tutos-photowebsite_index-after.png
       
   122    :alt: new index page screenshot
       
   123 
       
   124    Our simpler, less intimidating, index page (still translated in french)
       
   125 
       
   126 
       
   127 Step 3: more navigation improvments
       
   128 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
       
   129 
       
   130 There are still a few problems I want to solve...
       
   131 
       
   132 * Images in a folder are displayed in a somewhat random order. I would like to
       
   133   have them ordered by file's name (which will usually, inside a given folder,
       
   134   also result ordering photo by their date and time)
       
   135 
       
   136 * When clicking a photo from an album view, you've to get back to the gallery
       
   137   view to go to the next photo. This is pretty annoying...
       
   138 
       
   139 * Also, when viewing an image, there is no clue about the folder to which this
       
   140   image belongs to.
       
   141 
       
   142 I will first try to explain the ordering problem. By default, when accessing
       
   143 related entities by using the ORM's API, you should get them ordered according to
       
   144 the target's class `cw_fetch_order`. If we take a look at the file cube'schema,
       
   145 we can see:
       
   146 
       
   147 .. sourcecode:: python
       
   148 
       
   149     class File(AnyEntity):
       
   150 	"""customized class for File entities"""
       
   151 	__regid__ = 'File'
       
   152 	fetch_attrs, cw_fetch_order = fetch_config(['data_name', 'title'])
       
   153 
       
   154 
       
   155 By default, `fetch_config` will return a `cw_fetch_order` method that will order
       
   156 on the first attribute in the list. So, we could expect to get files ordered by
       
   157 their name. But we don't.  What's up doc ?
       
   158 
       
   159 The problem is that files are related to folder using the `filed_under` relation.
       
   160 And that relation is ambiguous, eg it can lead to `File` entities, but also to
       
   161 `Folder` entities. In such case, since both entity types doesn't share the
       
   162 attribute on which we want to sort, we'll get linked entities sorted on a common
       
   163 attribute (usually `modification_date`).
       
   164 
       
   165 To fix this, we've to help the ORM. We'll do this in the method from the `ITree`
       
   166 folder's adapter, used in the folder's primary view to display the folder's
       
   167 content. Here's the code, that I've put in our cube's `entities.py` file, since
       
   168 it's more logical stuff than view stuff:
       
   169 
       
   170 .. sourcecode:: python
       
   171 
       
   172     from cubes.folder import entities as folder
       
   173 
       
   174     class FolderITreeAdapter(folder.FolderITreeAdapter):
       
   175 
       
   176 	def different_type_children(self, entities=True):
       
   177 	    rql = self.entity.cw_related_rql(self.tree_relation,
       
   178 					     self.parent_role, ('File',))
       
   179 	    rset = self._cw.execute(rql, {'x': self.entity.eid})
       
   180 	    if entities:
       
   181 		return list(rset.entities())
       
   182 	    return rset
       
   183 
       
   184     def registration_callback(vreg):
       
   185 	vreg.register_and_replace(FolderITreeAdapter, folder.FolderITreeAdapter)
       
   186 
       
   187 As you can see, we simple inherit from the adapter defined in the `folder` cube,
       
   188 then we override the `different_type_children` method to give a clue to the ORM's
       
   189 `cw_related_rql` method, that is responsible to generate the rql to get entities
       
   190 related to the folder by the `filed_under` relation (the value of the
       
   191 `tree_relation` attribute).  The clue is that we only want to consider the `File`
       
   192 target entity type. By doing this, we remove the ambiguity and get back a RQL
       
   193 query that correctly order files by their `data_name` attribute.
       
   194 
       
   195 
       
   196 .. Note::
       
   197 
       
   198     * As seen earlier, we want to **replace** the folder's `ITree` adapter by our
       
   199       implementation, hence the custom `registration_callback` method.
       
   200 
       
   201 
       
   202 Ouf. That one was tricky...
       
   203 
       
   204 Now the easier parts. Let's start by adding some links on the file's primary view
       
   205 to see the previous / next image in the same folder. CubicWeb's provide a
       
   206 component that do exactly that. To make it appears, one have to be adaptable to
       
   207 the `IPrevNext` interface. Here is the related code sample, extracted from our
       
   208 cube's `views.py` file:
       
   209 
       
   210 .. sourcecode:: python
       
   211 
       
   212     from cubicweb.predicates import is_instance
       
   213     from cubicweb.web.views import navigation
       
   214 
       
   215 
       
   216     class FileIPrevNextAdapter(navigation.IPrevNextAdapter):
       
   217 	__select__ = is_instance('File')
       
   218 
       
   219 	def previous_entity(self):
       
   220 	    rset = self._cw.execute('File F ORDERBY FDN DESC LIMIT 1 WHERE '
       
   221 				    'X filed_under FOLDER, F filed_under FOLDER, '
       
   222 				    'F data_name FDN, X data_name > FDN, X eid %(x)s',
       
   223 				    {'x': self.entity.eid})
       
   224 	    if rset:
       
   225 		return rset.get_entity(0, 0)
       
   226 
       
   227 	def next_entity(self):
       
   228 	    rset = self._cw.execute('File F ORDERBY FDN ASC LIMIT 1 WHERE '
       
   229 				    'X filed_under FOLDER, F filed_under FOLDER, '
       
   230 				    'F data_name FDN, X data_name < FDN, X eid %(x)s',
       
   231 				    {'x': self.entity.eid})
       
   232 	    if rset:
       
   233 		return rset.get_entity(0, 0)
       
   234 
       
   235 
       
   236 The `IPrevNext` interface implemented by the adapter simply consist in the
       
   237 `previous_entity` / `next_entity` methods, that should respectivly return the
       
   238 previous / next entity or `None`. We make an RQL query to get files in the same
       
   239 folder, ordered similarly (eg by their `data_name` attribute). We set
       
   240 ascendant/descendant ordering and a strict comparison with current file's name
       
   241 (the "X" variable representing the current file).
       
   242 
       
   243 Notice that this query supposes we wont have two files of the same name in the
       
   244 same folder, else things may go wrong. Fixing this is out of the scope of this
       
   245 blog. And as I would like to have at some point a smarter, context sensitive
       
   246 previous/next entity, I'll probably never fix this query (though if I had to, I
       
   247 would probably choosing to add a constraint in the schema so that we can't add
       
   248 two files of the same name in a folder).
       
   249 
       
   250 One more thing: by default, the component will be displayed below the content
       
   251 zone (the one with the white background). You can change this in the site's
       
   252 properties through the ui, but you can also change the default value in the code
       
   253 by modifying the `context` attribute of the component:
       
   254 
       
   255 .. sourcecode:: python
       
   256 
       
   257     navigation.NextPrevNavigationComponent.context = 'navcontentbottom'
       
   258 
       
   259 .. Note::
       
   260 
       
   261    `context` may be one of 'navtop', 'navbottom', 'navcontenttop' or
       
   262    'navcontentbottom'; the first two being outside the main content zone, the two
       
   263    others inside it.
       
   264 
       
   265 .. figure:: ../../images/tutos-photowebsite_prevnext.png
       
   266    :alt: screenshot of the previous/next entity component
       
   267 
       
   268    The previous/next entity component, at the bottom of the main content zone.
       
   269 
       
   270 Now, the only remaining stuff in my todo list is to see the file's folder. I'll use
       
   271 the standard breadcrumb component to do so. Similarly as what we've seen before, this
       
   272 component is controled by the :class:`IBreadCrumbs` interface, so we'll have to provide a custom
       
   273 adapter for `File` entity, telling the a file's parent entity is its folder:
       
   274 
       
   275 .. sourcecode:: python
       
   276 
       
   277     from cubicweb.web.views import ibreadcrumbs
       
   278 
       
   279     class FileIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter):
       
   280 	__select__ = is_instance('File')
       
   281 
       
   282 	def parent_entity(self):
       
   283 	    if self.entity.filed_under:
       
   284 		return self.entity.filed_under[0]
       
   285 
       
   286 In that case, we simply use attribute notation provided by the ORM to get the
       
   287 folder in which the current file (e.g. `self.entity`) is located.
       
   288 
       
   289 .. Note::
       
   290 
       
   291    The :class:`IBreadCrumbs` interface is a `breadcrumbs` method, but the default
       
   292    :class:`IBreadCrumbsAdapter` provides a default implementation for it that will look
       
   293    at the value returned by its `parent_entity` method. It also provides a
       
   294    default implementation for this method for entities adapting to the `ITree`
       
   295    interface, but as our `File` doesn't, we've to provide a custom adapter.
       
   296 
       
   297 .. figure:: ../../images/tutos-photowebsite_breadcrumbs.png
       
   298    :alt: screenshot of the breadcrumb component
       
   299 
       
   300    The breadcrumb component when on a file entity, now displaying parent folder.
       
   301 
       
   302 
       
   303 Step 4: preparing the release and migrating the instance
       
   304 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
       
   305 Now that greatly enhanced our cube, it's time to release it to upgrade production site.
       
   306 I'll probably detail that process later, but I currently simply transfer the new code
       
   307 to the server running the web site.
       
   308 
       
   309 However, I've still today some step to respect to get things done properly...
       
   310 
       
   311 First, as I've added some translatable string, I've to run: ::
       
   312 
       
   313   $ cubicweb-ctl i18ncube sytweb
       
   314 
       
   315 To update the cube's gettext catalogs (the '.po' files under the cube's `i18n`
       
   316 directory). Once the above command is executed, I'll then update translations.
       
   317 
       
   318 To see if everything is ok on my test instance, I do: ::
       
   319 
       
   320   $ cubicweb-ctl i18ninstance sytweb
       
   321   $ cubicweb-ctl start -D sytweb
       
   322 
       
   323 The first command compile i18n catalogs (e.g. generates '.mo' files) for my test
       
   324 instance. The second command start it in debug mode, so I can open my browser and
       
   325 navigate through the web site to see if everything is ok...
       
   326 
       
   327 .. Note::
       
   328 
       
   329    In the 'cubicweb-ctl i18ncube' command, `sytweb` refers to the **cube**, while
       
   330    in the two other, it refers to the **instance** (if you can't see the
       
   331    difference, reread CubicWeb's concept chapter !).
       
   332 
       
   333 
       
   334 Once I've checked it's ok, I simply have to bump the version number in the
       
   335 `__pkginfo__` module to trigger a migration once I'll have updated the code on
       
   336 the production site. I can check then check the migration is also going fine, by
       
   337 first restoring a dump from the production site, then upgrading my test instance.
       
   338 
       
   339 To generate a dump from the production site: ::
       
   340 
       
   341   $ cubicweb-ctl db-dump sytweb
       
   342   pg_dump -Fc --username=syt --no-owner --file /home/syt/etc/cubicweb.d/sytweb/backup/tmpYIN0YI/system sytweb
       
   343   -> backup file /home/syt/etc/cubicweb.d/sytweb/backup/sytweb-2010-07-13_10-22-40.tar.gz
       
   344 
       
   345 I can now get back the dump file ('sytweb-2010-07-13_10-22-40.tar.gz') to my test
       
   346 machine (using `scp` for instance) to restore it and start migration: ::
       
   347 
       
   348   $ cubicweb-ctl db-restore sytweb sytweb-2010-07-13_10-22-40.tar.gz
       
   349   $ cubicweb-ctl upgrade sytweb
       
   350 
       
   351 You'll have to answer some questions, as we've seen in `an earlier post`_.
       
   352 
       
   353 Now that everything is tested, I can transfer the new code to the production
       
   354 server, `apt-get upgrade` cubicweb and its dependencies, and eventually
       
   355 upgrade the production instance.
       
   356 
       
   357 
       
   358 .. _`several improvments`: http://www.cubicweb.org/blogentry/1179899
       
   359 .. _`3.8`: http://www.cubicweb.org/blogentry/917107
       
   360 .. _`first blog of this series`: http://www.cubicweb.org/blogentry/824642
       
   361 .. _`an earlier post`: http://www.cubicweb.org/867464