19 |
19 |
20 __docformat__ = "restructuredtext en" |
20 __docformat__ = "restructuredtext en" |
21 _ = unicode |
21 _ = unicode |
22 |
22 |
23 from logilab.mtconverter import xml_escape |
23 from logilab.mtconverter import xml_escape |
24 |
24 from logilab.common.deprecation import class_deprecated, class_renamed |
25 from cubicweb import Unauthorized, role as get_role, target as get_target |
25 |
|
26 from cubicweb import Unauthorized, role as get_role, target as get_target, tags |
26 from cubicweb.schema import display_name |
27 from cubicweb.schema import display_name |
27 from cubicweb.selectors import (no_cnx, one_line_rset, primary_view, |
28 from cubicweb.selectors import (no_cnx, one_line_rset, primary_view, |
28 match_context_prop, partial_relation_possible, |
29 match_context_prop, partial_relation_possible, |
29 partial_has_related_entities) |
30 partial_has_related_entities) |
30 from cubicweb.view import View, ReloadableMixIn |
31 from cubicweb.appobject import AppObject |
|
32 from cubicweb.view import View, ReloadableMixIn, Component |
31 from cubicweb.uilib import domid, js |
33 from cubicweb.uilib import domid, js |
32 from cubicweb.web import INTERNAL_FIELD_VALUE, stdmsgs |
34 from cubicweb.web import INTERNAL_FIELD_VALUE, stdmsgs |
33 from cubicweb.web.htmlwidgets import (BoxLink, BoxWidget, SideBoxWidget, |
35 from cubicweb.web.htmlwidgets import (BoxLink, BoxWidget, SideBoxWidget, |
34 RawBoxItem, BoxSeparator) |
36 RawBoxItem, BoxSeparator) |
35 from cubicweb.web.action import UnregisteredAction |
37 from cubicweb.web.action import UnregisteredAction |
36 |
38 |
37 |
39 |
38 class BoxTemplate(View): |
40 def sort_by_category(actions, categories_in_order=None): |
39 """base template for boxes, usually a (contextual) list of possible |
41 """return a list of (category, actions_sorted_by_title)""" |
40 |
42 result = [] |
41 actions. Various classes attributes may be used to control the box |
43 actions_by_cat = {} |
42 rendering. |
44 for action in actions: |
43 |
45 actions_by_cat.setdefault(action.category, []).append( |
44 You may override on of the formatting callbacks is this is not necessary |
46 (action.title, action) ) |
45 for your custom box. |
47 for key, values in actions_by_cat.items(): |
46 |
48 actions_by_cat[key] = [act for title, act in sorted(values)] |
47 Classes inheriting from this class usually only have to override call |
49 if categories_in_order: |
48 to fetch desired actions, and then to do something like :: |
50 for cat in categories_in_order: |
49 |
|
50 box.render(self.w) |
|
51 """ |
|
52 __registry__ = 'boxes' |
|
53 __select__ = ~no_cnx() & match_context_prop() |
|
54 |
|
55 categories_in_order = () |
|
56 cw_property_defs = { |
|
57 _('visible'): dict(type='Boolean', default=True, |
|
58 help=_('display the box or not')), |
|
59 _('order'): dict(type='Int', default=99, |
|
60 help=_('display order of the box')), |
|
61 # XXX 'incontext' boxes are handled by the default primary view |
|
62 _('context'): dict(type='String', default='left', |
|
63 vocabulary=(_('left'), _('incontext'), _('right')), |
|
64 help=_('context where this box should be displayed')), |
|
65 } |
|
66 context = 'left' |
|
67 htmlitemclass = 'boxItem' |
|
68 |
|
69 def sort_actions(self, actions): |
|
70 """return a list of (category, actions_sorted_by_title)""" |
|
71 result = [] |
|
72 actions_by_cat = {} |
|
73 for action in actions: |
|
74 actions_by_cat.setdefault(action.category, []).append( |
|
75 (action.title, action) ) |
|
76 for key, values in actions_by_cat.items(): |
|
77 actions_by_cat[key] = [act for title, act in sorted(values)] |
|
78 for cat in self.categories_in_order: |
|
79 if cat in actions_by_cat: |
51 if cat in actions_by_cat: |
80 result.append( (cat, actions_by_cat[cat]) ) |
52 result.append( (cat, actions_by_cat[cat]) ) |
81 for item in sorted(actions_by_cat.items()): |
53 for item in sorted(actions_by_cat.items()): |
82 result.append(item) |
54 result.append(item) |
83 return result |
55 return result |
84 |
56 |
85 def mk_action(self, title, path, escape=True, **kwargs): |
57 |
86 """factory function to create dummy actions compatible with the |
58 class EditRelationMixIn(ReloadableMixIn): |
87 .format_actions method |
|
88 """ |
|
89 if escape: |
|
90 title = xml_escape(title) |
|
91 return self.box_action(self._action(title, path, **kwargs)) |
|
92 |
|
93 def _action(self, title, path, **kwargs): |
|
94 return UnregisteredAction(self._cw, self.cw_rset, title, path, **kwargs) |
|
95 |
|
96 # formating callbacks |
|
97 |
|
98 def boxitem_link_tooltip(self, action): |
|
99 if action.__regid__: |
|
100 return u'keyword: %s' % action.__regid__ |
|
101 return u'' |
|
102 |
|
103 def box_action(self, action): |
|
104 cls = getattr(action, 'html_class', lambda: None)() or self.htmlitemclass |
|
105 return BoxLink(action.url(), self._cw._(action.title), |
|
106 cls, self.boxitem_link_tooltip(action)) |
|
107 |
|
108 |
|
109 class RQLBoxTemplate(BoxTemplate): |
|
110 """abstract box for boxes displaying the content of a rql query not |
|
111 related to the current result set. |
|
112 |
|
113 It rely on etype, rtype (both optional, usable to control registration |
|
114 according to application schema and display according to connected |
|
115 user's rights) and rql attributes |
|
116 """ |
|
117 |
|
118 rql = None |
|
119 |
|
120 def to_display_rql(self): |
|
121 assert self.rql is not None, self.__regid__ |
|
122 return (self.rql,) |
|
123 |
|
124 def call(self, **kwargs): |
|
125 try: |
|
126 rset = self._cw.execute(*self.to_display_rql()) |
|
127 except Unauthorized: |
|
128 # can't access to something in the query, forget this box |
|
129 return |
|
130 if len(rset) == 0: |
|
131 return |
|
132 box = BoxWidget(self._cw._(self.title), self.__regid__) |
|
133 for i, (teid, tname) in enumerate(rset): |
|
134 entity = rset.get_entity(i, 0) |
|
135 box.append(self.mk_action(tname, entity.absolute_url())) |
|
136 box.render(w=self.w) |
|
137 |
|
138 |
|
139 class UserRQLBoxTemplate(RQLBoxTemplate): |
|
140 """same as rql box template but the rql is build using the eid of the |
|
141 request's user |
|
142 """ |
|
143 |
|
144 def to_display_rql(self): |
|
145 assert self.rql is not None, self.__regid__ |
|
146 return (self.rql, {'x': self._cw.user.eid}) |
|
147 |
|
148 |
|
149 class EntityBoxTemplate(BoxTemplate): |
|
150 """base class for boxes related to a single entity""" |
|
151 __select__ = BoxTemplate.__select__ & one_line_rset() & primary_view() |
|
152 context = 'incontext' |
|
153 |
|
154 def call(self, row=0, col=0, **kwargs): |
|
155 """classes inheriting from EntityBoxTemplate should define cell_call""" |
|
156 self.cell_call(row, col, **kwargs) |
|
157 |
|
158 |
|
159 class RelatedEntityBoxTemplate(EntityBoxTemplate): |
|
160 __select__ = EntityBoxTemplate.__select__ & partial_has_related_entities() |
|
161 |
|
162 def cell_call(self, row, col, **kwargs): |
|
163 entity = self.cw_rset.get_entity(row, col) |
|
164 limit = self._cw.property_value('navigation.related-limit') + 1 |
|
165 role = get_role(self) |
|
166 self.w(u'<div class="sideBox">') |
|
167 self.wview('sidebox', entity.related(self.rtype, role, limit=limit), |
|
168 title=display_name(self._cw, self.rtype, role, |
|
169 context=entity.__regid__)) |
|
170 self.w(u'</div>') |
|
171 |
|
172 |
|
173 class EditRelationBoxTemplate(ReloadableMixIn, EntityBoxTemplate): |
|
174 """base class for boxes which let add or remove entities linked |
|
175 by a given relation |
|
176 |
|
177 subclasses should define at least id, rtype and target |
|
178 class attributes. |
|
179 """ |
|
180 |
|
181 def cell_call(self, row, col, view=None, **kwargs): |
|
182 self._cw.add_js('cubicweb.ajax.js') |
|
183 entity = self.cw_rset.get_entity(row, col) |
|
184 title = display_name(self._cw, self.rtype, get_role(self), context=entity.__regid__) |
|
185 box = SideBoxWidget(title, self.__regid__) |
|
186 related = self.related_boxitems(entity) |
|
187 unrelated = self.unrelated_boxitems(entity) |
|
188 box.extend(related) |
|
189 if related and unrelated: |
|
190 box.append(BoxSeparator()) |
|
191 box.extend(unrelated) |
|
192 box.render(self.w) |
|
193 |
|
194 def div_id(self): |
|
195 return self.__regid__ |
|
196 |
|
197 def box_item(self, entity, etarget, rql, label): |
59 def box_item(self, entity, etarget, rql, label): |
198 """builds HTML link to edit relation between `entity` and `etarget` |
60 """builds HTML link to edit relation between `entity` and `etarget`""" |
199 """ |
|
200 role, target = get_role(self), get_target(self) |
61 role, target = get_role(self), get_target(self) |
201 args = {role[0] : entity.eid, target[0] : etarget.eid} |
62 args = {role[0] : entity.eid, target[0] : etarget.eid} |
202 url = self._cw.user_rql_callback((rql, args)) |
63 url = self._cw.user_rql_callback((rql, args)) |
203 # for each target, provide a link to edit the relation |
64 # for each target, provide a link to edit the relation |
204 label = u'[<a href="%s">%s</a>] %s' % (xml_escape(url), label, |
65 return u'[<a href="%s">%s</a>] %s' % (xml_escape(url), label, |
205 etarget.view('incontext')) |
66 etarget.view('incontext')) |
206 return RawBoxItem(label, liclass=u'invisible') |
|
207 |
67 |
208 def related_boxitems(self, entity): |
68 def related_boxitems(self, entity): |
209 rql = 'DELETE S %s O WHERE S eid %%(s)s, O eid %%(o)s' % self.rtype |
69 rql = 'DELETE S %s O WHERE S eid %%(s)s, O eid %%(o)s' % self.rtype |
210 related = [] |
70 return [self.box_item(entity, etarget, rql, u'-') |
211 for etarget in self.related_entities(entity): |
71 for etarget in self.related_entities(entity)] |
212 related.append(self.box_item(entity, etarget, rql, u'-')) |
72 |
213 return related |
73 def related_entities(self, entity): |
|
74 return entity.related(self.rtype, get_role(self), entities=True) |
214 |
75 |
215 def unrelated_boxitems(self, entity): |
76 def unrelated_boxitems(self, entity): |
216 rql = 'SET S %s O WHERE S eid %%(s)s, O eid %%(o)s' % self.rtype |
77 rql = 'SET S %s O WHERE S eid %%(s)s, O eid %%(o)s' % self.rtype |
217 unrelated = [] |
78 return [self.box_item(entity, etarget, rql, u'+') |
218 for etarget in self.unrelated_entities(entity): |
79 for etarget in self.unrelated_entities(entity)] |
219 unrelated.append(self.box_item(entity, etarget, rql, u'+')) |
|
220 return unrelated |
|
221 |
|
222 def related_entities(self, entity): |
|
223 return entity.related(self.rtype, get_role(self), entities=True) |
|
224 |
80 |
225 def unrelated_entities(self, entity): |
81 def unrelated_entities(self, entity): |
226 """returns the list of unrelated entities, using the entity's |
82 """returns the list of unrelated entities, using the entity's |
227 appropriate vocabulary function |
83 appropriate vocabulary function |
228 """ |
84 """ |
242 if filteretype is None or entity.__regid__ == filteretype: |
98 if filteretype is None or entity.__regid__ == filteretype: |
243 entities.append(entity) |
99 entities.append(entity) |
244 return entities |
100 return entities |
245 |
101 |
246 |
102 |
247 class AjaxEditRelationBoxTemplate(EntityBoxTemplate): |
103 # generic classes for the new box system ####################################### |
248 __select__ = EntityBoxTemplate.__select__ & partial_relation_possible() |
104 |
|
105 from cubicweb.selectors import match_context, contextual |
|
106 |
|
107 class EmptyComponent(Exception): |
|
108 """some selectable component has actually no content and should not be |
|
109 rendered |
|
110 """ |
|
111 |
|
112 class Layout(Component): |
|
113 __regid__ = 'layout' |
|
114 __abstract__ = True |
|
115 |
|
116 |
|
117 class Box(AppObject): # XXX ContextComponent |
|
118 __registry__ = 'boxes' |
|
119 __select__ = ~no_cnx() & match_context_prop() |
|
120 |
|
121 categories_in_order = () |
|
122 cw_property_defs = { |
|
123 _('visible'): dict(type='Boolean', default=True, |
|
124 help=_('display the box or not')), |
|
125 _('order'): dict(type='Int', default=99, |
|
126 help=_('display order of the box')), |
|
127 # XXX 'incontext' boxes are handled by the default primary view |
|
128 _('context'): dict(type='String', default='left', |
|
129 vocabulary=(_('left'), _('incontext'), _('right')), |
|
130 help=_('context where this box should be displayed')), |
|
131 } |
|
132 context = 'left' |
|
133 contextual = False |
|
134 title = None |
|
135 # XXX support kwargs for compat with old boxes which gets the view as |
|
136 # argument |
|
137 def render(self, w, **kwargs): |
|
138 getlayout = self._cw.vreg['components'].select |
|
139 try: |
|
140 # XXX ensure context is given when the component is reloaded through |
|
141 # ajax |
|
142 context = self.cw_extra_kwargs['context'] |
|
143 except KeyError: |
|
144 context = self.cw_propval('context') |
|
145 layout = getlayout('layout', self._cw, rset=self.cw_rset, |
|
146 row=self.cw_row, col=self.cw_col, |
|
147 view=self, context=context) |
|
148 layout.render(w) |
|
149 |
|
150 def init_rendering(self): |
|
151 """init rendering callback: that's the good time to check your component |
|
152 has some content to display. If not, you can still raise |
|
153 :exc:`EmptyComponent` to inform it should be skipped. |
|
154 |
|
155 Also, :exc:`Unauthorized` will be catched, logged, then the component |
|
156 will be skipped. |
|
157 """ |
|
158 self.items = [] |
|
159 |
|
160 @property |
|
161 def domid(self): |
|
162 """return the HTML DOM identifier for this component""" |
|
163 return domid(self.__regid__) |
|
164 |
|
165 @property |
|
166 def cssclass(self): |
|
167 """return the CSS class name for this component""" |
|
168 return domid(self.__regid__) |
|
169 |
|
170 def render_title(self, w): |
|
171 """return the title for this component""" |
|
172 if self.title is None: |
|
173 raise NotImplementedError() |
|
174 w(self._cw._(self.title)) |
|
175 |
|
176 def render_body(self, w): |
|
177 """return the body (content) for this component""" |
|
178 raise NotImplementedError() |
|
179 |
|
180 def render_items(self, w, items=None, klass=u'boxListing'): |
|
181 if items is None: |
|
182 items = self.items |
|
183 assert items |
|
184 w(u'<ul class="%s">' % klass) |
|
185 for item in items: |
|
186 if hasattr(item, 'render'): |
|
187 item.render(w) # XXX display <li> by itself |
|
188 else: |
|
189 w(u'<li>') |
|
190 w(item) |
|
191 w(u'</li>') |
|
192 w(u'</ul>') |
|
193 |
|
194 def append(self, item): |
|
195 self.items.append(item) |
|
196 |
|
197 def box_action(self, action): # XXX action_link |
|
198 return self.build_link(self._cw._(action.title), action.url()) |
|
199 |
|
200 def build_link(self, title, url, **kwargs): |
|
201 if self._cw.selected(url): |
|
202 try: |
|
203 kwargs['klass'] += ' selected' |
|
204 except KeyError: |
|
205 kwargs['klass'] = 'selected' |
|
206 return tags.a(title, href=url, **kwargs) |
|
207 |
|
208 |
|
209 class EntityBox(Box): # XXX ContextEntityComponent |
|
210 """base class for boxes related to a single entity""" |
|
211 __select__ = Box.__select__ & one_line_rset() |
|
212 context = 'incontext' |
|
213 contextual = True |
|
214 |
|
215 def __init__(self, *args, **kwargs): |
|
216 super(EntityBox, self).__init__(*args, **kwargs) |
|
217 try: |
|
218 entity = kwargs['entity'] |
|
219 except KeyError: |
|
220 entity = self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0) |
|
221 self.entity = entity |
|
222 |
|
223 @property |
|
224 def domid(self): |
|
225 return domid(self.__regid__) + unicode(self.entity.eid) |
|
226 |
|
227 |
|
228 # high level abstract box classes ############################################## |
|
229 |
|
230 |
|
231 class RQLBox(Box): |
|
232 """abstract box for boxes displaying the content of a rql query not |
|
233 related to the current result set. |
|
234 """ |
|
235 rql = None |
|
236 |
|
237 def to_display_rql(self): |
|
238 assert self.rql is not None, self.__regid__ |
|
239 return (self.rql,) |
|
240 |
|
241 def init_rendering(self): |
|
242 rset = self._cw.execute(*self.to_display_rql()) |
|
243 if not rset: |
|
244 raise EmptyComponent() |
|
245 if len(rset[0]) == 2: |
|
246 self.items = [] |
|
247 for i, (eid, label) in enumerate(rset): |
|
248 entity = rset.get_entity(i, 0) |
|
249 self.items.append(self.build_link(label, entity.absolute_url())) |
|
250 else: |
|
251 self.items = [self.build_link(e.dc_title(), e.absolute_url()) |
|
252 for e in rset.entities()] |
|
253 |
|
254 def render_body(self, w): |
|
255 self.render_items(w) |
|
256 |
|
257 |
|
258 class EditRelationBox(EditRelationMixIn, EntityBox): |
|
259 """base class for boxes which let add or remove entities linked by a given |
|
260 relation |
|
261 |
|
262 subclasses should define at least id, rtype and target class attributes. |
|
263 """ |
|
264 def render_title(self, w): |
|
265 return display_name(self._cw, self.rtype, get_role(self), |
|
266 context=self.entity.__regid__) |
|
267 |
|
268 def render_body(self, w): |
|
269 self._cw.add_js('cubicweb.ajax.js') |
|
270 related = self.related_boxitems(self.entity) |
|
271 unrelated = self.unrelated_boxitems(self.entity) |
|
272 self.items.extend(related) |
|
273 if related and unrelated: |
|
274 self.items.append(BoxSeparator()) |
|
275 self.items.extend(unrelated) |
|
276 self.render_items(w) |
|
277 |
|
278 |
|
279 class AjaxEditRelationBox(EntityBox): |
|
280 __select__ = EntityBox.__select__ & ( |
|
281 partial_relation_possible(action='add') | partial_has_related_entities()) |
249 |
282 |
250 # view used to display related entties |
283 # view used to display related entties |
251 item_vid = 'incontext' |
284 item_vid = 'incontext' |
252 # values separator when multiple values are allowed |
285 # values separator when multiple values are allowed |
253 separator = ',' |
286 separator = ',' |
272 |
305 |
273 # function(eid, linked entity eid) |
306 # function(eid, linked entity eid) |
274 # -> remove the relation |
307 # -> remove the relation |
275 fname_remove = None |
308 fname_remove = None |
276 |
309 |
277 def cell_call(self, row, col, **kwargs): |
310 def __init__(self, *args, **kwargs): |
|
311 super(AjaxEditRelationBox, self).__init__(*args, **kwargs) |
|
312 self.rdef = self.entity.e_schema.rdef(self.rtype, self.role, self.target_etype) |
|
313 |
|
314 def render_title(self, w): |
|
315 w(self.rdef.rtype.display_name(self._cw, self.role, |
|
316 context=self.entity.__regid__)) |
|
317 |
|
318 def render_body(self, w): |
278 req = self._cw |
319 req = self._cw |
279 entity = self.cw_rset.get_entity(row, col) |
320 entity = self.entity |
280 related = entity.related(self.rtype, self.role) |
321 related = entity.related(self.rtype, self.role) |
281 rdef = entity.e_schema.rdef(self.rtype, self.role, self.target_etype) |
|
282 if self.role == 'subject': |
322 if self.role == 'subject': |
283 mayadd = rdef.has_perm(req, 'add', fromeid=entity.eid) |
323 mayadd = self.rdef.has_perm(req, 'add', fromeid=entity.eid) |
284 maydel = rdef.has_perm(req, 'delete', fromeid=entity.eid) |
324 maydel = self.rdef.has_perm(req, 'delete', fromeid=entity.eid) |
285 else: |
325 else: |
286 mayadd = rdef.has_perm(req, 'add', toeid=entity.eid) |
326 mayadd = self.rdef.has_perm(req, 'add', toeid=entity.eid) |
287 maydel = rdef.has_perm(req, 'delete', toeid=entity.eid) |
327 maydel = self.rdef.has_perm(req, 'delete', toeid=entity.eid) |
288 if not (related or mayadd): |
|
289 return |
|
290 if mayadd or maydel: |
328 if mayadd or maydel: |
291 req.add_js(('cubicweb.ajax.js', 'cubicweb.ajax.box.js')) |
329 req.add_js(('cubicweb.ajax.js', 'cubicweb.ajax.box.js')) |
292 _ = req._ |
330 _ = req._ |
293 w = self.w |
|
294 divid = domid(self.__regid__) + unicode(entity.eid) |
|
295 w(u'<div class="sideBox" id="%s%s">' % (domid(self.__regid__), entity.eid)) |
|
296 w(u'<div class="sideBoxTitle"><span>%s</span></div>' % |
|
297 rdef.rtype.display_name(req, self.role, context=entity.__regid__)) |
|
298 w(u'<div class="sideBox"><div class="sideBoxBody">') |
|
299 if related: |
331 if related: |
300 w(u'<table>') |
332 w(u'<table>') |
301 for rentity in related.entities(): |
333 for rentity in related.entities(): |
302 # for each related entity, provide a link to remove the relation |
334 # for each related entity, provide a link to remove the relation |
303 subview = rentity.view(self.item_vid) |
335 subview = rentity.view(self.item_vid) |
305 jscall = unicode(js.ajaxBoxRemoveLinkedEntity( |
337 jscall = unicode(js.ajaxBoxRemoveLinkedEntity( |
306 self.__regid__, entity.eid, rentity.eid, |
338 self.__regid__, entity.eid, rentity.eid, |
307 self.fname_remove, |
339 self.fname_remove, |
308 self.removed_msg and _(self.removed_msg))) |
340 self.removed_msg and _(self.removed_msg))) |
309 w(u'<tr><td>[<a href="javascript: %s">-</a>]</td>' |
341 w(u'<tr><td>[<a href="javascript: %s">-</a>]</td>' |
310 '<td class="tagged">%s</td></tr>' % (xml_escape(jscall), |
342 '<td class="tagged"> %s</td></tr>' % (xml_escape(jscall), |
311 subview)) |
343 subview)) |
312 else: |
344 else: |
313 w(u'<tr><td class="tagged">%s</td></tr>' % (subview)) |
345 w(u'<tr><td class="tagged">%s</td></tr>' % (subview)) |
314 w(u'</table>') |
346 w(u'</table>') |
315 else: |
347 else: |
316 w(_('no related entity')) |
348 w(_('no related entity')) |
317 if mayadd: |
349 if mayadd: |
318 req.add_js('jquery.autocomplete.js') |
350 req.add_js('jquery.autocomplete.js') |
319 req.add_css('jquery.autocomplete.css') |
351 req.add_css('jquery.autocomplete.css') |
320 multiple = rdef.role_cardinality(self.role) in '*+' |
352 multiple = self.rdef.role_cardinality(self.role) in '*+' |
321 w(u'<table><tr><td>') |
353 w(u'<table><tr><td>') |
322 jscall = unicode(js.ajaxBoxShowSelector( |
354 jscall = unicode(js.ajaxBoxShowSelector( |
323 self.__regid__, entity.eid, self.fname_vocabulary, |
355 self.__regid__, entity.eid, self.fname_vocabulary, |
324 self.fname_validate, self.added_msg and _(self.added_msg), |
356 self.fname_validate, self.added_msg and _(self.added_msg), |
325 _(stdmsgs.BUTTON_OK[0]), _(stdmsgs.BUTTON_CANCEL[0]), |
357 _(stdmsgs.BUTTON_OK[0]), _(stdmsgs.BUTTON_CANCEL[0]), |
326 multiple and self.separator)) |
358 multiple and self.separator)) |
327 w('<a class="button sglink" href="javascript: %s">%s</a>' % ( |
359 w('<a class="button sglink" href="javascript: %s">%s</a>' % ( |
328 xml_escape(jscall), |
360 xml_escape(jscall), |
329 multiple and _('add_relation') or _('update_relation'))) |
361 multiple and _('add_relation') or _('update_relation'))) |
330 w(u'</td><td>') |
362 w(u'</td><td>') |
331 w(u'<div id="%sHolder"></div>' % divid) |
363 w(u'<div id="%sHolder"></div>' % self.domid) |
332 w(u'</td></tr></table>') |
364 w(u'</td></tr></table>') |
333 w(u'</div>\n') |
365 |
334 w(u'</div></div>\n') |
366 |
|
367 # old box system, deprecated ################################################### |
|
368 |
|
369 class BoxTemplate(View): |
|
370 """base template for boxes, usually a (contextual) list of possible |
|
371 |
|
372 actions. Various classes attributes may be used to control the box |
|
373 rendering. |
|
374 |
|
375 You may override on of the formatting callbacks is this is not necessary |
|
376 for your custom box. |
|
377 |
|
378 Classes inheriting from this class usually only have to override call |
|
379 to fetch desired actions, and then to do something like :: |
|
380 |
|
381 box.render(self.w) |
|
382 """ |
|
383 __metaclass__ = class_deprecated |
|
384 __deprecation_warning__ = '*BoxTemplate classes are deprecated, use *Box instead' |
|
385 |
|
386 __registry__ = 'boxes' |
|
387 __select__ = ~no_cnx() & match_context_prop() |
|
388 |
|
389 categories_in_order = () |
|
390 cw_property_defs = { |
|
391 _('visible'): dict(type='Boolean', default=True, |
|
392 help=_('display the box or not')), |
|
393 _('order'): dict(type='Int', default=99, |
|
394 help=_('display order of the box')), |
|
395 # XXX 'incontext' boxes are handled by the default primary view |
|
396 _('context'): dict(type='String', default='left', |
|
397 vocabulary=(_('left'), _('incontext'), _('right')), |
|
398 help=_('context where this box should be displayed')), |
|
399 } |
|
400 context = 'left' |
|
401 |
|
402 def sort_actions(self, actions): |
|
403 """return a list of (category, actions_sorted_by_title)""" |
|
404 return sort_by_category(actions, self.categories_in_order) |
|
405 |
|
406 def mk_action(self, title, url, escape=True, **kwargs): |
|
407 """factory function to create dummy actions compatible with the |
|
408 .format_actions method |
|
409 """ |
|
410 if escape: |
|
411 title = xml_escape(title) |
|
412 return self.box_action(self._action(title, url, **kwargs)) |
|
413 |
|
414 def _action(self, title, url, **kwargs): |
|
415 return UnregisteredAction(self._cw, title, url, **kwargs) |
|
416 |
|
417 # formating callbacks |
|
418 |
|
419 def boxitem_link_tooltip(self, action): |
|
420 if action.__regid__: |
|
421 return u'keyword: %s' % action.__regid__ |
|
422 return u'' |
|
423 |
|
424 def box_action(self, action): |
|
425 klass = getattr(action, 'html_class', lambda: None)() |
|
426 return BoxLink(action.url(), self._cw._(action.title), |
|
427 klass, self.boxitem_link_tooltip(action)) |
|
428 |
|
429 |
|
430 class RQLBoxTemplate(BoxTemplate): |
|
431 """abstract box for boxes displaying the content of a rql query not |
|
432 related to the current result set. |
|
433 """ |
|
434 |
|
435 rql = None |
|
436 |
|
437 def to_display_rql(self): |
|
438 assert self.rql is not None, self.__regid__ |
|
439 return (self.rql,) |
|
440 |
|
441 def call(self, **kwargs): |
|
442 try: |
|
443 rset = self._cw.execute(*self.to_display_rql()) |
|
444 except Unauthorized: |
|
445 # can't access to something in the query, forget this box |
|
446 return |
|
447 if len(rset) == 0: |
|
448 return |
|
449 box = BoxWidget(self._cw._(self.title), self.__regid__) |
|
450 for i, (teid, tname) in enumerate(rset): |
|
451 entity = rset.get_entity(i, 0) |
|
452 box.append(self.mk_action(tname, entity.absolute_url())) |
|
453 box.render(w=self.w) |
|
454 |
|
455 |
|
456 class UserRQLBoxTemplate(RQLBoxTemplate): |
|
457 """same as rql box template but the rql is build using the eid of the |
|
458 request's user |
|
459 """ |
|
460 |
|
461 def to_display_rql(self): |
|
462 assert self.rql is not None, self.__regid__ |
|
463 return (self.rql, {'x': self._cw.user.eid}) |
|
464 |
|
465 |
|
466 class EntityBoxTemplate(BoxTemplate): |
|
467 """base class for boxes related to a single entity""" |
|
468 __select__ = BoxTemplate.__select__ & one_line_rset() & primary_view() |
|
469 context = 'incontext' |
|
470 |
|
471 def call(self, row=0, col=0, **kwargs): |
|
472 """classes inheriting from EntityBoxTemplate should define cell_call""" |
|
473 self.cell_call(row, col, **kwargs) |
|
474 |
|
475 |
|
476 class EditRelationBoxTemplate(EditRelationMixIn, EntityBoxTemplate): |
|
477 """base class for boxes which let add or remove entities linked |
|
478 by a given relation |
|
479 |
|
480 subclasses should define at least id, rtype and target |
|
481 class attributes. |
|
482 """ |
|
483 |
|
484 def cell_call(self, row, col, view=None, **kwargs): |
|
485 self._cw.add_js('cubicweb.ajax.js') |
|
486 entity = self.cw_rset.get_entity(row, col) |
|
487 title = display_name(self._cw, self.rtype, get_role(self), |
|
488 context=entity.__regid__) |
|
489 box = SideBoxWidget(title, self.__regid__) |
|
490 related = self.related_boxitems(entity) |
|
491 unrelated = self.unrelated_boxitems(entity) |
|
492 box.extend(related) |
|
493 if related and unrelated: |
|
494 box.append(BoxSeparator()) |
|
495 box.extend(unrelated) |
|
496 box.render(self.w) |
|
497 |
|
498 def box_item(self, entity, etarget, rql, label): |
|
499 label = super(EditRelationBoxTemplate, self).box_item( |
|
500 entity, etarget, rql, label) |
|
501 return RawBoxItem(label, liclass=u'invisible') |
|
502 |
|
503 |
|
504 AjaxEditRelationBoxTemplate = class_renamed( |
|
505 'AjaxEditRelationBoxTemplate', AjaxEditRelationBox, |
|
506 '[3.10] AjaxEditRelationBoxTemplate has been renamed to AjaxEditRelationBox') |
|
507 |