Mercurial > libervia-web
comparison src/browser/sat_browser/blog.py @ 589:a5019e62c3e9 frontends_multi_profiles
browser side: big refactoring to base Libervia on QuickFrontend, first draft:
/!\ not finished, partially working and highly instable
- add collections module with an OrderedDict like class
- SatWebFrontend inherit from QuickApp
- general sat_frontends tools.jid module is used
- bridge/json methods have moved to json module
- UniBox is partially removed (should be totally removed before merge to trunk)
- Signals are now register with the generic registerSignal method (which is called mainly in QuickFrontend)
- the generic getOrCreateWidget method from QuickWidgetsManager is used instead of Libervia's specific methods
- all Widget are now based more or less directly on QuickWidget
- with the new QuickWidgetsManager.getWidgets method, it's no more necessary to check all widgets which are instance of a particular class
- ChatPanel and related moved to chat module
- MicroblogPanel and related moved to blog module
- global and overcomplicated send method has been disabled: each class should manage its own sending
- for consistency with other frontends, former ContactPanel has been renamed to ContactList and vice versa
- for the same reason, ChatPanel has been renamed to Chat
- for compatibility with QuickFrontend, a fake profile is used in several places, it is set to C.PROF_KEY_NONE (real profile is managed server side for obvious security reasons)
- changed default url for web panel to SàT website, and contact address to generic SàT contact address
- ContactList is based on QuickContactList, UI changes are done in update method
- bride call (now json module) have been greatly improved, in particular call can be done in the same way as for other frontends (bridge.method_name(arg1, arg2, ..., callback=cb, errback=eb). Blocking method must be called like async methods due to javascript architecture
- in bridge calls, a callback can now exists without errback
- hard reload on BridgeSignals remote error has been disabled, a better option should be implemented
- use of constants where that make sens, some style improvments
- avatars are temporarily disabled
- lot of code disabled, will be fixed or removed before merge
- various other changes, check diff for more details
server side: manage remote exception on getEntityData, removed getProfileJid call, added getWaitingConf, added getRoomsSubjects
author | Goffi <goffi@goffi.org> |
---|---|
date | Sat, 24 Jan 2015 01:45:39 +0100 |
parents | src/browser/sat_browser/panels.py@bade589dbd5a |
children | d78126d82ca0 |
comparison
equal
deleted
inserted
replaced
585:bade589dbd5a | 589:a5019e62c3e9 |
---|---|
1 #!/usr/bin/python | |
2 # -*- coding: utf-8 -*- | |
3 | |
4 # Libervia: a Salut à Toi frontend | |
5 # Copyright (C) 2011, 2012, 2013, 2014 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 import pyjd # this is dummy in pyjs | |
21 from sat.core.log import getLogger | |
22 log = getLogger(__name__) | |
23 | |
24 from sat.core.i18n import _ | |
25 | |
26 from pyjamas.ui.SimplePanel import SimplePanel | |
27 from pyjamas.ui.VerticalPanel import VerticalPanel | |
28 from pyjamas.ui.HorizontalPanel import HorizontalPanel | |
29 from pyjamas.ui.HTMLPanel import HTMLPanel | |
30 from pyjamas.ui.Label import Label | |
31 from pyjamas.ui.Button import Button | |
32 from pyjamas.ui.HTML import HTML | |
33 from pyjamas.ui.Image import Image | |
34 from pyjamas.ui.ClickListener import ClickHandler | |
35 from pyjamas.ui.FlowPanel import FlowPanel | |
36 from pyjamas.ui.KeyboardListener import KEY_ENTER, KeyboardHandler | |
37 from pyjamas.ui.FocusListener import FocusHandler | |
38 from pyjamas.Timer import Timer | |
39 | |
40 from datetime import datetime | |
41 from time import time | |
42 | |
43 import html_tools | |
44 import base_panels | |
45 import dialog | |
46 import base_widget | |
47 import richtext | |
48 from constants import Const as C | |
49 from sat_frontends.quick_frontend import quick_widgets | |
50 | |
51 # TODO: at some point we should decide which behaviors to keep and remove these two constants | |
52 TOGGLE_EDITION_USE_ICON = False # set to True to use an icon inside the "toggle syntax" button | |
53 NEW_MESSAGE_USE_BUTTON = False # set to True to display the "New message" button instead of an empty entry | |
54 | |
55 | |
56 class MicroblogItem(): | |
57 # XXX: should be moved in a separated module | |
58 | |
59 def __init__(self, data): | |
60 self.id = data['id'] | |
61 self.type = data.get('type', 'main_item') | |
62 self.empty = data.get('new', False) | |
63 self.title = data.get('title', '') | |
64 self.title_xhtml = data.get('title_xhtml', '') | |
65 self.content = data.get('content', '') | |
66 self.content_xhtml = data.get('content_xhtml', '') | |
67 self.author = data['author'] | |
68 self.updated = float(data.get('updated', 0)) # XXX: int doesn't work here | |
69 self.published = float(data.get('published', self.updated)) # XXX: int doesn't work here | |
70 self.service = data.get('service', '') | |
71 self.node = data.get('node', '') | |
72 self.comments = data.get('comments', False) | |
73 self.comments_service = data.get('comments_service', '') | |
74 self.comments_node = data.get('comments_node', '') | |
75 | |
76 | |
77 class MicroblogEntry(SimplePanel, ClickHandler, FocusHandler, KeyboardHandler): | |
78 | |
79 def __init__(self, blog_panel, data): | |
80 """ | |
81 @param blog_panel: the parent panel | |
82 @param data: dict containing the blog item data, or a MicroblogItem instance. | |
83 """ | |
84 self._base_item = data if isinstance(data, MicroblogItem) else MicroblogItem(data) | |
85 for attr in ['id', 'type', 'empty', 'title', 'title_xhtml', 'content', 'content_xhtml', | |
86 'author', 'updated', 'published', 'comments', 'service', 'node', | |
87 'comments_service', 'comments_node']: | |
88 getter = lambda attr: lambda inst: getattr(inst._base_item, attr) | |
89 setter = lambda attr: lambda inst, value: setattr(inst._base_item, attr, value) | |
90 setattr(MicroblogEntry, attr, property(getter(attr), setter(attr))) | |
91 | |
92 SimplePanel.__init__(self) | |
93 self._blog_panel = blog_panel | |
94 | |
95 self.panel = FlowPanel() | |
96 self.panel.setStyleName('mb_entry') | |
97 | |
98 self.header = HTMLPanel('') | |
99 self.panel.add(self.header) | |
100 | |
101 self.entry_actions = VerticalPanel() | |
102 self.entry_actions.setStyleName('mb_entry_actions') | |
103 self.panel.add(self.entry_actions) | |
104 | |
105 entry_avatar = SimplePanel() | |
106 entry_avatar.setStyleName('mb_entry_avatar') | |
107 # FIXME | |
108 self.avatar = Image(C.DEFAULT_AVATAR) # self._blog_panel.host.getAvatar(self.author)) | |
109 entry_avatar.add(self.avatar) | |
110 self.panel.add(entry_avatar) | |
111 | |
112 if TOGGLE_EDITION_USE_ICON: | |
113 self.entry_dialog = HorizontalPanel() | |
114 else: | |
115 self.entry_dialog = VerticalPanel() | |
116 self.entry_dialog.setStyleName('mb_entry_dialog') | |
117 self.panel.add(self.entry_dialog) | |
118 | |
119 self.add(self.panel) | |
120 ClickHandler.__init__(self) | |
121 self.addClickListener(self) | |
122 | |
123 self.__pub_data = (self.service, self.node, self.id) | |
124 self.__setContent() | |
125 | |
126 def __setContent(self): | |
127 """Actually set the entry content (header, icons, bubble...)""" | |
128 self.delete_label = self.update_label = self.comment_label = None | |
129 self.bubble = self._current_comment = None | |
130 self.__setHeader() | |
131 self.__setBubble() | |
132 self.__setIcons() | |
133 | |
134 def __setHeader(self): | |
135 """Set the entry header""" | |
136 if self.empty: | |
137 return | |
138 update_text = u" — ✍ " + "<span class='mb_entry_timestamp'>%s</span>" % datetime.fromtimestamp(self.updated) | |
139 self.header.setHTML("""<div class='mb_entry_header'> | |
140 <span class='mb_entry_author'>%(author)s</span> on | |
141 <span class='mb_entry_timestamp'>%(published)s</span>%(updated)s | |
142 </div>""" % {'author': html_tools.html_sanitize(self.author), | |
143 'published': datetime.fromtimestamp(self.published), | |
144 'updated': update_text if self.published != self.updated else '' | |
145 } | |
146 ) | |
147 | |
148 def __setIcons(self): | |
149 """Set the entry icons (delete, update, comment)""" | |
150 if self.empty: | |
151 return | |
152 | |
153 def addIcon(label, title): | |
154 label = Label(label) | |
155 label.setTitle(title) | |
156 label.addClickListener(self) | |
157 self.entry_actions.add(label) | |
158 return label | |
159 | |
160 if self.comments: | |
161 self.comment_label = addIcon(u"↶", "Comment this message") | |
162 self.comment_label.setStyleName('mb_entry_action_larger') | |
163 is_publisher = self.author == self._blog_panel.host.whoami.bare | |
164 if is_publisher: | |
165 self.update_label = addIcon(u"✍", "Edit this message") | |
166 if is_publisher or str(self.node).endswith(self._blog_panel.host.whoami.bare): | |
167 self.delete_label = addIcon(u"✗", "Delete this message") | |
168 | |
169 def updateAvatar(self, new_avatar): | |
170 """Change the avatar of the entry | |
171 @param new_avatar: path to the new image""" | |
172 self.avatar.setUrl(new_avatar) | |
173 | |
174 def onClick(self, sender): | |
175 if sender == self: | |
176 self._blog_panel.setSelectedEntry(self) | |
177 elif sender == self.delete_label: | |
178 self._delete() | |
179 elif sender == self.update_label: | |
180 self.edit(True) | |
181 elif sender == self.comment_label: | |
182 self._comment() | |
183 | |
184 def __modifiedCb(self, content): | |
185 """Send the new content to the backend | |
186 @return: False to restore the original content if a deletion has been cancelled | |
187 """ | |
188 if not content['text']: # previous content has been emptied | |
189 self._delete(True) | |
190 return False | |
191 extra = {'published': str(self.published)} | |
192 if isinstance(self.bubble, richtext.RichTextEditor): | |
193 # TODO: if the user change his parameters after the message edition started, | |
194 # the message syntax could be different then the current syntax: pass the | |
195 # message syntax in extra for the frontend to use it instead of current syntax. | |
196 extra.update({'content_rich': content['text'], 'title': content['title']}) | |
197 if self.empty: | |
198 if self.type == 'main_item': | |
199 self._blog_panel.host.bridge.call('sendMblog', None, None, self._blog_panel.accepted_groups, content['text'], extra) | |
200 else: | |
201 self._blog_panel.host.bridge.call('sendMblogComment', None, self._parent_entry.comments, content['text'], extra) | |
202 else: | |
203 self._blog_panel.host.bridge.call('updateMblog', None, self.__pub_data, self.comments, content['text'], extra) | |
204 return True | |
205 | |
206 def __afterEditCb(self, content): | |
207 """Remove the entry if it was an empty one (used for creating a new blog post). | |
208 Data for the actual new blog post will be received from the bridge""" | |
209 if self.empty: | |
210 self._blog_panel.removeEntry(self.type, self.id) | |
211 if self.type == 'main_item': # restore the "New message" button | |
212 self._blog_panel.refresh() | |
213 else: # allow to create a new comment | |
214 self._parent_entry._current_comment = None | |
215 self.entry_dialog.setWidth('auto') | |
216 try: | |
217 self.toggle_syntax_button.removeFromParent() | |
218 except TypeError: | |
219 pass | |
220 | |
221 def __setBubble(self, edit=False): | |
222 """Set the bubble displaying the initial content.""" | |
223 content = {'text': self.content_xhtml if self.content_xhtml else self.content, | |
224 'title': self.title_xhtml if self.title_xhtml else self.title} | |
225 if self.content_xhtml: | |
226 content.update({'syntax': C.SYNTAX_XHTML}) | |
227 if self.author != self._blog_panel.host.whoami.bare: | |
228 options = ['read_only'] | |
229 else: | |
230 options = [] if self.empty else ['update_msg'] | |
231 self.bubble = richtext.RichTextEditor(self._blog_panel.host, content, self.__modifiedCb, self.__afterEditCb, options) | |
232 else: # assume raw text message have no title | |
233 self.bubble = base_panels.LightTextEditor(content, self.__modifiedCb, self.__afterEditCb, options={'no_xhtml': True}) | |
234 self.bubble.addStyleName("bubble") | |
235 try: | |
236 self.toggle_syntax_button.removeFromParent() | |
237 except TypeError: | |
238 pass | |
239 self.entry_dialog.add(self.bubble) | |
240 self.edit(edit) | |
241 self.bubble.addEditListener(self.__showWarning) | |
242 | |
243 def __showWarning(self, sender, keycode): | |
244 if keycode == KEY_ENTER: | |
245 self._blog_panel.host.showWarning(None, None) | |
246 else: | |
247 self._blog_panel.host.showWarning(*self._blog_panel.getWarningData(self.type == 'comment')) | |
248 | |
249 def _delete(self, empty=False): | |
250 """Ask confirmation for deletion. | |
251 @return: False if the deletion has been cancelled.""" | |
252 def confirm_cb(answer): | |
253 if answer: | |
254 self._blog_panel.host.bridge.call('deleteMblog', None, self.__pub_data, self.comments) | |
255 else: # restore the text if it has been emptied during the edition | |
256 self.bubble.setContent(self.bubble._original_content) | |
257 | |
258 if self.empty: | |
259 text = _("New ") + (_("message") if self.comments else _("comment")) + _(" without body has been cancelled.") | |
260 dialog.InfoDialog(_("Information"), text).show() | |
261 return | |
262 text = "" | |
263 if empty: | |
264 text = (_("Message") if self.comments else _("Comment")) + _(" body has been emptied.<br/>") | |
265 target = _('message and all its comments') if self.comments else _('comment') | |
266 text += _("Do you really want to delete this %s?") % target | |
267 dialog.ConfirmDialog(confirm_cb, text=text).show() | |
268 | |
269 def _comment(self): | |
270 """Add an empty entry for a new comment""" | |
271 if self._current_comment: | |
272 self._current_comment.bubble.setFocus(True) | |
273 self._blog_panel.setSelectedEntry(self._current_comment, True) | |
274 return | |
275 data = {'id': str(time()), | |
276 'new': True, | |
277 'type': 'comment', | |
278 'author': self._blog_panel.host.whoami.bare, | |
279 'service': self.comments_service, | |
280 'node': self.comments_node | |
281 } | |
282 entry = self._blog_panel.addEntry(data) | |
283 if entry is None: | |
284 log.info("The entry of id %s can not be commented" % self.id) | |
285 return | |
286 entry._parent_entry = self | |
287 self._current_comment = entry | |
288 self.edit(True, entry) | |
289 self._blog_panel.setSelectedEntry(entry, True) | |
290 | |
291 def edit(self, edit, entry=None): | |
292 """Toggle the bubble between display and edit mode | |
293 @edit: boolean value | |
294 @entry: MicroblogEntry instance, or None to use self | |
295 """ | |
296 if entry is None: | |
297 entry = self | |
298 try: | |
299 entry.toggle_syntax_button.removeFromParent() | |
300 except TypeError: | |
301 pass | |
302 entry.bubble.edit(edit) | |
303 if edit: | |
304 if isinstance(entry.bubble, richtext.RichTextEditor): | |
305 image = '<a class="richTextIcon">A</a>' | |
306 html = '<a style="color: blue;">raw text</a>' | |
307 title = _('Switch to raw text edition') | |
308 else: | |
309 image = '<img src="media/icons/tango/actions/32/format-text-italic.png" class="richTextIcon"/>' | |
310 html = '<a style="color: blue;">rich text</a>' | |
311 title = _('Switch to rich text edition') | |
312 if TOGGLE_EDITION_USE_ICON: | |
313 entry.entry_dialog.setWidth('80%') | |
314 entry.toggle_syntax_button = Button(image, entry.toggleContentSyntax) | |
315 entry.toggle_syntax_button.setTitle(title) | |
316 entry.entry_dialog.add(entry.toggle_syntax_button) | |
317 else: | |
318 entry.toggle_syntax_button = HTML(html) | |
319 entry.toggle_syntax_button.addClickListener(entry.toggleContentSyntax) | |
320 entry.toggle_syntax_button.addStyleName('mb_entry_toggle_syntax') | |
321 entry.entry_dialog.add(entry.toggle_syntax_button) | |
322 entry.toggle_syntax_button.setStyleAttribute('top', '-20px') # XXX: need to force CSS | |
323 entry.toggle_syntax_button.setStyleAttribute('left', '-20px') | |
324 | |
325 def toggleContentSyntax(self): | |
326 """Toggle the editor between raw and rich text""" | |
327 original_content = self.bubble.getOriginalContent() | |
328 rich = not isinstance(self.bubble, richtext.RichTextEditor) | |
329 if rich: | |
330 original_content['syntax'] = C.SYNTAX_XHTML | |
331 | |
332 def setBubble(text): | |
333 self.content = text | |
334 self.content_xhtml = text if rich else '' | |
335 self.content_title = self.content_title_xhtml = '' | |
336 self.bubble.removeFromParent() | |
337 self.__setBubble(True) | |
338 self.bubble.setOriginalContent(original_content) | |
339 if rich: | |
340 self.bubble.setDisplayContent() # needed in case the edition is aborted, to not end with an empty bubble | |
341 | |
342 text = self.bubble.getContent()['text'] | |
343 if not text: | |
344 setBubble(' ') # something different than empty string is needed to initialize the rich text editor | |
345 return | |
346 if not rich: | |
347 def confirm_cb(answer): | |
348 if answer: | |
349 self._blog_panel.host.bridge.call('syntaxConvert', setBubble, text, C.SYNTAX_CURRENT, C.SYNTAX_TEXT) | |
350 dialog.ConfirmDialog(confirm_cb, text=_("Do you really want to lose the title and text formatting?")).show() | |
351 else: | |
352 self._blog_panel.host.bridge.call('syntaxConvert', setBubble, text, C.SYNTAX_TEXT, C.SYNTAX_XHTML) | |
353 | |
354 | |
355 class MicroblogPanel(quick_widgets.QuickWidget, base_widget.LiberviaWidget): | |
356 warning_msg_public = "This message will be <b>PUBLIC</b> and everybody will be able to see it, even people you don't know" | |
357 warning_msg_group = "This message will be published for all the people of the group <span class='warningTarget'>%s</span>" | |
358 # FIXME: all the generic parts must be moved to quick_frontends | |
359 | |
360 def __init__(self, host, accepted_groups, profiles=None): | |
361 """Panel used to show microblog | |
362 | |
363 @param accepted_groups: groups displayed in this panel, if empty, show all microblogs from all contacts | |
364 """ | |
365 self.setAcceptedGroup(accepted_groups) | |
366 quick_widgets.QuickWidget.__init__(self, host, self.target, C.PROF_KEY_NONE) | |
367 base_widget.LiberviaWidget.__init__(self, host, ", ".join(accepted_groups), selectable=True) | |
368 self.entries = {} | |
369 self.comments = {} | |
370 self.selected_entry = None | |
371 self.vpanel = VerticalPanel() | |
372 self.vpanel.setStyleName('microblogPanel') | |
373 self.setWidget(self.vpanel) | |
374 | |
375 @property | |
376 def target(self): | |
377 return tuple(self.accepted_groups) | |
378 | |
379 def refresh(self): | |
380 """Refresh the display of this widget. If the unibox is disabled, | |
381 display the 'New message' button or an empty bubble on top of the panel""" | |
382 if hasattr(self, 'new_button'): | |
383 self.new_button.setVisible(self.host.uni_box is None) | |
384 return | |
385 if self.host.uni_box is None: | |
386 def addBox(): | |
387 if hasattr(self, 'new_button'): | |
388 self.new_button.setVisible(False) | |
389 data = {'id': str(time()), | |
390 'new': True, | |
391 'author': self.host.whoami.bare, | |
392 } | |
393 entry = self.addEntry(data) | |
394 entry.edit(True) | |
395 if NEW_MESSAGE_USE_BUTTON: | |
396 self.new_button = Button("New message", listener=addBox) | |
397 self.new_button.setStyleName("microblogNewButton") | |
398 self.vpanel.insert(self.new_button, 0) | |
399 elif not self.getNewMainEntry(): | |
400 addBox() | |
401 | |
402 def getNewMainEntry(self): | |
403 """Get the new entry being edited, or None if it doesn't exists. | |
404 | |
405 @return (MicroblogEntry): the new entry being edited. | |
406 """ | |
407 try: | |
408 first = self.vpanel.children[0] | |
409 except IndexError: | |
410 return None | |
411 assert(first.type == 'main_item') | |
412 return first if first.empty else None | |
413 | |
414 @classmethod | |
415 def registerClass(cls): | |
416 base_widget.LiberviaWidget.addDropKey("GROUP", cls.createPanel) | |
417 base_widget.LiberviaWidget.addDropKey("CONTACT_TITLE", cls.createMetaPanel) | |
418 | |
419 @classmethod | |
420 def createPanel(cls, host, item): | |
421 """Generic panel creation for one, several or all groups (meta). | |
422 @parem host: the SatWebFrontend instance | |
423 @param item: single group as a string, list of groups | |
424 (as an array) or None (for the meta group = "all groups") | |
425 @return: the created MicroblogPanel | |
426 """ | |
427 _items = item if isinstance(item, list) else ([] if item is None else [item]) | |
428 _type = 'ALL' if _items == [] else 'GROUP' | |
429 # XXX: pyjamas doesn't support use of cls directly | |
430 _new_panel = MicroblogPanel(host, _items) | |
431 host.FillMicroblogPanel(_new_panel) | |
432 host.bridge.call('getMassiveLastMblogs', _new_panel.massiveInsert, _type, _items, 10) | |
433 host.setSelected(_new_panel) | |
434 _new_panel.refresh() | |
435 return _new_panel | |
436 | |
437 @classmethod | |
438 def createMetaPanel(cls, host, item): | |
439 """Needed for the drop keys to not be mixed between meta panel and panel for "Contacts" group""" | |
440 return MicroblogPanel.createPanel(host, None) | |
441 | |
442 @property | |
443 def accepted_groups(self): | |
444 return self._accepted_groups | |
445 | |
446 def matchEntity(self, item): | |
447 """ | |
448 @param item: single group as a string, list of groups | |
449 (as an array) or None (for the meta group = "all groups") | |
450 @return: True if self matches the given entity | |
451 """ | |
452 groups = item if isinstance(item, list) else ([] if item is None else [item]) | |
453 groups.sort() # sort() do not return the sorted list: do it here, not on the "return" line | |
454 return self.accepted_groups == groups | |
455 | |
456 def getWarningData(self, comment=None): | |
457 """ | |
458 @param comment: True if the composed message is a comment. If None, consider we are | |
459 composing from the unibox and guess the message type from self.selected_entry | |
460 @return: a couple (type, msg) for calling self.host.showWarning""" | |
461 if comment is None: # composing from the unibox | |
462 if self.selected_entry and not self.selected_entry.comments: | |
463 log.error("an item without comment is selected") | |
464 return ("NONE", None) | |
465 comment = self.selected_entry is not None | |
466 if comment: | |
467 return ("PUBLIC", "This is a <span class='warningTarget'>comment</span> and keep the initial post visibility, so it is potentialy public") | |
468 elif not self._accepted_groups: | |
469 # we have a meta MicroblogPanel, we publish publicly | |
470 return ("PUBLIC", self.warning_msg_public) | |
471 else: | |
472 # we only accept one group at the moment | |
473 # FIXME: manage several groups | |
474 return ("GROUP", self.warning_msg_group % self._accepted_groups[0]) | |
475 | |
476 def onTextEntered(self, text): | |
477 if self.selected_entry: | |
478 # we are entering a comment | |
479 comments_url = self.selected_entry.comments | |
480 if not comments_url: | |
481 raise Exception("ERROR: the comments URL is empty") | |
482 target = ("COMMENT", comments_url) | |
483 elif not self._accepted_groups: | |
484 # we are entering a public microblog | |
485 target = ("PUBLIC", None) | |
486 else: | |
487 # we are entering a microblog restricted to a group | |
488 # FIXME: manage several groups | |
489 target = ("GROUP", self._accepted_groups[0]) | |
490 self.host.send([target], text) | |
491 | |
492 def accept_all(self): | |
493 return not self._accepted_groups # we accept every microblog only if we are not filtering by groups | |
494 | |
495 def getEntries(self): | |
496 """Ask all the entries for the currenly accepted groups, | |
497 and fill the panel""" | |
498 | |
499 def massiveInsert(self, mblogs): | |
500 """Insert several microblogs at once | |
501 @param mblogs: dictionary of microblogs, as the result of getMassiveLastGroupBlogs | |
502 """ | |
503 count = sum([len(value) for value in mblogs.values()]) | |
504 log.debug("Massive insertion of %d microblogs" % count) | |
505 for publisher in mblogs: | |
506 log.debug("adding blogs for [%s]" % publisher) | |
507 for mblog in mblogs[publisher]: | |
508 if not "content" in mblog: | |
509 log.warning("No content found in microblog [%s]" % mblog) | |
510 continue | |
511 self.addEntry(mblog) | |
512 | |
513 def mblogsInsert(self, mblogs): | |
514 """ Insert several microblogs at once | |
515 @param mblogs: list of microblogs | |
516 """ | |
517 for mblog in mblogs: | |
518 if not "content" in mblog: | |
519 log.warning("No content found in microblog [%s]" % mblog) | |
520 continue | |
521 self.addEntry(mblog) | |
522 | |
523 def _chronoInsert(self, vpanel, entry, reverse=True): | |
524 """ Insert an entry in chronological order | |
525 @param vpanel: VerticalPanel instance | |
526 @param entry: MicroblogEntry | |
527 @param reverse: more recent entry on top if True, chronological order else""" | |
528 assert(isinstance(reverse, bool)) | |
529 if entry.empty: | |
530 entry.published = time() | |
531 # we look for the right index to insert our entry: | |
532 # if reversed, we insert the entry above the first entry | |
533 # in the past | |
534 idx = 0 | |
535 | |
536 for child in vpanel.children: | |
537 if not isinstance(child, MicroblogEntry): | |
538 idx += 1 | |
539 continue | |
540 condition_to_stop = child.empty or (child.published > entry.published) | |
541 if condition_to_stop != reverse: # != is XOR | |
542 break | |
543 idx += 1 | |
544 | |
545 vpanel.insert(entry, idx) | |
546 | |
547 def addEntry(self, data): | |
548 """Add an entry to the panel | |
549 @param data: dict containing the item data | |
550 @return: the added entry, or None | |
551 """ | |
552 _entry = MicroblogEntry(self, data) | |
553 if _entry.type == "comment": | |
554 comments_hash = (_entry.service, _entry.node) | |
555 if not comments_hash in self.comments: | |
556 # The comments node is not known in this panel | |
557 return None | |
558 parent = self.comments[comments_hash] | |
559 parent_idx = self.vpanel.getWidgetIndex(parent) | |
560 # we find or create the panel where the comment must be inserted | |
561 try: | |
562 sub_panel = self.vpanel.getWidget(parent_idx + 1) | |
563 except IndexError: | |
564 sub_panel = None | |
565 if not sub_panel or not isinstance(sub_panel, VerticalPanel): | |
566 sub_panel = VerticalPanel() | |
567 sub_panel.setStyleName('microblogPanel') | |
568 sub_panel.addStyleName('subPanel') | |
569 self.vpanel.insert(sub_panel, parent_idx + 1) | |
570 for idx in xrange(0, len(sub_panel.getChildren())): | |
571 comment = sub_panel.getIndexedChild(idx) | |
572 if comment.id == _entry.id: | |
573 # update an existing comment | |
574 sub_panel.remove(comment) | |
575 sub_panel.insert(_entry, idx) | |
576 return _entry | |
577 # we want comments to be inserted in chronological order | |
578 self._chronoInsert(sub_panel, _entry, reverse=False) | |
579 return _entry | |
580 | |
581 if _entry.id in self.entries: # update | |
582 idx = self.vpanel.getWidgetIndex(self.entries[_entry.id]) | |
583 self.vpanel.remove(self.entries[_entry.id]) | |
584 self.vpanel.insert(_entry, idx) | |
585 else: # new entry | |
586 self._chronoInsert(self.vpanel, _entry) | |
587 self.entries[_entry.id] = _entry | |
588 | |
589 if _entry.comments: | |
590 # entry has comments, we keep the comments service/node as a reference | |
591 comments_hash = (_entry.comments_service, _entry.comments_node) | |
592 self.comments[comments_hash] = _entry | |
593 self.host.bridge.call('getMblogComments', self.mblogsInsert, _entry.comments_service, _entry.comments_node) | |
594 | |
595 return _entry | |
596 | |
597 def removeEntry(self, type_, id_): | |
598 """Remove an entry from the panel | |
599 @param type_: entry type ('main_item' or 'comment') | |
600 @param id_: entry id | |
601 """ | |
602 for child in self.vpanel.getChildren(): | |
603 if isinstance(child, MicroblogEntry) and type_ == 'main_item': | |
604 if child.id == id_: | |
605 main_idx = self.vpanel.getWidgetIndex(child) | |
606 try: | |
607 sub_panel = self.vpanel.getWidget(main_idx + 1) | |
608 if isinstance(sub_panel, VerticalPanel): | |
609 sub_panel.removeFromParent() | |
610 except IndexError: | |
611 pass | |
612 child.removeFromParent() | |
613 self.selected_entry = None | |
614 break | |
615 elif isinstance(child, VerticalPanel) and type_ == 'comment': | |
616 for comment in child.getChildren(): | |
617 if comment.id == id_: | |
618 comment.removeFromParent() | |
619 self.selected_entry = None | |
620 break | |
621 | |
622 def ensureVisible(self, entry): | |
623 """Scroll to an entry to ensure its visibility | |
624 | |
625 @param entry (MicroblogEntry): the entry | |
626 """ | |
627 try: | |
628 self.vpanel.getParent().ensureVisible(entry) # scroll to the clicked entry | |
629 except AttributeError: | |
630 log.warning("FIXME: MicroblogPanel.vpanel should be wrapped in a ScrollPanel!") | |
631 | |
632 def setSelectedEntry(self, entry, ensure_visible=False): | |
633 """Select an entry. | |
634 | |
635 @param entry (MicroblogEntry): the entry to select | |
636 @param ensure_visible (boolean): if True, also scroll to the entry | |
637 """ | |
638 if ensure_visible: | |
639 self.ensureVisible(entry) | |
640 | |
641 if not self.host.uni_box or not entry.comments: | |
642 entry.addStyleName('selected_entry') # blink the clicked entry | |
643 clicked_entry = entry # entry may be None when the timer is done | |
644 Timer(500, lambda timer: clicked_entry.removeStyleName('selected_entry')) | |
645 if not self.host.uni_box: | |
646 return # unibox is disabled | |
647 | |
648 # from here the previous behavior (toggle main item selection) is conserved | |
649 entry = entry if entry.comments else None | |
650 if self.selected_entry == entry: | |
651 entry = None | |
652 if self.selected_entry: | |
653 self.selected_entry.removeStyleName('selected_entry') | |
654 if entry: | |
655 log.debug("microblog entry selected (author=%s)" % entry.author) | |
656 entry.addStyleName('selected_entry') | |
657 self.selected_entry = entry | |
658 | |
659 def updateValue(self, type_, jid, value): | |
660 """Update a jid value in entries | |
661 @param type_: one of 'avatar', 'nick' | |
662 @param jid: jid concerned | |
663 @param value: new value""" | |
664 def updateVPanel(vpanel): | |
665 for child in vpanel.children: | |
666 if isinstance(child, MicroblogEntry) and child.author == jid: | |
667 child.updateAvatar(value) | |
668 elif isinstance(child, VerticalPanel): | |
669 updateVPanel(child) | |
670 if type_ == 'avatar': | |
671 updateVPanel(self.vpanel) | |
672 | |
673 def setAcceptedGroup(self, group): | |
674 """Add one or more group(s) which can be displayed in this panel. | |
675 | |
676 Prevent from duplicate values and keep the list sorted. | |
677 @param group: string of the group, or list of string | |
678 """ | |
679 if isinstance(group, basestring): | |
680 groups = [group] | |
681 else: | |
682 groups = list(group) | |
683 try: | |
684 self._accepted_groups.extend(groups) | |
685 except (AttributeError, TypeError): # XXX: should be AttributeError, but pyjamas bugs here | |
686 self._accepted_groups = groups | |
687 self._accepted_groups.sort() | |
688 | |
689 def isJidAccepted(self, jid_s): | |
690 """Tell if a jid is actepted and shown in this panel | |
691 @param jid_s: jid | |
692 @return: True if the jid is accepted""" | |
693 if self.accept_all(): | |
694 return True | |
695 for group in self._accepted_groups: | |
696 if self.host.contact_panel.isContactInGroup(group, jid_s): | |
697 return True | |
698 return False |