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 {