Mercurial > libervia-backend
comparison sat_frontends/quick_frontend/quick_blog.py @ 2562:26edcf3a30eb
core, setup: huge cleaning:
- moved directories from src and frontends/src to sat and sat_frontends, which is the recommanded naming convention
- move twisted directory to root
- removed all hacks from setup.py, and added missing dependencies, it is now clean
- use https URL for website in setup.py
- removed "Environment :: X11 Applications :: GTK", as wix is deprecated and removed
- renamed sat.sh to sat and fixed its installation
- added python_requires to specify Python version needed
- replaced glib2reactor which use deprecated code by gtk3reactor
sat can now be installed directly from virtualenv without using --system-site-packages anymore \o/
author | Goffi <goffi@goffi.org> |
---|---|
date | Mon, 02 Apr 2018 19:44:50 +0200 |
parents | frontends/src/quick_frontend/quick_blog.py@0046283a285d |
children | 56f94936df1e |
comparison
equal
deleted
inserted
replaced
2561:bd30dc3ffe5a | 2562:26edcf3a30eb |
---|---|
1 #!/usr/bin/env python2 | |
2 # -*- coding: utf-8 -*- | |
3 | |
4 # helper class for making a SAT frontend | |
5 # Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> | |
6 | |
7 # This program is free software: you can redistribute it and/or modify | |
8 # it under the terms of the GNU Affero General Public License as published by | |
9 # the Free Software Foundation, either version 3 of the License, or | |
10 # (at your option) any later version. | |
11 | |
12 # This program is distributed in the hope that it will be useful, | |
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
15 # GNU Affero General Public License for more details. | |
16 | |
17 # You should have received a copy of the GNU Affero General Public License | |
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
19 | |
20 # from sat.core.i18n import _, D_ | |
21 from sat.core.log import getLogger | |
22 log = getLogger(__name__) | |
23 | |
24 | |
25 from sat_frontends.quick_frontend.constants import Const as C | |
26 from sat_frontends.quick_frontend import quick_widgets | |
27 from sat_frontends.tools import jid | |
28 from sat.tools.common import data_format | |
29 | |
30 try: | |
31 # FIXME: to be removed when an acceptable solution is here | |
32 unicode('') # XXX: unicode doesn't exist in pyjamas | |
33 except (TypeError, AttributeError): # Error raised is not the same depending on pyjsbuild options | |
34 unicode = str | |
35 | |
36 ENTRY_CLS = None | |
37 COMMENTS_CLS = None | |
38 | |
39 | |
40 class Item(object): | |
41 """Manage all (meta)data of an item""" | |
42 | |
43 def __init__(self, data): | |
44 """ | |
45 @param data(dict): microblog data as return by bridge methods | |
46 if data values are not defined, set default values | |
47 """ | |
48 self.id = data['id'] | |
49 self.title = data.get('title') | |
50 self.title_rich = None | |
51 self.title_xhtml = data.get('title_xhtml') | |
52 self.tags = list(data_format.dict2iter('tag', data)) | |
53 self.content = data.get('content') | |
54 self.content_rich = None | |
55 self.content_xhtml = data.get('content_xhtml') | |
56 self.author = data['author'] | |
57 try: | |
58 author_jid = data['author_jid'] | |
59 self.author_jid = jid.JID(author_jid) if author_jid else None | |
60 except KeyError: | |
61 self.author_jid = None | |
62 | |
63 try: | |
64 self.author_verified = C.bool(data['author_jid_verified']) | |
65 except KeyError: | |
66 self.author_verified = False | |
67 | |
68 try: | |
69 self.updated = float(data['updated']) # XXX: int doesn't work here (pyjamas bug) | |
70 except KeyError: | |
71 self.updated = None | |
72 | |
73 try: | |
74 self.published = float(data['published']) # XXX: int doesn't work here (pyjamas bug) | |
75 except KeyError: | |
76 self.published = None | |
77 | |
78 self.comments = data.get('comments') | |
79 try: | |
80 self.comments_service = jid.JID(data['comments_service']) | |
81 except KeyError: | |
82 self.comments_service = None | |
83 self.comments_node = data.get('comments_node') | |
84 | |
85 # def loadComments(self): | |
86 # """Load all the comments""" | |
87 # index = str(main_entry.comments_count - main_entry.hidden_count) | |
88 # rsm = {'max': str(main_entry.hidden_count), 'index': index} | |
89 # self.host.bridge.call('getMblogComments', self.mblogsInsert, main_entry.comments_service, main_entry.comments_node, rsm) | |
90 | |
91 | |
92 class EntriesManager(object): | |
93 """Class which manages list of (micro)blog entries""" | |
94 | |
95 def __init__(self, manager): | |
96 """ | |
97 @param manager (EntriesManager, None): parent EntriesManager | |
98 must be None for QuickBlog (and only for QuickBlog) | |
99 """ | |
100 self.manager = manager | |
101 if manager is None: | |
102 self.blog = self | |
103 else: | |
104 self.blog = manager.blog | |
105 self.entries = [] | |
106 self.edit_entry = None | |
107 | |
108 @property | |
109 def level(self): | |
110 """indicate how deep is this entry in the tree | |
111 | |
112 if level == -1, we have a QuickBlog | |
113 if level == 0, we have a main item | |
114 else we have a comment | |
115 """ | |
116 level = -1 | |
117 manager = self.manager | |
118 while manager is not None: | |
119 level += 1 | |
120 manager = manager.manager | |
121 return level | |
122 | |
123 def _addMBItems(self, items_tuple, service=None, node=None): | |
124 """Add Microblog items to this panel | |
125 update is NOT called after addition | |
126 | |
127 @param items_tuple(tuple): (items_data,items_metadata) tuple as returned by mbGet | |
128 """ | |
129 items, metadata = items_tuple | |
130 for item in items: | |
131 self.addEntry(item, service=service, node=node, with_update=False) | |
132 | |
133 def _addMBItemsWithComments(self, items_tuple, service=None, node=None): | |
134 """Add Microblog items to this panel | |
135 update is NOT called after addition | |
136 | |
137 @param items_tuple(tuple): (items_data,items_metadata) tuple as returned by mbGet | |
138 """ | |
139 items, metadata = items_tuple | |
140 for item, comments in items: | |
141 self.addEntry(item, comments, service=service, node=node, with_update=False) | |
142 | |
143 def addEntry(self, item=None, comments=None, service=None, node=None, with_update=True, editable=False, edit_entry=False): | |
144 """Add a microblog entry | |
145 | |
146 @param editable (bool): True if the entry can be modified | |
147 @param item (dict, None): blog item data, or None for an empty entry | |
148 @param comments (list, None): list of comments data if available | |
149 @param service (jid.JID, None): service where the entry is coming from | |
150 @param service (unicode, None): node hosting the entry | |
151 @param with_update (bool): if True, udpate is called with the new entry | |
152 @param edit_entry(bool): if True, will be in self.edit_entry instead of | |
153 self.entries, so it can be managed separately (e.g. first or last | |
154 entry regardless of sorting) | |
155 """ | |
156 new_entry = ENTRY_CLS(self, item, comments, service=service, node=node) | |
157 new_entry.setEditable(editable) | |
158 if edit_entry: | |
159 self.edit_entry = new_entry | |
160 else: | |
161 self.entries.append(new_entry) | |
162 if with_update: | |
163 self.update() | |
164 return new_entry | |
165 | |
166 def update(self, entry=None): | |
167 """Update the display with entries | |
168 | |
169 @param entry (Entry, None): if not None, must be the new entry. | |
170 If None, all the items will be checked to update the display | |
171 """ | |
172 # update is separated from addEntry to allow adding | |
173 # several entries at once, and updating at the end | |
174 raise NotImplementedError | |
175 | |
176 | |
177 class Entry(EntriesManager): | |
178 """Graphical representation of an Item | |
179 This class must be overriden by frontends""" | |
180 | |
181 def __init__(self, manager, item_data=None, comments_data=None, service=None, node=None): | |
182 """ | |
183 @param blog(QuickBlog): the parent QuickBlog | |
184 @param manager(EntriesManager): the parent EntriesManager | |
185 @param item_data(dict, None): dict containing the blog item data, or None for an empty entry | |
186 @param comments_data(list, None): list of comments data | |
187 """ | |
188 assert manager is not None | |
189 EntriesManager.__init__(self, manager) | |
190 self.service = service | |
191 self.node = node | |
192 self.editable = False | |
193 self.reset(item_data) | |
194 self.blog.id2entries[self.item.id] = self | |
195 if self.item.comments: | |
196 node_tuple = (self.item.comments_service, self.item.comments_node) | |
197 self.blog.node2entries.setdefault(node_tuple,[]).append(self) | |
198 | |
199 def reset(self, item_data): | |
200 """Reset the entry with given data | |
201 | |
202 used during init (it's a set and not a reset then) | |
203 or later (e.g. message sent, or cancellation of an edition | |
204 @param idem_data(dict, None): data as in __init__ | |
205 """ | |
206 if item_data is None: | |
207 self.new = True | |
208 item_data = {'id': None, | |
209 # TODO: find a best author value | |
210 'author': self.blog.host.whoami.node | |
211 } | |
212 else: | |
213 self.new = False | |
214 self.item = Item(item_data) | |
215 self.author_jid = self.blog.host.whoami.bare if self.new else self.item.author_jid | |
216 if self.author_jid is None and self.service and self.service.node: | |
217 self.author_jid = self.service | |
218 self.mode = C.ENTRY_MODE_TEXT if self.item.content_xhtml is None else C.ENTRY_MODE_XHTML | |
219 | |
220 def refresh(self): | |
221 """Refresh the display when data have been modified""" | |
222 pass | |
223 | |
224 def setEditable(self, editable=True): | |
225 """tell if the entry can be edited or not | |
226 | |
227 @param editable(bool): True if the entry can be edited | |
228 """ | |
229 #XXX: we don't use @property as property setter doesn't play well with pyjamas | |
230 raise NotImplementedError | |
231 | |
232 def addComments(self, comments_data): | |
233 """Add comments to this entry by calling addEntry repeatidly | |
234 | |
235 @param comments_data(tuple): data as returned by mbGetFromMany*RTResults | |
236 """ | |
237 # TODO: manage seperator between comments of coming from different services/nodes | |
238 for data in comments_data: | |
239 service, node, failure, comments, metadata = data | |
240 for comment in comments: | |
241 if not failure: | |
242 self.addEntry(comment, service=jid.JID(service), node=node) | |
243 else: | |
244 log.warning("getting comment failed: {}".format(failure)) | |
245 self.update() | |
246 | |
247 def send(self): | |
248 """Send entry according to parent QuickBlog configuration and current level""" | |
249 | |
250 # keys to keep other than content*, title* and tag* | |
251 # FIXME: see how to avoid comments node hijacking (someone could bind his post to another post's comments node) | |
252 keys_to_keep = ('id', 'comments', 'author', 'author_jid', 'published') | |
253 | |
254 mb_data = {} | |
255 for key in keys_to_keep: | |
256 value = getattr(self.item, key) | |
257 if value is not None: | |
258 mb_data[key] = unicode(value) | |
259 | |
260 for prefix in ('content', 'title'): | |
261 for suffix in ('', '_rich', '_xhtml'): | |
262 name = '{}{}'.format(prefix, suffix) | |
263 value = getattr(self.item, name) | |
264 if value is not None: | |
265 mb_data[name] = value | |
266 | |
267 data_format.iter2dict('tag', self.item.tags, mb_data) | |
268 | |
269 if self.blog.new_message_target not in (C.PUBLIC, C.GROUP): | |
270 raise NotImplementedError | |
271 | |
272 if self.level == 0: | |
273 mb_data["allow_comments"] = C.BOOL_TRUE | |
274 | |
275 if self.blog.new_message_target == C.GROUP: | |
276 data_format.iter2dict('group', self.blog.targets, mb_data) | |
277 | |
278 self.blog.host.bridge.mbSend( | |
279 unicode(self.service or ''), | |
280 self.node or '', | |
281 mb_data, | |
282 profile=self.blog.profile) | |
283 | |
284 def delete(self): | |
285 """Remove this Entry from parent manager | |
286 | |
287 This doesn't delete any entry in PubSub, just locally | |
288 all children entries will be recursively removed too | |
289 """ | |
290 # XXX: named delete and not remove to avoid conflict with pyjamas | |
291 log.debug(u"deleting entry {}".format('EDIT ENTRY' if self.new else self.item.id)) | |
292 for child in self.entries: | |
293 child.delete() | |
294 try: | |
295 self.manager.entries.remove(self) | |
296 except ValueError: | |
297 if self != self.manager.edit_entry: | |
298 log.error(u"Internal Error: entry not found in manager") | |
299 else: | |
300 self.manager.edit_entry = None | |
301 if not self.new: | |
302 # we must remove references to self | |
303 # in QuickBlog's dictionary | |
304 del self.blog.id2entries[self.item.id] | |
305 if self.item.comments: | |
306 comments_tuple = (self.item.comments_service, | |
307 self.item.comments_node) | |
308 other_entries = self.blog.node2entries[comments_tuple].remove(self) | |
309 if not other_entries: | |
310 del self.blog.node2entries[comments_tuple] | |
311 | |
312 def retract(self): | |
313 """Retract this item from microblog node | |
314 | |
315 if there is a comments node, it will be purged too | |
316 """ | |
317 # TODO: manage several comments nodes case. | |
318 if self.item.comments: | |
319 self.blog.host.bridge.psNodeDelete(unicode(self.item.comments_service) or "", self.item.comments_node, profile=self.blog.profile) | |
320 self.blog.host.bridge.mbRetract(unicode(self.service or ""), self.node or "", self.item.id, profile=self.blog.profile) | |
321 | |
322 | |
323 class QuickBlog(EntriesManager, quick_widgets.QuickWidget): | |
324 | |
325 def __init__(self, host, targets, profiles=None): | |
326 """Panel used to show microblog | |
327 | |
328 @param targets (tuple(unicode)): contact groups displayed in this panel. | |
329 If empty, show all microblogs from all contacts. targets is also used | |
330 to know where to send new messages. | |
331 """ | |
332 EntriesManager.__init__(self, None) | |
333 self.id2entries = {} # used to find an entry with it's item id | |
334 # must be kept up-to-date by Entry | |
335 self.node2entries = {} # same as above, values are lists in case of | |
336 # two entries link to the same comments node | |
337 if not targets: | |
338 targets = () # XXX: we use empty tuple instead of None to workaround a pyjamas bug | |
339 quick_widgets.QuickWidget.__init__(self, host, targets, C.PROF_KEY_NONE) | |
340 self._targets_type = C.ALL | |
341 else: | |
342 assert isinstance(targets[0], basestring) | |
343 quick_widgets.QuickWidget.__init__(self, host, targets[0], C.PROF_KEY_NONE) | |
344 for target in targets[1:]: | |
345 assert isinstance(target, basestring) | |
346 self.addTarget(target) | |
347 self._targets_type = C.GROUP | |
348 | |
349 @property | |
350 def new_message_target(self): | |
351 if self._targets_type == C.ALL: | |
352 return C.PUBLIC | |
353 elif self._targets_type == C.GROUP: | |
354 return C.GROUP | |
355 else: | |
356 raise ValueError("Unkown targets type") | |
357 | |
358 def __str__(self): | |
359 return u"Blog Widget [target: {}, profile: {}]".format(', '.join(self.targets), self.profile) | |
360 | |
361 def _getResultsCb(self, data, rt_session): | |
362 remaining, results = data | |
363 log.debug("Got {got_len} results, {rem_len} remaining".format(got_len=len(results), rem_len=remaining)) | |
364 for result in results: | |
365 service, node, failure, items, metadata = result | |
366 if not failure: | |
367 self._addMBItemsWithComments((items, metadata), service=jid.JID(service)) | |
368 | |
369 self.update() | |
370 if remaining: | |
371 self._getResults(rt_session) | |
372 | |
373 def _getResultsEb(self, failure): | |
374 log.warning("microblog getFromMany error: {}".format(failure)) | |
375 | |
376 def _getResults(self, rt_session): | |
377 """Manage results from mbGetFromMany RT Session | |
378 | |
379 @param rt_session(str): session id as returned by mbGetFromMany | |
380 """ | |
381 self.host.bridge.mbGetFromManyWithCommentsRTResult(rt_session, profile=self.profile, | |
382 callback=lambda data:self._getResultsCb(data, rt_session), | |
383 errback=self._getResultsEb) | |
384 | |
385 def getAll(self): | |
386 """Get all (micro)blogs from self.targets""" | |
387 def gotSession(rt_session): | |
388 self._getResults(rt_session) | |
389 | |
390 if self._targets_type in (C.ALL, C.GROUP): | |
391 targets = tuple(self.targets) if self._targets_type is C.GROUP else () | |
392 self.host.bridge.mbGetFromManyWithComments(self._targets_type, targets, 10, 10, {}, {"subscribe":C.BOOL_TRUE}, profile=self.profile, callback=gotSession) | |
393 own_pep = self.host.whoami.bare | |
394 self.host.bridge.mbGetFromManyWithComments(C.JID, (unicode(own_pep),), 10, 10, {}, {}, profile=self.profile, callback=gotSession) | |
395 else: | |
396 raise NotImplementedError(u'{} target type is not managed'.format(self._targets_type)) | |
397 | |
398 def isJidAccepted(self, jid_): | |
399 """Tell if a jid is actepted and must be shown in this panel | |
400 | |
401 @param jid_(jid.JID): jid to check | |
402 @return: True if the jid is accepted | |
403 """ | |
404 if self._targets_type == C.ALL: | |
405 return True | |
406 assert self._targets_type is C.GROUP # we don't manage other types for now | |
407 for group in self.targets: | |
408 if self.host.contact_lists[self.profile].isEntityInGroup(jid_, group): | |
409 return True | |
410 return False | |
411 | |
412 def addEntryIfAccepted(self, service, node, mb_data, groups, profile): | |
413 """add entry to this panel if it's acceptable | |
414 | |
415 This method check if the entry is new or an update, | |
416 if it below to a know node, or if it acceptable anyway | |
417 @param service(jid.JID): jid of the emitting pubsub service | |
418 @param node(unicode): node identifier | |
419 @param mb_data: microblog data | |
420 @param groups(list[unicode], None): groups which can receive this entry | |
421 None to accept everything | |
422 @param profile: %(doc_profile)s | |
423 """ | |
424 try: | |
425 entry = self.id2entries[mb_data['id']] | |
426 except KeyError: | |
427 # The entry is new | |
428 try: | |
429 parent_entries = self.node2entries[(service, node)] | |
430 except: | |
431 # The node is unknown, | |
432 # we need to check that we can accept the entry | |
433 if (self.isJidAccepted(service) | |
434 or (groups is None and service == self.host.profiles[self.profile].whoami.bare) | |
435 or (groups and groups.intersection(self.targets))): | |
436 self.addEntry(mb_data, service=service, node=node) | |
437 else: | |
438 # the entry is a comment in a known node | |
439 for parent_entry in parent_entries: | |
440 parent_entry.addEntry(mb_data, service=service, node=node) | |
441 else: | |
442 # The entry exist, it's an update | |
443 entry.reset(mb_data) | |
444 entry.refresh() | |
445 | |
446 def deleteEntryIfPresent(self, service, node, item_id, profile): | |
447 """Delete and entry if present in this QuickBlog | |
448 | |
449 @param sender(jid.JID): jid of the entry sender | |
450 @param mb_data: microblog data | |
451 @param service(jid.JID): sending service | |
452 @param node(unicode): hosting node | |
453 """ | |
454 try: | |
455 entry = self.id2entries[item_id] | |
456 except KeyError: | |
457 pass | |
458 else: | |
459 entry.delete() | |
460 | |
461 | |
462 def registerClass(type_, cls): | |
463 global ENTRY_CLS, COMMENTS_CLS | |
464 if type_ == "ENTRY": | |
465 ENTRY_CLS = cls | |
466 elif type == "COMMENT": | |
467 COMMENTS_CLS = cls | |
468 else: | |
469 raise ValueError("type_ should be ENTRY or COMMENT") | |
470 if COMMENTS_CLS is None: | |
471 COMMENTS_CLS = ENTRY_CLS |