|
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 |