Mercurial > libervia-web
changeset 282:ae3ec654836d
browser_side: added blog item modification/deletion
author | souliane <souliane@mailoo.org> |
---|---|
date | Tue, 10 Dec 2013 09:07:03 +0100 |
parents | 36ce989c73a5 |
children | 0eba1c4f9c6f |
files | browser_side/panels.py browser_side/richtext.py libervia.py libervia.tac public/libervia.css |
diffstat | 5 files changed, 240 insertions(+), 24 deletions(-) [+] |
line wrap: on
line diff
--- a/browser_side/panels.py Mon Dec 09 15:34:03 2013 +0100 +++ b/browser_side/panels.py Tue Dec 10 09:07:03 2013 +0100 @@ -34,10 +34,9 @@ from pyjamas.ui.PopupPanel import PopupPanel from pyjamas.ui.StackPanel import StackPanel from pyjamas.ui.ClickListener import ClickHandler -from pyjamas.ui.KeyboardListener import KEY_ENTER, KEY_UP, KEY_DOWN +from pyjamas.ui.KeyboardListener import KEY_ENTER, KEY_UP, KEY_DOWN, KeyboardHandler from pyjamas.ui.Event import BUTTON_LEFT, BUTTON_MIDDLE, BUTTON_RIGHT from pyjamas.ui.MouseListener import MouseHandler -from pyjamas.ui.ListBox import ListBox from pyjamas.Timer import Timer from pyjamas import DOM from card_game import CardPanel @@ -49,13 +48,15 @@ from time import time import dialog import base_widget -from richtext import RichTextEditor +from dialog import ConfirmDialog +import richtext from plugin_xep_0085 import ChatStateMachine from pyjamas import Window from __pyjamas__ import doc from sat_frontends.tools.games import SYMBOLS from sat_frontends import constants -from pyjamas.ui.ContextMenuPopupPanel import ContextMenuPopupPanel +from pyjamas.ui.FocusListener import FocusHandler +import logging const = constants.Const # to directly import 'const' doesn't work @@ -95,7 +96,7 @@ self.unibox.setVisible(True) self.host.resize() - RichTextEditor.getOrCreate(self.host, self, onCloseCallback) + richtext.RichTextEditor.getOrCreate(self.host, self, onCloseCallback) self.host.resize() @@ -318,35 +319,64 @@ print "Warning: can't manage comment [%s], some keys are missing in microblog data (%s)" % (data["comments"], data.keys()) self.comments = False if set(("service", "node")).issubset(data.keys()): + # comment item self.service = data["service"] self.node = data["node"] - self.hash = (self.service, self.node) + else: + # main item + try: + self.service = data['comments_service'] + self.node = data['comments_node'] + except KeyError: + logging.error("Main item %s is missing its comments information!" % self.id) + self.hash = (self.service, self.node) -class MicroblogEntry(SimplePanel, ClickHandler): +class MicroblogEntry(SimplePanel, ClickHandler, FocusHandler, KeyboardHandler): def __init__(self, blog_panel, mblog_entry): SimplePanel.__init__(self) self._blog_panel = blog_panel + self.entry = mblog_entry self.author = mblog_entry.author self.timestamp = mblog_entry.timestamp _datetime = datetime.fromtimestamp(mblog_entry.timestamp) self.comments = mblog_entry.comments + self.pub_data = (mblog_entry.hash[0], mblog_entry.hash[1], mblog_entry.id) + self.editable_content = (mblog_entry.xhtml, const._SYNTAX_XHTML) if mblog_entry.xhtml else (mblog_entry.content, None) self.panel = HTMLPanel(""" <div class='mb_entry_header'><span class='mb_entry_author'>%(author)s</span> on <span class='mb_entry_timestamp'>%(timestamp)s</span></div> + <div class='mb_entry_delete_update'> + <div id="id_delete"></div> + <div id="id_update"></div> + </div> <div class="mb_entry_avatar" id='id_avatar'></div> - <div class="mb_entry_dialog"> - <div class="bubble">%(body)s</div> + <div class="mb_entry_dialog" id='id_entry_dialog'></div> </div> """ % {"author": html_sanitize(self.author), - "timestamp": _datetime, - "body": addURLToText(html_sanitize(mblog_entry.content)) if not mblog_entry.xhtml else mblog_entry.xhtml + "timestamp": _datetime }) self.avatar = Image(blog_panel.host.getAvatar(self.author)) self.panel.add(self.avatar, "id_avatar") + body = addURLToText(html_sanitize(mblog_entry.content)) if not mblog_entry.xhtml else mblog_entry.xhtml + self.bubble = HTML(body) + self.bubble.setStyleName("bubble") + self.panel.add(self.bubble, "id_entry_dialog") self.panel.setStyleName('mb_entry') + if self.author == blog_panel.host.whoami.bare: + self.delete_label = Label(u"✗") + self.delete_label.setTitle("Delete this message") + self.panel.add(self.delete_label, "id_delete") + self.update_label = Label(u"✍") + self.update_label.setTitle("Edit this message") + self.panel.add(self.update_label, "id_update") + self.delete_label.addClickListener(self) + self.update_label.addClickListener(self) + else: + self.modify_label = self.delete_label = None + self.editbox = None self.add(self.panel) ClickHandler.__init__(self) self.addClickListener(self) @@ -357,8 +387,87 @@ self.avatar.setUrl(new_avatar) def onClick(self, sender): - print "microblog entry selected (author=%s)" % self.author - self._blog_panel.setSelectedEntry(self if self.comments else None) + if sender == self: + self._blog_panel.setSelectedEntry(self if self.comments else None) + elif sender == self.update_label: + self._update() + elif sender == self.delete_label: + self._delete() + + def onKeyUp(self, sender, keycode, modifiers): + """Update is done when ENTER key is pressed within the raw editbox""" + if sender != self.editbox or not self.editbox.getVisible(): + return + if keycode == KEY_ENTER: + self.updateContent() + + def onLostFocus(self, sender): + """Update is done when the focus leaves the raw editbox""" + if sender != self.editbox or not self.editbox.getVisible(): + return + self.updateContent() + + def updateContent(self, cancel=False): + """Send the new content to the backend""" + if not self.editbox or not self.editbox.getVisible(): + return + self.panel.remove(self.edit_panel) + self.panel.add(self.bubble, "id_entry_dialog") + new_text = self.editbox.getText().strip() + self.edit_panel = self.editbox = None + if cancel or new_text == self.editable_content[0] or new_text == "": + return + self.editable_content[0] = new_text + self._blog_panel.host.bridge.call('updateMblog', None, self.pub_data, self.comments, new_text, + {'rich': new_text} if self.entry.xhtml else {}) + + def _update(self): + """Change the bubble to an editbox""" + if self.editbox and self.editbox.getVisible(): + return + + def setOriginalText(text, container): + text = text.strip() + container.original_text = text + self.editbox.setWidth('100%') + self.editbox.setText(text) + panel = SimplePanel() + panel.add(container) + panel.setStyleName("bubble") + panel.addStyleName('bubble-editbox') + self.bubble.removeFromParent() + self.panel.add(panel, "id_entry_dialog") + self.editbox.setFocus(True) + self.editbox.setSelectionRange(len(text), 0) + self.edit_panel = panel + self.editable_content = (text, container.format if isinstance(container, richtext.RichTextEditor) else None) + + if self.entry.xhtml: + options = ('no_recipient', 'no_sync_unibox', 'no_style', 'update_msg', 'no_close') + + def cb(result): + self.updateContent(result == richtext.CANCEL) + + editor = richtext.RichTextEditor(self._blog_panel.host, self.panel, cb, options=options) + editor.setVisible(True) # needed to build the toolbar + self.editbox = editor.textarea + self._blog_panel.host.bridge.call('syntaxConvert', lambda d: setOriginalText(d, editor), + self.editable_content[0], self.editable_content[1]) + else: + self.editbox = TextArea() + self.editbox.addFocusListener(self) + self.editbox.addKeyboardListener(self) + setOriginalText(self.editable_content[0], self.editbox) + + def _delete(self): + """Ask confirmation for deletion""" + def confirm_cb(answer): + if answer: + self._blog_panel.host.bridge.call('deleteMblog', None, self.pub_data, self.comments) + + target = 'message and all its comments' if self.comments else 'comment' + _dialog = ConfirmDialog(confirm_cb, text="Do you really want to delete this %s?" % target) + _dialog.show() class MicroblogPanel(base_widget.LiberviaWidget): @@ -527,30 +636,65 @@ sub_panel.setStyleName('microblogPanel') sub_panel.addStyleName('subPanel') self.vpanel.insert(sub_panel, parent_idx + 1) - + for idx in xrange(0, len(sub_panel.getChildren())): + comment = sub_panel.getIndexedChild(idx) + if comment.pub_data[2] == mblog_item.id: + # update an existing comment + sub_panel.remove(comment) + sub_panel.insert(_entry, idx) + return # we want comments to be inserted in chronological order self._chronoInsert(sub_panel, _entry, reverse=False) return - if mblog_item.id in self.entries: - return + update = mblog_item.id in self.entries _entry = MicroblogEntry(self, mblog_item) - + if update: + idx = self.vpanel.getWidgetIndex(self.entries[mblog_item.id]) + self.vpanel.remove(self.entries[mblog_item.id]) + self.vpanel.insert(_entry, idx) + else: + self._chronoInsert(self.vpanel, _entry) self.entries[mblog_item.id] = _entry - self._chronoInsert(self.vpanel, _entry) - if mblog_item.comments: # entry has comments, we keep the comment node as a reference self.comments[mblog_item.comments_hash] = _entry self.host.bridge.call('getMblogComments', self.mblogsInsert, mblog_item.comments_service, mblog_item.comments_node) + def removeEntry(self, type_, id_): + """Remove an entry from the panel + @param type_: entry type ('main_item' or 'comment') + @param id_: entry id + """ + for child in self.vpanel.getChildren(): + if isinstance(child, MicroblogEntry) and type_ == 'main_item': + print child.pub_data + if child.pub_data[2] == id_: + main_idx = self.vpanel.getWidgetIndex(child) + try: + sub_panel = self.vpanel.getWidget(main_idx + 1) + if isinstance(sub_panel, VerticalPanel): + sub_panel.removeFromParent() + except IndexError: + pass + child.removeFromParent() + self.selected_entry = None + break + elif isinstance(child, VerticalPanel) and type_ == 'comment': + for comment in child.getChildren(): + if comment.pub_data[2] == id_: + comment.removeFromParent() + self.selected_entry = None + break + def setSelectedEntry(self, entry): if self.selected_entry == entry: entry = None if self.selected_entry: self.selected_entry.removeStyleName('selected_entry') if entry: + print "microblog entry selected (author=%s)" % entry.author entry.addStyleName('selected_entry') self.selected_entry = entry
--- a/browser_side/richtext.py Mon Dec 09 15:34:03 2013 +0100 +++ b/browser_side/richtext.py Tue Dec 10 09:07:03 2013 +0100 @@ -174,6 +174,11 @@ self.setWidget(offset1 + count, 0, toolbar) count += 1 + @property + def format(self): + """Get the current text format""" + return self._format if hasattr(self, '_format') else None + def addToolbarButton(self, toolbar, _format, key): """Add a button with the defined parameters.""" button = Button('<img src="%s" class="richTextIcon" />' %
--- a/libervia.py Mon Dec 09 15:34:03 2013 +0100 +++ b/libervia.py Tue Dec 10 09:07:03 2013 +0100 @@ -119,7 +119,8 @@ "tarotGameContratChoosed", "tarotGamePlayCards", "launchRadioCollective", "getWaitingSub", "subscription", "delContact", "updateContact", "getCard", "getEntityData", "getParamsUI", "asyncGetParamA", "setParam", "launchAction", - "disconnect", "chatStateComposing", "getNewAccountDomain", "confirmationAnswer" + "disconnect", "chatStateComposing", "getNewAccountDomain", "confirmationAnswer", + "syntaxConvert" ]) @@ -458,6 +459,7 @@ if not self.initialised: self.init_cache.append((sender, event_type, data)) return + sender = JID(sender).bare if event_type == "MICROBLOG": if not 'content' in data: print ("WARNING: No content found in microblog data") @@ -473,9 +475,30 @@ self.addBlogEntry(lib_wid, sender, _groups, mblog_entry) if sender == self.whoami.bare: - self.mblog_cache.append((_groups, mblog_entry)) - if len(self.mblog_cache) > MAX_MBLOG_CACHE: - del self.mblog_cache[0:len(self.mblog_cache - MAX_MBLOG_CACHE)] + found = False + for index in xrange(0, len(self.mblog_cache)): + entry = self.mblog_cache[index] + if entry[1].id == mblog_entry.id: + # replace existing entry + self.mblog_cache.remove(entry) + self.mblog_cache.insert(index, (_groups, mblog_entry)) + found = True + break + if not found: + self.mblog_cache.append((_groups, mblog_entry)) + if len(self.mblog_cache) > MAX_MBLOG_CACHE: + del self.mblog_cache[0:len(self.mblog_cache - MAX_MBLOG_CACHE)] + elif event_type == 'MICROBLOG_DELETE': + for lib_wid in self.libervia_widgets: + if isinstance(lib_wid, panels.MicroblogPanel): + lib_wid.removeEntry(data['type'], data['id']) + print self.whoami.bare, sender, data['type'] + if sender == self.whoami.bare and data['type'] == 'main_item': + for index in xrange(0, len(self.mblog_cache)): + entry = self.mblog_cache[index] + if entry[1].id == data['id']: + self.mblog_cache.remove(entry) + break def addBlogEntry(self, mblog_panel, sender, _groups, mblog_entry): """Check if an entry can go in MicroblogPanel and add to it
--- a/libervia.tac Mon Dec 09 15:34:03 2013 +0100 +++ b/libervia.tac Tue Dec 10 09:07:03 2013 +0100 @@ -41,7 +41,7 @@ from server_side.blog import MicroBlog from zope.interface import Interface, Attribute, implements from xml.dom import minidom - +from sat_frontends.constants import Const #import time @@ -251,6 +251,28 @@ else: raise Exception("Invalid data") + def jsonrpc_deleteMblog(self, pub_data, comments): + """Delete a microblog node + @param pub_data: a tuple (service, comment node identifier, item identifier) + @param comments: comments node identifier (for main item) or False + """ + profile = ISATSession(self.session).profile + return self.sat_host.bridge.deleteGroupBlog(pub_data, comments if comments else '', profile) + + def jsonrpc_updateMblog(self, pub_data, comments, message, extra={}): + """Modify a microblog node + @param pub_data: a tuple (service, comment node identifier, item identifier) + @param comments: comments node identifier (for main item) or False + @param message: new message + @param extra: dict which option name as key, which can be: + - allow_comments: True to accept an other level of comments, False else (default: False) + - rich: if present, contain rich text in currently selected syntax + """ + profile = ISATSession(self.session).profile + if comments: + extra['allow_comments'] = 'True' + return self.sat_host.bridge.updateGroupBlog(pub_data, comments if comments else '', message, extra, profile) + def jsonrpc_sendMblogComment(self, node, text, extra={}): """ Send microblog message @param node: url of the comments node @@ -482,6 +504,16 @@ profile = ISATSession(self.session).profile self.sat_host.bridge.confirmationAnswer(confirmation_id, result, answer_data, profile) + def jsonrpc_syntaxConvert(self, text, syntax_from=Const._SYNTAX_XHTML, syntax_to=Const._SYNTAX_CURRENT): + """ Convert a text between two syntaxes + @param text: text to convert + @param syntax_from: source syntax (e.g. "markdown") + @param syntax_to: dest syntax (e.g.: "XHTML") + @param safe: clean resulting XHTML to avoid malicious code if True (forced here) + @return: converted text """ + profile = ISATSession(self.session).profile + return self.sat_host.bridge.syntaxConvert(text, syntax_from, syntax_to, True, profile) + class Register(JSONRPCMethodManager): """This class manage the registration procedure with SàT
--- a/public/libervia.css Mon Dec 09 15:34:03 2013 +0100 +++ b/public/libervia.css Tue Dec 10 09:07:03 2013 +0100 @@ -786,6 +786,7 @@ .mb_entry_author { font-weight: bold; + padding-left: 5px; } .mb_entry_avatar { @@ -830,10 +831,21 @@ width: 20; } +.bubble-editbox { + width: auto; +} + .mb_entry_timestamp { font-style: italic; } +.mb_entry_delete_update { + float: right; + padding: 0px 5px; + cursor: pointer; + font-size: larger; +} + /* Chat & MUC Room */ .chatPanel {