53 for item in sorted(actions_by_cat.items()): |
49 for item in sorted(actions_by_cat.items()): |
54 result.append(item) |
50 result.append(item) |
55 return result |
51 return result |
56 |
52 |
57 |
53 |
58 class EditRelationMixIn(ReloadableMixIn): |
|
59 def box_item(self, entity, etarget, rql, label): |
|
60 """builds HTML link to edit relation between `entity` and `etarget`""" |
|
61 role, target = get_role(self), get_target(self) |
|
62 args = {role[0] : entity.eid, target[0] : etarget.eid} |
|
63 url = self._cw.user_rql_callback((rql, args)) |
|
64 # for each target, provide a link to edit the relation |
|
65 return u'[<a href="%s">%s</a>] %s' % (xml_escape(url), label, |
|
66 etarget.view('incontext')) |
|
67 |
|
68 def related_boxitems(self, entity): |
|
69 rql = 'DELETE S %s O WHERE S eid %%(s)s, O eid %%(o)s' % self.rtype |
|
70 return [self.box_item(entity, etarget, rql, u'-') |
|
71 for etarget in self.related_entities(entity)] |
|
72 |
|
73 def related_entities(self, entity): |
|
74 return entity.related(self.rtype, get_role(self), entities=True) |
|
75 |
|
76 def unrelated_boxitems(self, entity): |
|
77 rql = 'SET S %s O WHERE S eid %%(s)s, O eid %%(o)s' % self.rtype |
|
78 return [self.box_item(entity, etarget, rql, u'+') |
|
79 for etarget in self.unrelated_entities(entity)] |
|
80 |
|
81 def unrelated_entities(self, entity): |
|
82 """returns the list of unrelated entities, using the entity's |
|
83 appropriate vocabulary function |
|
84 """ |
|
85 skip = set(unicode(e.eid) for e in entity.related(self.rtype, get_role(self), |
|
86 entities=True)) |
|
87 skip.add(None) |
|
88 skip.add(INTERNAL_FIELD_VALUE) |
|
89 filteretype = getattr(self, 'etype', None) |
|
90 entities = [] |
|
91 form = self._cw.vreg['forms'].select('edition', self._cw, |
|
92 rset=self.cw_rset, |
|
93 row=self.cw_row or 0) |
|
94 field = form.field_by_name(self.rtype, get_role(self), entity.e_schema) |
|
95 for _, eid in field.vocabulary(form): |
|
96 if eid not in skip: |
|
97 entity = self._cw.entity_from_eid(eid) |
|
98 if filteretype is None or entity.__regid__ == filteretype: |
|
99 entities.append(entity) |
|
100 return entities |
|
101 |
|
102 |
|
103 # generic classes for the new box system ####################################### |
|
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()) |
|
282 |
|
283 # view used to display related entties |
|
284 item_vid = 'incontext' |
|
285 # values separator when multiple values are allowed |
|
286 separator = ',' |
|
287 # msgid of the message to display when some new relation has been added/removed |
|
288 added_msg = None |
|
289 removed_msg = None |
|
290 |
|
291 # class attributes below *must* be set in concret classes (additionaly to |
|
292 # rtype / role [/ target_etype]. They should correspond to js_* methods on |
|
293 # the json controller |
|
294 |
|
295 # function(eid) |
|
296 # -> expected to return a list of values to display as input selector |
|
297 # vocabulary |
|
298 fname_vocabulary = None |
|
299 |
|
300 # function(eid, value) |
|
301 # -> handle the selector's input (eg create necessary entities and/or |
|
302 # relations). If the relation is multiple, you'll get a list of value, else |
|
303 # a single string value. |
|
304 fname_validate = None |
|
305 |
|
306 # function(eid, linked entity eid) |
|
307 # -> remove the relation |
|
308 fname_remove = None |
|
309 |
|
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): |
|
319 req = self._cw |
|
320 entity = self.entity |
|
321 related = entity.related(self.rtype, self.role) |
|
322 if self.role == 'subject': |
|
323 mayadd = self.rdef.has_perm(req, 'add', fromeid=entity.eid) |
|
324 maydel = self.rdef.has_perm(req, 'delete', fromeid=entity.eid) |
|
325 else: |
|
326 mayadd = self.rdef.has_perm(req, 'add', toeid=entity.eid) |
|
327 maydel = self.rdef.has_perm(req, 'delete', toeid=entity.eid) |
|
328 if mayadd or maydel: |
|
329 req.add_js(('cubicweb.ajax.js', 'cubicweb.ajax.box.js')) |
|
330 _ = req._ |
|
331 if related: |
|
332 w(u'<table>') |
|
333 for rentity in related.entities(): |
|
334 # for each related entity, provide a link to remove the relation |
|
335 subview = rentity.view(self.item_vid) |
|
336 if maydel: |
|
337 jscall = unicode(js.ajaxBoxRemoveLinkedEntity( |
|
338 self.__regid__, entity.eid, rentity.eid, |
|
339 self.fname_remove, |
|
340 self.removed_msg and _(self.removed_msg))) |
|
341 w(u'<tr><td>[<a href="javascript: %s">-</a>]</td>' |
|
342 '<td class="tagged"> %s</td></tr>' % (xml_escape(jscall), |
|
343 subview)) |
|
344 else: |
|
345 w(u'<tr><td class="tagged">%s</td></tr>' % (subview)) |
|
346 w(u'</table>') |
|
347 else: |
|
348 w(_('no related entity')) |
|
349 if mayadd: |
|
350 req.add_js('jquery.autocomplete.js') |
|
351 req.add_css('jquery.autocomplete.css') |
|
352 multiple = self.rdef.role_cardinality(self.role) in '*+' |
|
353 w(u'<table><tr><td>') |
|
354 jscall = unicode(js.ajaxBoxShowSelector( |
|
355 self.__regid__, entity.eid, self.fname_vocabulary, |
|
356 self.fname_validate, self.added_msg and _(self.added_msg), |
|
357 _(stdmsgs.BUTTON_OK[0]), _(stdmsgs.BUTTON_CANCEL[0]), |
|
358 multiple and self.separator)) |
|
359 w('<a class="button sglink" href="javascript: %s">%s</a>' % ( |
|
360 xml_escape(jscall), |
|
361 multiple and _('add_relation') or _('update_relation'))) |
|
362 w(u'</td><td>') |
|
363 w(u'<div id="%sHolder"></div>' % self.domid) |
|
364 w(u'</td></tr></table>') |
|
365 |
|
366 |
|
367 # old box system, deprecated ################################################### |
54 # old box system, deprecated ################################################### |
368 |
55 |
369 class BoxTemplate(View): |
56 class BoxTemplate(View): |
370 """base template for boxes, usually a (contextual) list of possible |
57 """base template for boxes, usually a (contextual) list of possible |
371 |
|
372 actions. Various classes attributes may be used to control the box |
58 actions. Various classes attributes may be used to control the box |
373 rendering. |
59 rendering. |
374 |
60 |
375 You may override on of the formatting callbacks is this is not necessary |
61 You may override one of the formatting callbacks if this is not necessary |
376 for your custom box. |
62 for your custom box. |
377 |
63 |
378 Classes inheriting from this class usually only have to override call |
64 Classes inheriting from this class usually only have to override call |
379 to fetch desired actions, and then to do something like :: |
65 to fetch desired actions, and then to do something like :: |
380 |
66 |
381 box.render(self.w) |
67 box.render(self.w) |
382 """ |
68 """ |
383 __metaclass__ = class_deprecated |
69 __metaclass__ = class_deprecated |
384 __deprecation_warning__ = '*BoxTemplate classes are deprecated, use *Box instead' |
70 __deprecation_warning__ = '[3.10] *BoxTemplate classes are deprecated, use *CtxComponent instead (%(cls)s)' |
385 |
71 |
386 __registry__ = 'boxes' |
72 __registry__ = 'ctxcomponents' |
387 __select__ = ~no_cnx() & match_context_prop() |
73 __select__ = ~no_cnx() |
388 |
74 |
389 categories_in_order = () |
75 categories_in_order = () |
390 cw_property_defs = { |
76 cw_property_defs = { |
391 _('visible'): dict(type='Boolean', default=True, |
77 _('visible'): dict(type='Boolean', default=True, |
392 help=_('display the box or not')), |
78 help=_('display the box or not')), |