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