|
1 Building my photos web site with |cubicweb| part V: let's make it even more user friendly |
|
2 ========================================================================================= |
|
3 |
|
4 .. _uiprops: |
|
5 |
|
6 Step 1: tired of the default look? |
|
7 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
|
8 |
|
9 OK... Now our site has its most desired features. But... I would like to make it look |
|
10 somewhat like *my* website. It is not www.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 :file:`logo.png` file into the cube's :file:`data` |
|
15 directory. As data files are looked at according to cubes order (CubicWeb |
|
16 resources coming last), that file will be selected instead of CubicWeb's one. |
|
17 |
|
18 .. Note:: |
|
19 As the location for static resources are cached, you'll have to restart |
|
20 your instance for this to be taken into account. |
|
21 |
|
22 Though there are some cases where you don't want to use a :file:`logo.png` file. |
|
23 For instance if it's a JPEG file. You can still change the logo by defining in |
|
24 the cube's :file:`uiprops.py` file: |
|
25 |
|
26 .. sourcecode:: python |
|
27 |
|
28 LOGO = data('logo.jpg') |
|
29 |
|
30 The uiprops machinery is used to define some static file resources, |
|
31 such as the logo, default Javascript / CSS files, as well as CSS |
|
32 properties (we'll see that later). |
|
33 |
|
34 .. Note:: |
|
35 This file is imported specifically by |cubicweb|, with a predefined name space, |
|
36 containing for instance the `data` function, telling the file is somewhere |
|
37 in a cube or CubicWeb's data directory. |
|
38 |
|
39 One side effect of this is that it can't be imported as a regular python |
|
40 module. |
|
41 |
|
42 The nice thing is that in debug mode, change to a :file:`uiprops.py` file are detected |
|
43 and then automatically reloaded. |
|
44 |
|
45 Now, as it's a photos web-site, I would like to have a photo of mine as background... |
|
46 After some trials I won't detail here, I've found a working recipe explained `here`_. |
|
47 All I've to do is to override some stuff of the default CubicWeb user interface to |
|
48 apply it as explained. |
|
49 |
|
50 The first thing to to get the ``<img/>`` tag as first element after the |
|
51 ``<body>`` tag. If you know a way to avoid this by simply specifying the image |
|
52 in the CSS, tell me! The easiest way to do so is to override the |
|
53 :class:`HTMLPageHeader` view, since that's the one that is directly called once |
|
54 the ``<body>`` has been written. How did I find this? By looking in the |
|
55 :mod:`cubiweb.web.views.basetemplates` module, since I know that global page |
|
56 layouts sits there. I could also have grep the "body" tag in |
|
57 :mod:`cubicweb.web.views`... Finding this was the hardest part. Now all I need is |
|
58 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 As you may have guessed, my background image is in a :file:`background.jpg` file |
|
77 in the cube's :file:`data` directory, but there are still some things to explain |
|
78 to newcomers here: |
|
79 |
|
80 * The :meth:`call` method is there the main access point of the view. It's called by |
|
81 the view's :meth:`render` method. It is not the only access point for a view, but |
|
82 this will be detailed later. |
|
83 |
|
84 * Calling `self.w` writes something to the output stream. Except for binary views |
|
85 (which do not generate text), it *must* be passed an Unicode string. |
|
86 |
|
87 * The proper way to get a file in :file:`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 :func:`registration_callback` stuff, you should understand it |
|
91 now! 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 to behave nicely (which |
|
94 is not the case at all for now). I'll put all this in a :file:`cubes.sytweb.css` |
|
95 file, stored as usual in our :file:`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 things 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 have to modify the :file:`uiprops.py` file |
|
141 introduced 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 in addition 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 |
|
186 Boxes present to the user some ways to use the application. Let's first do a few |
|
187 user interface tweaks in our :file:`views.py` file: |
|
188 |
|
189 .. sourcecode:: python |
|
190 |
|
191 from cubicweb.predicates import none_rset |
|
192 from cubicweb.web.views import bookmark |
|
193 from cubes.zone import views as zone |
|
194 from cubes.tag import views as tag |
|
195 |
|
196 # change bookmarks box selector so it's only displayed on startup views |
|
197 bookmark.BookmarksBox.__select__ = bookmark.BookmarksBox.__select__ & none_rset() |
|
198 # move zone box to the left instead of in the context frame and tweak its order |
|
199 zone.ZoneBox.context = 'left' |
|
200 zone.ZoneBox.order = 100 |
|
201 # move tags box to the left instead of in the context frame and tweak its order |
|
202 tag.TagsBox.context = 'left' |
|
203 tag.TagsBox.order = 102 |
|
204 # hide similarity box, not interested |
|
205 tag.SimilarityBox.visible = False |
|
206 |
|
207 The idea is to move all boxes in the left column, so we get more space for the |
|
208 photos. Now, serious things: I want a box similar to the tags box but to handle |
|
209 the `Person displayed_on File` relation. We can do this simply by adding a |
|
210 :class:`AjaxEditRelationCtxComponent` subclass to our views, as below: |
|
211 |
|
212 .. sourcecode:: python |
|
213 |
|
214 from logilab.common.decorators import monkeypatch |
|
215 from cubicweb import ValidationError |
|
216 from cubicweb.web.views import uicfg, component |
|
217 from cubicweb.web.views import basecontrollers |
|
218 |
|
219 # hide displayed_on relation using uicfg since it will be displayed by the box below |
|
220 uicfg.primaryview_section.tag_object_of(('*', 'displayed_on', '*'), 'hidden') |
|
221 |
|
222 class PersonBox(component.AjaxEditRelationCtxComponent): |
|
223 __regid__ = 'sytweb.displayed-on-box' |
|
224 # box position |
|
225 order = 101 |
|
226 context = 'left' |
|
227 # define relation to be handled |
|
228 rtype = 'displayed_on' |
|
229 role = 'object' |
|
230 target_etype = 'Person' |
|
231 # messages |
|
232 added_msg = _('person has been added') |
|
233 removed_msg = _('person has been removed') |
|
234 # bind to js_* methods of the json controller |
|
235 fname_vocabulary = 'unrelated_persons' |
|
236 fname_validate = 'link_to_person' |
|
237 fname_remove = 'unlink_person' |
|
238 |
|
239 |
|
240 @monkeypatch(basecontrollers.JSonController) |
|
241 @basecontrollers.jsonize |
|
242 def js_unrelated_persons(self, eid): |
|
243 """return tag unrelated to an entity""" |
|
244 rql = "Any F + ' ' + S WHERE P surname S, P firstname F, X eid %(x)s, NOT P displayed_on X" |
|
245 return [name for (name,) in self._cw.execute(rql, {'x' : eid})] |
|
246 |
|
247 |
|
248 @monkeypatch(basecontrollers.JSonController) |
|
249 def js_link_to_person(self, eid, people): |
|
250 req = self._cw |
|
251 for name in people: |
|
252 name = name.strip().title() |
|
253 if not name: |
|
254 continue |
|
255 try: |
|
256 firstname, surname = name.split(None, 1) |
|
257 except: |
|
258 raise ValidationError(eid, {('displayed_on', 'object'): 'provide <first name> <surname>'}) |
|
259 rset = req.execute('Person P WHERE ' |
|
260 'P firstname %(firstname)s, P surname %(surname)s', |
|
261 locals()) |
|
262 if rset: |
|
263 person = rset.get_entity(0, 0) |
|
264 else: |
|
265 person = req.create_entity('Person', firstname=firstname, |
|
266 surname=surname) |
|
267 req.execute('SET P displayed_on X WHERE ' |
|
268 'P eid %(p)s, X eid %(x)s, NOT P displayed_on X', |
|
269 {'p': person.eid, 'x' : eid}) |
|
270 |
|
271 @monkeypatch(basecontrollers.JSonController) |
|
272 def js_unlink_person(self, eid, personeid): |
|
273 self._cw.execute('DELETE P displayed_on X WHERE P eid %(p)s, X eid %(x)s', |
|
274 {'p': personeid, 'x': eid}) |
|
275 |
|
276 |
|
277 You basically subclass to configure with some class attributes. The `fname_*` |
|
278 attributes give the name of methods that should be defined on the json control to |
|
279 make the AJAX part of the widget work: one to get the vocabulary, one to add a |
|
280 relation and another to delete a relation. These methods must start by a `js_` |
|
281 prefix and are added to the controller using the `@monkeypatch` decorator. In my |
|
282 case, the most complicated method is the one which adds a relation, since it |
|
283 tries to see if the person already exists, and else automatically create it, |
|
284 assuming the user entered "firstname surname". |
|
285 |
|
286 Let's see how it looks like on a file primary view: |
|
287 |
|
288 .. image:: ../../images/tutos-photowebsite_boxes.png |
|
289 |
|
290 Great, it's now as easy for me to link my pictures to people than to tag them. |
|
291 Also, visitors get a consistent display of these two pieces of information. |
|
292 |
|
293 .. Note:: |
|
294 The ui component system has been refactored in `CubicWeb 3.10`_, which also |
|
295 introduced the :class:`AjaxEditRelationCtxComponent` class. |
|
296 |
|
297 |
|
298 Step 3: configuring facets |
|
299 ~~~~~~~~~~~~~~~~~~~~~~~~~~ |
|
300 |
|
301 The last feature we'll add today is facet configuration. If you access to the |
|
302 '/file' url, you'll see a set of 'facets' appearing in the left column. Facets |
|
303 provide an intuitive way to build a query incrementally, by proposing to the user |
|
304 various way to restrict the result set. For instance CubicWeb proposes a facet to |
|
305 restrict based on who created an entity; the tag cube proposes a facet to |
|
306 restrict based on tags; the zoe cube a facet to restrict based on geographical |
|
307 location, and so on. In that gist, I want to propose a facet to restrict based on |
|
308 the people displayed on the picture. To do so, there are various classes in the |
|
309 :mod:`cubicweb.web.facet` module which simply have to be configured using class |
|
310 attributes as we've done for the box. In our case, we'll define a subclass of |
|
311 :class:`RelationFacet`. |
|
312 |
|
313 .. Note:: |
|
314 |
|
315 Since that's ui stuff, we'll continue to add code below to our |
|
316 :file:`views.py` file. Though we begin to have a lot of various code their, so |
|
317 it's may be a good time to split our views module into submodules of a `view` |
|
318 package. In our case of a simple application (glue) cube, we could start using |
|
319 for instance the layout below: :: |
|
320 |
|
321 views/__init__.py # uicfg configuration, facets |
|
322 views/layout.py # header/footer/background stuff |
|
323 views/components.py # boxes, adapters |
|
324 views/pages.py # index view, 404 view |
|
325 |
|
326 .. sourcecode:: python |
|
327 |
|
328 from cubicweb.web import facet |
|
329 |
|
330 class DisplayedOnFacet(facet.RelationFacet): |
|
331 __regid__ = 'displayed_on-facet' |
|
332 # relation to be displayed |
|
333 rtype = 'displayed_on' |
|
334 role = 'object' |
|
335 # view to use to display persons |
|
336 label_vid = 'combobox' |
|
337 |
|
338 Let's say we also want to filter according to the `visibility` attribute. This is |
|
339 even simpler as we just have to derive from the :class:`AttributeFacet` class: |
|
340 |
|
341 .. sourcecode:: python |
|
342 |
|
343 class VisibilityFacet(facet.AttributeFacet): |
|
344 __regid__ = 'visibility-facet' |
|
345 rtype = 'visibility' |
|
346 |
|
347 Now if I search for some pictures on my site, I get the following facets available: |
|
348 |
|
349 .. image:: ../../images/tutos-photowebsite_facets.png |
|
350 |
|
351 .. Note:: |
|
352 |
|
353 By default a facet must be applyable to every entity in the result set and |
|
354 provide at leat two elements of vocabulary to be displayed (for instance you |
|
355 won't see the `created_by` facet if the same user has created all |
|
356 entities). This may explain why you don't see yours... |
|
357 |
|
358 |
|
359 Conclusion |
|
360 ~~~~~~~~~~ |
|
361 |
|
362 We started to see the power behind the infrastructure provided by the |
|
363 framework, both on the pure ui (CSS, Javascript) side and on the Python side |
|
364 (high level generic classes for components, including boxes and facets). We now |
|
365 have, with a few lines of code, a full-featured web site with a personalized look. |
|
366 |
|
367 Of course we'll probably want more as time goes, but we can now |
|
368 concentrate on making good pictures, publishing albums and sharing them with |
|
369 friends... |
|
370 |
|
371 |
|
372 |
|
373 .. _`CubicWeb 3.10`: http://www.cubicweb.org/blogentry/1330518 |
|
374 .. _`here`: http://webdesign.about.com/od/css3/f/blfaqbgsize.htm |