diff -r 76ab3c71aff2 -r c67bcee93248 doc/book/en/tutorials/advanced/part04_ui-base.rst --- a/doc/book/en/tutorials/advanced/part04_ui-base.rst Mon Jul 06 17:39:35 2015 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,361 +0,0 @@ -Let's make it more user friendly -================================ - - -Step 1: let's improve site's usability for our visitors -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The first thing I've noticed is that people to whom I send links to photos with -some login/password authentication get lost, because they don't grasp they have -to login by clicking on the 'authenticate' link. That's much probably because -they only get a 404 when trying to access an unauthorized folder, and the site -doesn't make clear that 1. you're not authenticated, 2. you could get more -content by authenticating yourself. - -So, to improve this situation, I decided that I should: - -* make a login box appears for anonymous, so they see at a first glance a place - to put the login / password information I provided - -* customize the 404 page, proposing to login to anonymous. - -Here is the code, samples from my cube's `views.py` file: - -.. sourcecode:: python - - from cubicweb.predicates import is_instance - from cubicweb.web import component - from cubicweb.web.views import error - from cubicweb.predicates import anonymous_user - - class FourOhFour(error.FourOhFour): - __select__ = error.FourOhFour.__select__ & anonymous_user() - - def call(self): - self.w(u"

%s

" % self._cw._('this resource does not exist')) - self.w(u"

%s

" % self._cw._('have you tried to login?')) - - - class LoginBox(component.CtxComponent): - """display a box containing links to all startup views""" - __regid__ = 'sytweb.loginbox' - __select__ = component.CtxComponent.__select__ & anonymous_user() - - title = _('Authenticate yourself') - order = 70 - - def render_body(self, w): - cw = self._cw - form = cw.vreg['forms'].select('logform', cw) - form.render(w=w, table_class='', display_progress_div=False) - -The first class provides a new specific implementation of the default page you -get on 404 error, to display an adapted message to anonymous user. - -.. Note:: - - Thanks to the selection mecanism, it will be selected for anoymous user, - since the additional `anonymous_user()` selector gives it a higher score than - the default, and not for authenticated since this selector will return 0 in - such case (hence the object won't be selectable) - -The second class defines a simple box, that will be displayed by default with -boxes in the left column, thanks to default :class:`component.CtxComponent` -selector. The HTML is written to match default CubicWeb boxes style. The code -fetch the actual login form and render it. - - -.. figure:: ../../images/tutos-photowebsite_login-box.png - :alt: login box / 404 screenshot - - The login box and the custom 404 page for an anonymous visitor (translated in french) - - -Step 2: providing a custom index page -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Another thing we can easily do to improve the site is... A nicer index page -(e.g. the first page you get when accessing the web site)! The default one is -quite intimidating (that should change in a near future). I will provide a much -simpler index page that simply list available folders (e.g. photo albums in that -site). - -.. sourcecode:: python - - from cubicweb.web.views import startup - - class IndexView(startup.IndexView): - def call(self, **kwargs): - self.w(u'
\n') - if self._cw.cnx.anonymous_connection: - self.w(u'

%s

\n' % self._cw._('Public Albums')) - else: - self.w(u'

%s

\n' % self._cw._('Albums for %s') % self._cw.user.login) - self._cw.vreg['views'].select('tree', self._cw).render(w=self.w) - self.w(u'
\n') - - def registration_callback(vreg): - vreg.register_all(globals().values(), __name__, (IndexView,)) - vreg.register_and_replace(IndexView, startup.IndexView) - -As you can see, we override the default index view found in -`cubicweb.web.views.startup`, geting back nothing but its identifier and selector -since we override the top level view's `call` method. - -.. Note:: - - in that case, we want our index view to **replace** the existing one. To do so - we've to implements the `registration_callback` function, in which we tell to - register everything in the module *but* our IndexView, then we register it - instead of the former index view. - -Also, we added a title that tries to make it more evident that the visitor is -authenticated, or not. Hopefuly people will get it now! - - -.. figure:: ../../images/tutos-photowebsite_index-before.png - :alt: default index page screenshot - - The default index page - -.. figure:: ../../images/tutos-photowebsite_index-after.png - :alt: new index page screenshot - - Our simpler, less intimidating, index page (still translated in french) - - -Step 3: more navigation improvments -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -There are still a few problems I want to solve... - -* Images in a folder are displayed in a somewhat random order. I would like to - have them ordered by file's name (which will usually, inside a given folder, - also result ordering photo by their date and time) - -* When clicking a photo from an album view, you've to get back to the gallery - view to go to the next photo. This is pretty annoying... - -* Also, when viewing an image, there is no clue about the folder to which this - image belongs to. - -I will first try to explain the ordering problem. By default, when accessing -related entities by using the ORM's API, you should get them ordered according to -the target's class `cw_fetch_order`. If we take a look at the file cube'schema, -we can see: - -.. sourcecode:: python - - class File(AnyEntity): - """customized class for File entities""" - __regid__ = 'File' - fetch_attrs, cw_fetch_order = fetch_config(['data_name', 'title']) - - -By default, `fetch_config` will return a `cw_fetch_order` method that will order -on the first attribute in the list. So, we could expect to get files ordered by -their name. But we don't. What's up doc ? - -The problem is that files are related to folder using the `filed_under` relation. -And that relation is ambiguous, eg it can lead to `File` entities, but also to -`Folder` entities. In such case, since both entity types doesn't share the -attribute on which we want to sort, we'll get linked entities sorted on a common -attribute (usually `modification_date`). - -To fix this, we've to help the ORM. We'll do this in the method from the `ITree` -folder's adapter, used in the folder's primary view to display the folder's -content. Here's the code, that I've put in our cube's `entities.py` file, since -it's more logical stuff than view stuff: - -.. sourcecode:: python - - from cubes.folder import entities as folder - - class FolderITreeAdapter(folder.FolderITreeAdapter): - - def different_type_children(self, entities=True): - rql = self.entity.cw_related_rql(self.tree_relation, - self.parent_role, ('File',)) - rset = self._cw.execute(rql, {'x': self.entity.eid}) - if entities: - return list(rset.entities()) - return rset - - def registration_callback(vreg): - vreg.register_and_replace(FolderITreeAdapter, folder.FolderITreeAdapter) - -As you can see, we simple inherit from the adapter defined in the `folder` cube, -then we override the `different_type_children` method to give a clue to the ORM's -`cw_related_rql` method, that is responsible to generate the rql to get entities -related to the folder by the `filed_under` relation (the value of the -`tree_relation` attribute). The clue is that we only want to consider the `File` -target entity type. By doing this, we remove the ambiguity and get back a RQL -query that correctly order files by their `data_name` attribute. - - -.. Note:: - - * As seen earlier, we want to **replace** the folder's `ITree` adapter by our - implementation, hence the custom `registration_callback` method. - - -Ouf. That one was tricky... - -Now the easier parts. Let's start by adding some links on the file's primary view -to see the previous / next image in the same folder. CubicWeb's provide a -component that do exactly that. To make it appears, one have to be adaptable to -the `IPrevNext` interface. Here is the related code sample, extracted from our -cube's `views.py` file: - -.. sourcecode:: python - - from cubicweb.predicates import is_instance - from cubicweb.web.views import navigation - - - class FileIPrevNextAdapter(navigation.IPrevNextAdapter): - __select__ = is_instance('File') - - def previous_entity(self): - rset = self._cw.execute('File F ORDERBY FDN DESC LIMIT 1 WHERE ' - 'X filed_under FOLDER, F filed_under FOLDER, ' - 'F data_name FDN, X data_name > FDN, X eid %(x)s', - {'x': self.entity.eid}) - if rset: - return rset.get_entity(0, 0) - - def next_entity(self): - rset = self._cw.execute('File F ORDERBY FDN ASC LIMIT 1 WHERE ' - 'X filed_under FOLDER, F filed_under FOLDER, ' - 'F data_name FDN, X data_name < FDN, X eid %(x)s', - {'x': self.entity.eid}) - if rset: - return rset.get_entity(0, 0) - - -The `IPrevNext` interface implemented by the adapter simply consist in the -`previous_entity` / `next_entity` methods, that should respectivly return the -previous / next entity or `None`. We make an RQL query to get files in the same -folder, ordered similarly (eg by their `data_name` attribute). We set -ascendant/descendant ordering and a strict comparison with current file's name -(the "X" variable representing the current file). - -Notice that this query supposes we wont have two files of the same name in the -same folder, else things may go wrong. Fixing this is out of the scope of this -blog. And as I would like to have at some point a smarter, context sensitive -previous/next entity, I'll probably never fix this query (though if I had to, I -would probably choosing to add a constraint in the schema so that we can't add -two files of the same name in a folder). - -One more thing: by default, the component will be displayed below the content -zone (the one with the white background). You can change this in the site's -properties through the ui, but you can also change the default value in the code -by modifying the `context` attribute of the component: - -.. sourcecode:: python - - navigation.NextPrevNavigationComponent.context = 'navcontentbottom' - -.. Note:: - - `context` may be one of 'navtop', 'navbottom', 'navcontenttop' or - 'navcontentbottom'; the first two being outside the main content zone, the two - others inside it. - -.. figure:: ../../images/tutos-photowebsite_prevnext.png - :alt: screenshot of the previous/next entity component - - The previous/next entity component, at the bottom of the main content zone. - -Now, the only remaining stuff in my todo list is to see the file's folder. I'll use -the standard breadcrumb component to do so. Similarly as what we've seen before, this -component is controled by the :class:`IBreadCrumbs` interface, so we'll have to provide a custom -adapter for `File` entity, telling the a file's parent entity is its folder: - -.. sourcecode:: python - - from cubicweb.web.views import ibreadcrumbs - - class FileIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter): - __select__ = is_instance('File') - - def parent_entity(self): - if self.entity.filed_under: - return self.entity.filed_under[0] - -In that case, we simply use attribute notation provided by the ORM to get the -folder in which the current file (e.g. `self.entity`) is located. - -.. Note:: - - The :class:`IBreadCrumbs` interface is a `breadcrumbs` method, but the default - :class:`IBreadCrumbsAdapter` provides a default implementation for it that will look - at the value returned by its `parent_entity` method. It also provides a - default implementation for this method for entities adapting to the `ITree` - interface, but as our `File` doesn't, we've to provide a custom adapter. - -.. figure:: ../../images/tutos-photowebsite_breadcrumbs.png - :alt: screenshot of the breadcrumb component - - The breadcrumb component when on a file entity, now displaying parent folder. - - -Step 4: preparing the release and migrating the instance -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Now that greatly enhanced our cube, it's time to release it to upgrade production site. -I'll probably detail that process later, but I currently simply transfer the new code -to the server running the web site. - -However, I've still today some step to respect to get things done properly... - -First, as I've added some translatable string, I've to run: :: - - $ cubicweb-ctl i18ncube sytweb - -To update the cube's gettext catalogs (the '.po' files under the cube's `i18n` -directory). Once the above command is executed, I'll then update translations. - -To see if everything is ok on my test instance, I do: :: - - $ cubicweb-ctl i18ninstance sytweb - $ cubicweb-ctl start -D sytweb - -The first command compile i18n catalogs (e.g. generates '.mo' files) for my test -instance. The second command start it in debug mode, so I can open my browser and -navigate through the web site to see if everything is ok... - -.. Note:: - - In the 'cubicweb-ctl i18ncube' command, `sytweb` refers to the **cube**, while - in the two other, it refers to the **instance** (if you can't see the - difference, reread CubicWeb's concept chapter !). - - -Once I've checked it's ok, I simply have to bump the version number in the -`__pkginfo__` module to trigger a migration once I'll have updated the code on -the production site. I can check then check the migration is also going fine, by -first restoring a dump from the production site, then upgrading my test instance. - -To generate a dump from the production site: :: - - $ cubicweb-ctl db-dump sytweb - pg_dump -Fc --username=syt --no-owner --file /home/syt/etc/cubicweb.d/sytweb/backup/tmpYIN0YI/system sytweb - -> backup file /home/syt/etc/cubicweb.d/sytweb/backup/sytweb-2010-07-13_10-22-40.tar.gz - -I can now get back the dump file ('sytweb-2010-07-13_10-22-40.tar.gz') to my test -machine (using `scp` for instance) to restore it and start migration: :: - - $ cubicweb-ctl db-restore sytweb sytweb-2010-07-13_10-22-40.tar.gz - $ cubicweb-ctl upgrade sytweb - -You'll have to answer some questions, as we've seen in `an earlier post`_. - -Now that everything is tested, I can transfer the new code to the production -server, `apt-get upgrade` cubicweb and its dependencies, and eventually -upgrade the production instance. - - -.. _`several improvments`: http://www.cubicweb.org/blogentry/1179899 -.. _`3.8`: http://www.cubicweb.org/blogentry/917107 -.. _`first blog of this series`: http://www.cubicweb.org/blogentry/824642 -.. _`an earlier post`: http://www.cubicweb.org/867464