13 # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more |
13 # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more |
14 # details. |
14 # details. |
15 # |
15 # |
16 # You should have received a copy of the GNU Lesser General Public License along |
16 # You should have received a copy of the GNU Lesser General Public License along |
17 # with CubicWeb. If not, see <http://www.gnu.org/licenses/>. |
17 # with CubicWeb. If not, see <http://www.gnu.org/licenses/>. |
18 """Set of tree-building widgets, based on jQuery treeview plugin |
18 """Set of tree views / tree-building widgets, some based on jQuery treeview |
19 |
19 plugin. |
20 """ |
20 """ |
21 __docformat__ = "restructuredtext en" |
21 __docformat__ = "restructuredtext en" |
22 |
22 |
|
23 from warnings import warn |
|
24 |
23 from logilab.mtconverter import xml_escape |
25 from logilab.mtconverter import xml_escape |
|
26 from logilab.common.decorators import cached |
|
27 |
24 from cubicweb.utils import make_uid |
28 from cubicweb.utils import make_uid |
|
29 from cubicweb.selectors import implements, adaptable |
|
30 from cubicweb.view import EntityView, EntityAdapter, implements_adapter_compat |
|
31 from cubicweb.web import json |
25 from cubicweb.interfaces import ITree |
32 from cubicweb.interfaces import ITree |
26 from cubicweb.selectors import implements |
33 from cubicweb.web.views import baseviews |
27 from cubicweb.view import EntityView |
|
28 from cubicweb.web import json |
|
29 |
34 |
30 def treecookiename(treeid): |
35 def treecookiename(treeid): |
31 return str('%s-treestate' % treeid) |
36 return str('%s-treestate' % treeid) |
32 |
37 |
|
38 |
|
39 class ITreeAdapter(EntityAdapter): |
|
40 """This adapter has to be overriden to be configured using the |
|
41 tree_relation, child_role and parent_role class attributes to |
|
42 benefit from this default implementation |
|
43 """ |
|
44 __regid__ = 'ITree' |
|
45 __select__ = implements(ITree) # XXX for bw compat, else should be abstract |
|
46 |
|
47 tree_relation = None |
|
48 child_role = 'subject' |
|
49 parent_role = 'object' |
|
50 |
|
51 @implements_adapter_compat('ITree') |
|
52 def children_rql(self): |
|
53 """returns RQL to get children |
|
54 |
|
55 XXX should be removed from the public interface |
|
56 """ |
|
57 return self.entity.related_rql(self.tree_relation, self.parent_role) |
|
58 |
|
59 @implements_adapter_compat('ITree') |
|
60 def different_type_children(self, entities=True): |
|
61 """return children entities of different type as this entity. |
|
62 |
|
63 according to the `entities` parameter, return entity objects or the |
|
64 equivalent result set |
|
65 """ |
|
66 res = self.entity.related(self.tree_relation, self.parent_role, |
|
67 entities=entities) |
|
68 eschema = self.entity.e_schema |
|
69 if entities: |
|
70 return [e for e in res if e.e_schema != eschema] |
|
71 return res.filtered_rset(lambda x: x.e_schema != eschema, self.entity.cw_col) |
|
72 |
|
73 @implements_adapter_compat('ITree') |
|
74 def same_type_children(self, entities=True): |
|
75 """return children entities of the same type as this entity. |
|
76 |
|
77 according to the `entities` parameter, return entity objects or the |
|
78 equivalent result set |
|
79 """ |
|
80 res = self.entity.related(self.tree_relation, self.parent_role, |
|
81 entities=entities) |
|
82 eschema = self.entity.e_schema |
|
83 if entities: |
|
84 return [e for e in res if e.e_schema == eschema] |
|
85 return res.filtered_rset(lambda x: x.e_schema is eschema, self.entity.cw_col) |
|
86 |
|
87 @implements_adapter_compat('ITree') |
|
88 def is_leaf(self): |
|
89 """returns true if this node as no child""" |
|
90 return len(self.children()) == 0 |
|
91 |
|
92 @implements_adapter_compat('ITree') |
|
93 def is_root(self): |
|
94 """returns true if this node has no parent""" |
|
95 return self.parent() is None |
|
96 |
|
97 @implements_adapter_compat('ITree') |
|
98 def root(self): |
|
99 """return the root object""" |
|
100 return self._cw.entity_from_eid(self.path()[0]) |
|
101 |
|
102 @implements_adapter_compat('ITree') |
|
103 def parent(self): |
|
104 """return the parent entity if any, else None (e.g. if we are on the |
|
105 root) |
|
106 """ |
|
107 try: |
|
108 return self.entity.related(self.tree_relation, self.child_role, |
|
109 entities=True)[0] |
|
110 except (KeyError, IndexError): |
|
111 return None |
|
112 |
|
113 @implements_adapter_compat('ITree') |
|
114 def children(self, entities=True, sametype=False): |
|
115 """return children entities |
|
116 |
|
117 according to the `entities` parameter, return entity objects or the |
|
118 equivalent result set |
|
119 """ |
|
120 if sametype: |
|
121 return self.same_type_children(entities) |
|
122 else: |
|
123 return self.entity.related(self.tree_relation, self.parent_role, |
|
124 entities=entities) |
|
125 |
|
126 @implements_adapter_compat('ITree') |
|
127 def iterparents(self, strict=True): |
|
128 def _uptoroot(self): |
|
129 curr = self |
|
130 while True: |
|
131 curr = curr.parent() |
|
132 if curr is None: |
|
133 break |
|
134 yield curr |
|
135 curr = curr.cw_adapt_to('ITree') |
|
136 if not strict: |
|
137 return chain([self.entity], _uptoroot(self)) |
|
138 return _uptoroot(self) |
|
139 |
|
140 @implements_adapter_compat('ITree') |
|
141 def iterchildren(self, _done=None): |
|
142 """iterates over the item's children""" |
|
143 if _done is None: |
|
144 _done = set() |
|
145 for child in self.children(): |
|
146 if child.eid in _done: |
|
147 self.error('loop in %s tree', child.__regid__.lower()) |
|
148 continue |
|
149 yield child |
|
150 _done.add(child.eid) |
|
151 |
|
152 @implements_adapter_compat('ITree') |
|
153 def prefixiter(self, _done=None): |
|
154 if _done is None: |
|
155 _done = set() |
|
156 if self.entity.eid in _done: |
|
157 return |
|
158 _done.add(self.entity.eid) |
|
159 yield self.entity |
|
160 for child in self.same_type_children(): |
|
161 for entity in child.cw_adapt_to('ITree').prefixiter(_done): |
|
162 yield entity |
|
163 |
|
164 @cached |
|
165 @implements_adapter_compat('ITree') |
|
166 def path(self): |
|
167 """returns the list of eids from the root object to this object""" |
|
168 path = [] |
|
169 adapter = self |
|
170 entity = adapter.entity |
|
171 while entity is not None: |
|
172 if entity.eid in path: |
|
173 self.error('loop in %s tree', entity.__regid__.lower()) |
|
174 break |
|
175 path.append(entity.eid) |
|
176 try: |
|
177 # check we are not jumping to another tree |
|
178 if (adapter.tree_relation != self.tree_relation or |
|
179 adapter.child_role != self.child_role): |
|
180 break |
|
181 entity = adapter.parent() |
|
182 adapter = entity.cw_adapt_to('ITree') |
|
183 except AttributeError: |
|
184 break |
|
185 path.reverse() |
|
186 return path |
|
187 |
|
188 |
|
189 def _done_init(done, view, row, col): |
|
190 """handle an infinite recursion safety belt""" |
|
191 if done is None: |
|
192 done = set() |
|
193 entity = view.cw_rset.get_entity(row, col) |
|
194 if entity.eid in done: |
|
195 msg = entity._cw._('loop in %(rel)s relation (%(eid)s)') % { |
|
196 'rel': entity.tree_attribute, |
|
197 'eid': entity.eid |
|
198 } |
|
199 return None, msg |
|
200 done.add(entity.eid) |
|
201 return done, entity |
|
202 |
|
203 |
|
204 class BaseTreeView(baseviews.ListView): |
|
205 """base tree view""" |
|
206 __regid__ = 'tree' |
|
207 __select__ = adaptable('ITree') |
|
208 item_vid = 'treeitem' |
|
209 |
|
210 def call(self, done=None, **kwargs): |
|
211 if done is None: |
|
212 done = set() |
|
213 super(TreeViewMixIn, self).call(done=done, **kwargs) |
|
214 |
|
215 def cell_call(self, row, col=0, vid=None, done=None, **kwargs): |
|
216 done, entity = _done_init(done, self, row, col) |
|
217 if done is None: |
|
218 # entity is actually an error message |
|
219 self.w(u'<li class="badcontent">%s</li>' % entity) |
|
220 return |
|
221 self.open_item(entity) |
|
222 entity.view(vid or self.item_vid, w=self.w, **kwargs) |
|
223 relatedrset = entity.cw_adapt_to('ITree').children(entities=False) |
|
224 self.wview(self.__regid__, relatedrset, 'null', done=done, **kwargs) |
|
225 self.close_item(entity) |
|
226 |
|
227 def open_item(self, entity): |
|
228 self.w(u'<li class="%s">\n' % entity.__regid__.lower()) |
|
229 def close_item(self, entity): |
|
230 self.w(u'</li>\n') |
|
231 |
|
232 |
|
233 |
|
234 class TreePathView(EntityView): |
|
235 """a recursive path view""" |
|
236 __regid__ = 'path' |
|
237 __select__ = adaptable('ITree') |
|
238 item_vid = 'oneline' |
|
239 separator = u' > ' |
|
240 |
|
241 def call(self, **kwargs): |
|
242 self.w(u'<div class="pathbar">') |
|
243 super(TreePathMixIn, self).call(**kwargs) |
|
244 self.w(u'</div>') |
|
245 |
|
246 def cell_call(self, row, col=0, vid=None, done=None, **kwargs): |
|
247 done, entity = _done_init(done, self, row, col) |
|
248 if done is None: |
|
249 # entity is actually an error message |
|
250 self.w(u'<span class="badcontent">%s</span>' % entity) |
|
251 return |
|
252 parent = entity.cw_adapt_to('ITree').parent_entity() |
|
253 if parent: |
|
254 parent.view(self.__regid__, w=self.w, done=done) |
|
255 self.w(self.separator) |
|
256 entity.view(vid or self.item_vid, w=self.w) |
|
257 |
|
258 |
|
259 # XXX rename regid to ajaxtree/foldabletree or something like that (same for |
|
260 # treeitemview) |
33 class TreeView(EntityView): |
261 class TreeView(EntityView): |
|
262 """ajax tree view, click to expand folder""" |
|
263 |
34 __regid__ = 'treeview' |
264 __regid__ = 'treeview' |
35 itemvid = 'treeitemview' |
265 itemvid = 'treeitemview' |
36 subvid = 'oneline' |
266 subvid = 'oneline' |
37 css_classes = 'treeview widget' |
267 css_classes = 'treeview widget' |
38 title = _('tree view') |
268 title = _('tree view') |
110 """ |
340 """ |
111 __regid__ = 'filetree-oneline' |
341 __regid__ = 'filetree-oneline' |
112 |
342 |
113 def cell_call(self, row, col): |
343 def cell_call(self, row, col): |
114 entity = self.cw_rset.get_entity(row, col) |
344 entity = self.cw_rset.get_entity(row, col) |
115 if ITree.is_implemented_by(entity.__class__) and not entity.is_leaf(): |
345 if entity.cw_adapt_to('ITree') and not entity.is_leaf(): |
116 self.w(u'<div class="folder">%s</div>\n' % entity.view('oneline')) |
346 self.w(u'<div class="folder">%s</div>\n' % entity.view('oneline')) |
117 else: |
347 else: |
118 # XXX define specific CSS classes according to mime types |
348 # XXX define specific CSS classes according to mime types |
119 self.w(u'<div class="file">%s</div>\n' % entity.view('oneline')) |
349 self.w(u'<div class="file">%s</div>\n' % entity.view('oneline')) |
120 |
350 |
121 |
351 |
122 class DefaultTreeViewItemView(EntityView): |
352 class DefaultTreeViewItemView(EntityView): |
123 """default treeitem view for entities which don't implement ITree""" |
353 """default treeitem view for entities which don't adapt to ITree""" |
124 __regid__ = 'treeitemview' |
354 __regid__ = 'treeitemview' |
125 |
355 |
126 def cell_call(self, row, col, vid='oneline', treeid=None, **morekwargs): |
356 def cell_call(self, row, col, vid='oneline', treeid=None, **morekwargs): |
127 assert treeid is not None |
357 assert treeid is not None |
128 itemview = self._cw.view(vid, self.cw_rset, row=row, col=col) |
358 itemview = self._cw.view(vid, self.cw_rset, row=row, col=col) |
129 last_class = morekwargs['is_last'] and ' class="last"' or '' |
359 last_class = morekwargs['is_last'] and ' class="last"' or '' |
130 self.w(u'<li%s>%s</li>' % (last_class, itemview)) |
360 self.w(u'<li%s>%s</li>' % (last_class, itemview)) |
131 |
361 |
132 |
362 |
133 class TreeViewItemView(EntityView): |
363 class TreeViewItemView(EntityView): |
134 """specific treeitem view for entities which implement ITree |
364 """specific treeitem view for entities which adapt to ITree |
135 |
365 |
136 (each item should be expandable if it's not a tree leaf) |
366 (each item should be expandable if it's not a tree leaf) |
137 """ |
367 """ |
138 __regid__ = 'treeitemview' |
368 __regid__ = 'treeitemview' |
139 __select__ = implements(ITree) |
369 __select__ = adaptable('ITree') |
140 default_branch_state_is_open = False |
370 default_branch_state_is_open = False |
141 |
371 |
142 def open_state(self, eeid, treeid): |
372 def open_state(self, eeid, treeid): |
143 cookies = self._cw.get_cookie() |
373 cookies = self._cw.get_cookie() |
144 treestate = cookies.get(treecookiename(treeid)) |
374 treestate = cookies.get(treecookiename(treeid)) |