1 """abstract box classes for CubicWeb web client |
1 """abstract box classes for CubicWeb web client |
2 |
2 |
3 :organization: Logilab |
3 :organization: Logilab |
4 :copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved. |
4 :copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved. |
5 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr |
5 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr |
6 """ |
6 """ |
7 __docformat__ = "restructuredtext en" |
7 __docformat__ = "restructuredtext en" |
8 |
8 |
9 from logilab.common.decorators import cached |
|
10 from logilab.mtconverter import html_escape |
9 from logilab.mtconverter import html_escape |
11 |
10 |
12 from cubicweb import Unauthorized, role as get_role |
11 from cubicweb import Unauthorized, role as get_role, target as get_target |
13 from cubicweb.common.registerers import ( |
12 from cubicweb.selectors import (one_line_rset, primary_view, |
14 accepts_registerer, extresources_registerer, |
13 match_context_prop, partial_has_related_entities, |
15 etype_rtype_priority_registerer) |
14 accepts_compat, has_relation_compat, |
16 from cubicweb.common.selectors import ( |
15 condition_compat, require_group_compat) |
17 etype_rtype_selector, one_line_rset, accept, has_relation, |
16 from cubicweb.view import View, ReloadableMixIn |
18 primary_view, match_context_prop, has_related_entities, |
|
19 _rql_condition) |
|
20 from cubicweb.common.view import Template |
|
21 from cubicweb.common.appobject import ReloadableMixIn |
|
22 |
17 |
23 from cubicweb.web.htmlwidgets import (BoxLink, BoxWidget, SideBoxWidget, |
18 from cubicweb.web.htmlwidgets import (BoxLink, BoxWidget, SideBoxWidget, |
24 RawBoxItem, BoxSeparator) |
19 RawBoxItem, BoxSeparator) |
25 from cubicweb.web.action import UnregisteredAction |
20 from cubicweb.web.action import UnregisteredAction |
26 |
21 |
27 _ = unicode |
22 _ = unicode |
28 |
23 |
29 |
24 |
30 class BoxTemplate(Template): |
25 class BoxTemplate(View): |
31 """base template for boxes, usually a (contextual) list of possible |
26 """base template for boxes, usually a (contextual) list of possible |
32 |
27 |
33 actions. Various classes attributes may be used to control the box |
28 actions. Various classes attributes may be used to control the box |
34 rendering. |
29 rendering. |
35 |
30 |
36 You may override on of the formatting callbacks is this is not necessary |
31 You may override on of the formatting callbacks is this is not necessary |
37 for your custom box. |
32 for your custom box. |
38 |
33 |
39 Classes inheriting from this class usually only have to override call |
34 Classes inheriting from this class usually only have to override call |
40 to fetch desired actions, and then to do something like :: |
35 to fetch desired actions, and then to do something like :: |
41 |
36 |
42 box.render(self.w) |
37 box.render(self.w) |
43 """ |
38 """ |
44 __registry__ = 'boxes' |
39 __registry__ = 'boxes' |
45 __selectors__ = Template.__selectors__ + (match_context_prop,) |
40 __select__ = match_context_prop() |
46 |
41 registered = classmethod(require_group_compat(View.registered)) |
|
42 |
47 categories_in_order = () |
43 categories_in_order = () |
48 property_defs = { |
44 property_defs = { |
49 _('visible'): dict(type='Boolean', default=True, |
45 _('visible'): dict(type='Boolean', default=True, |
50 help=_('display the box or not')), |
46 help=_('display the box or not')), |
51 _('order'): dict(type='Int', default=99, |
47 _('order'): dict(type='Int', default=99, |
93 |
89 |
94 def box_action(self, action): |
90 def box_action(self, action): |
95 cls = getattr(action, 'html_class', lambda: None)() or self.htmlitemclass |
91 cls = getattr(action, 'html_class', lambda: None)() or self.htmlitemclass |
96 return BoxLink(action.url(), self.req._(action.title), |
92 return BoxLink(action.url(), self.req._(action.title), |
97 cls, self.boxitem_link_tooltip(action)) |
93 cls, self.boxitem_link_tooltip(action)) |
98 |
94 |
99 |
95 |
100 class RQLBoxTemplate(BoxTemplate): |
96 class RQLBoxTemplate(BoxTemplate): |
101 """abstract box for boxes displaying the content of a rql query not |
97 """abstract box for boxes displaying the content of a rql query not |
102 related to the current result set. |
98 related to the current result set. |
103 |
99 |
104 It rely on etype, rtype (both optional, usable to control registration |
100 It rely on etype, rtype (both optional, usable to control registration |
105 according to application schema and display according to connected |
101 according to application schema and display according to connected |
106 user's rights) and rql attributes |
102 user's rights) and rql attributes |
107 """ |
103 """ |
108 __registerer__ = etype_rtype_priority_registerer |
104 #XXX __selectors__ = BoxTemplate.__selectors__ + (etype_rtype_selector,) |
109 __selectors__ = BoxTemplate.__selectors__ + (etype_rtype_selector,) |
|
110 |
105 |
111 rql = None |
106 rql = None |
112 |
107 |
113 def to_display_rql(self): |
108 def to_display_rql(self): |
114 assert self.rql is not None, self.id |
109 assert self.rql is not None, self.id |
115 return (self.rql,) |
110 return (self.rql,) |
116 |
111 |
117 def call(self, **kwargs): |
112 def call(self, **kwargs): |
118 try: |
113 try: |
119 rset = self.req.execute(*self.to_display_rql()) |
114 rset = self.req.execute(*self.to_display_rql()) |
120 except Unauthorized: |
115 except Unauthorized: |
121 # can't access to something in the query, forget this box |
116 # can't access to something in the query, forget this box |
126 for i, (teid, tname) in enumerate(rset): |
121 for i, (teid, tname) in enumerate(rset): |
127 entity = rset.get_entity(i, 0) |
122 entity = rset.get_entity(i, 0) |
128 box.append(self.mk_action(tname, entity.absolute_url())) |
123 box.append(self.mk_action(tname, entity.absolute_url())) |
129 box.render(w=self.w) |
124 box.render(w=self.w) |
130 |
125 |
131 |
126 |
132 class UserRQLBoxTemplate(RQLBoxTemplate): |
127 class UserRQLBoxTemplate(RQLBoxTemplate): |
133 """same as rql box template but the rql is build using the eid of the |
128 """same as rql box template but the rql is build using the eid of the |
134 request's user |
129 request's user |
135 """ |
130 """ |
136 |
131 |
137 def to_display_rql(self): |
132 def to_display_rql(self): |
138 assert self.rql is not None, self.id |
133 assert self.rql is not None, self.id |
139 return (self.rql, {'x': self.req.user.eid}, 'x') |
134 return (self.rql, {'x': self.req.user.eid}, 'x') |
140 |
|
141 |
|
142 class ExtResourcesBoxTemplate(BoxTemplate): |
|
143 """base class for boxes displaying external resources such as the RSS logo. |
|
144 It should list necessary resources with the .need_resources attribute. |
|
145 """ |
|
146 __registerer__ = extresources_registerer |
|
147 need_resources = () |
|
148 |
135 |
149 |
136 |
150 class EntityBoxTemplate(BoxTemplate): |
137 class EntityBoxTemplate(BoxTemplate): |
151 """base class for boxes related to a single entity""" |
138 """base class for boxes related to a single entity""" |
152 __registerer__ = accepts_registerer |
139 __select__ = BoxTemplate.__select__ & one_line_rset() & primary_view() |
153 __selectors__ = (one_line_rset, primary_view, |
140 registered = accepts_compat(has_relation_compat(condition_compat(BoxTemplate.registered))) |
154 match_context_prop, etype_rtype_selector, |
|
155 has_relation, accept, _rql_condition) |
|
156 accepts = ('Any',) |
|
157 context = 'incontext' |
141 context = 'incontext' |
158 condition = None |
142 |
159 |
|
160 def call(self, row=0, col=0, **kwargs): |
143 def call(self, row=0, col=0, **kwargs): |
161 """classes inheriting from EntityBoxTemplate should define cell_call""" |
144 """classes inheriting from EntityBoxTemplate should define cell_call""" |
162 self.cell_call(row, col, **kwargs) |
145 self.cell_call(row, col, **kwargs) |
163 |
146 |
164 |
147 |
165 class RelatedEntityBoxTemplate(EntityBoxTemplate): |
148 class RelatedEntityBoxTemplate(EntityBoxTemplate): |
166 __selectors__ = EntityBoxTemplate.__selectors__ + (has_related_entities,) |
149 __select__ = EntityBoxTemplate.__select__ & partial_has_related_entities() |
167 |
150 |
168 def cell_call(self, row, col, **kwargs): |
151 def cell_call(self, row, col, **kwargs): |
169 entity = self.entity(row, col) |
152 entity = self.entity(row, col) |
170 limit = self.req.property_value('navigation.related-limit') + 1 |
153 limit = self.req.property_value('navigation.related-limit') + 1 |
171 role = get_role(self) |
154 role = get_role(self) |
172 self.w(u'<div class="sideRelated">') |
155 self.w(u'<div class="sideRelated">') |
180 by a given relation |
163 by a given relation |
181 |
164 |
182 subclasses should define at least id, rtype and target |
165 subclasses should define at least id, rtype and target |
183 class attributes. |
166 class attributes. |
184 """ |
167 """ |
185 |
168 |
186 def cell_call(self, row, col, view=None): |
169 def cell_call(self, row, col, view=None, **kwargs): |
187 self.req.add_js('cubicweb.ajax.js') |
170 self.req.add_js('cubicweb.ajax.js') |
188 entity = self.entity(row, col) |
171 entity = self.entity(row, col) |
189 box = SideBoxWidget(display_name(self.req, self.rtype), self.id) |
172 box = SideBoxWidget(display_name(self.req, self.rtype), self.id) |
190 count = self.w_related(box, entity) |
173 count = self.w_related(box, entity) |
191 if count: |
174 if count: |
192 box.append(BoxSeparator()) |
175 box.append(BoxSeparator()) |
193 self.w_unrelated(box, entity) |
176 if not self.w_unrelated(box, entity): |
|
177 del box.items[-1] # remove useless separator |
194 box.render(self.w) |
178 box.render(self.w) |
195 |
179 |
196 def div_id(self): |
180 def div_id(self): |
197 return self.id |
181 return self.id |
198 |
182 |
199 @cached |
|
200 def xtarget(self): |
|
201 if self.target == 'subject': |
|
202 return 'object', 'subject' |
|
203 return 'subject', 'object' |
|
204 |
|
205 def box_item(self, entity, etarget, rql, label): |
183 def box_item(self, entity, etarget, rql, label): |
206 """builds HTML link to edit relation between `entity` and `etarget` |
184 """builds HTML link to edit relation between `entity` and `etarget` |
207 """ |
185 """ |
208 x, target = self.xtarget() |
186 role, target = get_role(self), get_target(self) |
209 args = {x[0] : entity.eid, target[0] : etarget.eid} |
187 args = {role[0] : entity.eid, target[0] : etarget.eid} |
210 url = self.user_rql_callback((rql, args)) |
188 url = self.user_rql_callback((rql, args)) |
211 # for each target, provide a link to edit the relation |
189 # for each target, provide a link to edit the relation |
212 label = u'[<a href="%s">%s</a>] %s' % (url, label, |
190 label = u'[<a href="%s">%s</a>] %s' % (url, label, |
213 etarget.view('incontext')) |
191 etarget.view('incontext')) |
214 return RawBoxItem(label, liclass=u'invisible') |
192 return RawBoxItem(label, liclass=u'invisible') |
215 |
193 |
216 def w_related(self, box, entity): |
194 def w_related(self, box, entity): |
217 """appends existing relations to the `box`""" |
195 """appends existing relations to the `box`""" |
218 rql = 'DELETE S %s O WHERE S eid %%(s)s, O eid %%(o)s' % self.rtype |
196 rql = 'DELETE S %s O WHERE S eid %%(s)s, O eid %%(o)s' % self.rtype |
219 related = self.related_entities(entity) |
197 related = self.related_entities(entity) |
220 for etarget in related: |
198 for etarget in related: |
221 box.append(self.box_item(entity, etarget, rql, u'-')) |
199 box.append(self.box_item(entity, etarget, rql, u'-')) |
222 return len(related) |
200 return len(related) |
223 |
201 |
224 def w_unrelated(self, box, entity): |
202 def w_unrelated(self, box, entity): |
225 """appends unrelated entities to the `box`""" |
203 """appends unrelated entities to the `box`""" |
226 rql = 'SET S %s O WHERE S eid %%(s)s, O eid %%(o)s' % self.rtype |
204 rql = 'SET S %s O WHERE S eid %%(s)s, O eid %%(o)s' % self.rtype |
|
205 i = 0 |
227 for etarget in self.unrelated_entities(entity): |
206 for etarget in self.unrelated_entities(entity): |
228 box.append(self.box_item(entity, etarget, rql, u'+')) |
207 box.append(self.box_item(entity, etarget, rql, u'+')) |
|
208 i += 1 |
|
209 return i |
229 |
210 |
230 def unrelated_entities(self, entity): |
211 def unrelated_entities(self, entity): |
231 """returns the list of unrelated entities |
212 """returns the list of unrelated entities |
232 |
213 |
233 if etype is not defined on the Box's class, the default |
214 if etype is not defined on the Box's class, the default |
234 behaviour is to use the entity's appropraite vocabulary function |
215 behaviour is to use the entity's appropraite vocabulary function |
235 """ |
216 """ |
236 x, target = self.xtarget() |
|
237 # use entity.unrelated if we've been asked for a particular etype |
217 # use entity.unrelated if we've been asked for a particular etype |
238 if hasattr(self, 'etype'): |
218 if hasattr(self, 'etype'): |
239 return entity.unrelated(self.rtype, self.etype, x).entities() |
219 return entity.unrelated(self.rtype, self.etype, get_role(self)).entities() |
240 # in other cases, use vocabulary functions |
220 # in other cases, use vocabulary functions |
241 entities = [] |
221 entities = [] |
242 for _, eid in entity.vocabulary(self.rtype, x): |
222 form = self.vreg.select_object('forms', 'edition', self.req, self.rset, |
|
223 row=self.row or 0) |
|
224 field = form.field_by_name(self.rtype, get_role(self), entity.e_schema) |
|
225 for _, eid in form.form_field_vocabulary(field): |
243 if eid is not None: |
226 if eid is not None: |
244 rset = self.req.eid_rset(eid) |
227 rset = self.req.eid_rset(eid) |
245 entities.append(rset.get_entity(0, 0)) |
228 entities.append(rset.get_entity(0, 0)) |
246 return entities |
229 return entities |
247 |
230 |
248 def related_entities(self, entity): |
231 def related_entities(self, entity): |
249 x, target = self.xtarget() |
232 return entity.related(self.rtype, get_role(self), entities=True) |
250 return entity.related(self.rtype, x, entities=True) |
233 |
251 |
|