changeset 608:ea27925ef2a8 frontends_multi_profiles

merges souliane changes
author Goffi <goffi@goffi.org>
date Mon, 09 Feb 2015 21:58:49 +0100
parents 537649f6a2d0 (current diff) c22b47d63fe2 (diff)
children ec77c2bc18d3
files src/browser/sat_browser/base_widget.py src/browser/sat_browser/contact_list.py src/server/server.py
diffstat 7 files changed, 202 insertions(+), 151 deletions(-) [+]
line wrap: on
line diff
--- a/setup.py	Mon Feb 09 21:56:30 2015 +0100
+++ b/setup.py	Mon Feb 09 21:58:49 2015 +0100
@@ -119,7 +119,10 @@
         os.symlink(os.path.dirname(sat.__file__), os.path.join(tmp_dir,"sat")) # FIXME: only work on unixes
         os.symlink(os.path.dirname(sat_frontends.__file__), os.path.join(tmp_dir,"sat_frontends")) # FIXME: only work on unixes
         os.symlink(os.path.dirname(libervia.__file__), os.path.join(tmp_dir,"libervia")) # FIXME: only work on unixes
-        result = subprocess.call(['pyjsbuild', 'libervia_main', '-d', '--no-compile-inplace', '-I', tmp_dir, '-o', self.pyjamas_output_dir])
+        for module in ('libervia_main', 'libervia_test'):
+            result = subprocess.call(['pyjsbuild', module, '-d', '--no-compile-inplace', '-I', tmp_dir, '-o', self.pyjamas_output_dir])
+            if result != 0:
+                continue
         shutil.rmtree(tmp_dir)
         os.chdir(cwd)
         return result
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/libervia_test.py	Mon Feb 09 21:58:49 2015 +0100
@@ -0,0 +1,78 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Libervia: a Salut à Toi frontend
+# Copyright (C) 2011, 2012, 2013, 2014 Jérôme Poisson <goffi@goffi.org>
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+
+# Just visit <root_url>/test. If you don't get any AssertError pop-up,
+# everything is fine. #TODO: nicely display the results in HTML output.
+
+
+### logging configuration ###
+from sat_browser import logging
+logging.configure()
+from sat.core.log import getLogger
+log = getLogger(__name__)
+###
+
+from sat_frontends.tools import jid
+from sat_browser import contact_list
+
+
+def test_JID():
+    """Check that the JID class reproduces the Twisted behavior"""
+    j1 = jid.JID("t1@test.org")
+    j1b = jid.JID("t1@test.org")
+    t1 = "t1@test.org"
+
+    assert j1 == j1b
+    assert j1 != t1
+    assert t1 != j1
+    assert hash(j1) == hash(j1b)
+    assert hash(j1) != hash(t1)
+
+
+def test_JIDIterable():
+    """Check that our iterables reproduce the Twisted behavior"""
+
+    j1 = jid.JID("t1@test.org")
+    j1b = jid.JID("t1@test.org")
+    j2 = jid.JID("t2@test.org")
+    t1 = "t1@test.org"
+    t2 = "t2@test.org"
+    jid_set = set([j1, t2])
+    jid_list = contact_list.JIDList([j1, t2])
+    jid_dict = {j1: "dummy 1", t2: "dummy 2"}
+    for iterable in (jid_set, jid_list, jid_dict):
+        log.info("Testing %s" % type(iterable))
+        assert j1 in iterable
+        assert j1b in iterable
+        assert j2 not in iterable
+        assert t1 not in iterable
+        assert t2 in iterable
+
+    # Check that the extra JIDList class is still needed
+    log.info("Testing Pyjamas native list")
+    jid_native_list = ([j1, t2])
+    assert j1 in jid_native_list
+    assert j1b not in jid_native_list  # this is NOT Twisted's behavior
+    assert j2 in jid_native_list  # this is NOT Twisted's behavior
+    assert t1 in jid_native_list  # this is NOT Twisted's behavior
+    assert t2 in jid_native_list
+
+test_JID()
+test_JIDIterable()
--- a/src/browser/public/libervia.css	Mon Feb 09 21:56:30 2015 +0100
+++ b/src/browser/public/libervia.css	Mon Feb 09 21:58:49 2015 +0100
@@ -1389,12 +1389,6 @@
     vertical-align: baseline;
 }
 
-.itemPanel-dragover {
-    border-radius: 5px;
-    background: none repeat scroll 0% 0% rgb(135, 179, 255);
-    border: 1px dashed rgb(35,79,255);
-}
-
 .recipientSpacer {
     height: 15px;
 }
@@ -1432,6 +1426,12 @@
     vertical-align:middle;
 }
 
+.contactGroupPanel.dragover {
+    border-radius: 5px !important;
+    background: none repeat scroll 0% 0% rgb(135, 179, 255) !important;
+    border: 1px dashed rgb(35,79,255) !important;
+}
+
 .toggleAssignedContacts {
     white-space: nowrap;
 }
--- a/src/browser/sat_browser/base_widget.py	Mon Feb 09 21:56:30 2015 +0100
+++ b/src/browser/sat_browser/base_widget.py	Mon Feb 09 21:58:49 2015 +0100
@@ -46,6 +46,10 @@
 import base_menu
 
 
+class NoLiberviaWidgetException(Exception):
+    pass
+
+
 class DragLabel(DragWidget):
 
     def __init__(self, text, type_, host=None):
@@ -106,14 +110,14 @@
 
     @classmethod
     def addDropKey(cls, key, cb):
-        """Add a association between a key and a class to create on drop
+        """Add a association between a key and a class to create on drop.
 
         @param key: key to be associated (e.g. "CONTACT", "CHAT")
-        @param cb: either a LiberviaWidget instance, or a callback which return one
+        @param cb: a callable (either a class or method) returning a
+            LiberviaWidget instance
         """
         DropCell.drop_keys[key] = cb
 
-
     def onDragEnter(self, event):
         if self == LiberviaDragWidget.current:
             return
@@ -138,6 +142,10 @@
         return (row.rowIndex, cell.cellIndex)
 
     def onDrop(self, event):
+        """
+        @raise NoLiberviaWidgetException: something else than a LiberviaWidget
+            has been returned by the callback.
+        """
         self.removeStyleName('dragover')
         DOM.eventPreventDefault(event)
         dt = event.dataTransfer
@@ -171,6 +179,8 @@
             widgets_panel.removeWidget(_new_panel)
         elif item_type in self.drop_keys:
             _new_panel = self.drop_keys[item_type](self.host, item)
+            if not isinstance(_new_panel, LiberviaWidget):
+                raise NoLiberviaWidgetException
         else:
             log.warning("unmanaged item type")
             return
--- a/src/browser/sat_browser/contact_list.py	Mon Feb 09 21:56:30 2015 +0100
+++ b/src/browser/sat_browser/contact_list.py	Mon Feb 09 21:58:49 2015 +0100
@@ -602,40 +602,20 @@
     #     self.updateVisibility(self._contacts_panel.contacts, self.groups.keys())
 
 
-def mayContainJID(iterable, item):
-    """Tells if the given item is in the iterable, works with JID.
-
-    @param iterable(object): list, set or another iterable object
-    @param item (object): element
-    @return: bool
-    """
-    # Pyjamas JID-friendly implementation of the "in" operator. Since our JID
-    # doesn't inherit from str, without this method the test would return True
-    # only when the objects references are the same.
-    if isinstance(item, jid.JID):
-        return hash(item) in [hash(other) for other in iterable if isinstance(other, jid.JID)]
-    return super(type(iterable), iterable).__contains__(self, item)
-
-
-class JIDSet(set):
-    """JID set implementation for Pyjamas"""
+class JIDList(list):
+    """JID-friendly list implementation for Pyjamas"""
 
     def __contains__(self, item):
-        return mayContainJID(self, item)
-
-
-class JIDList(list):
-    """JID list implementation for Pyjamas"""
-
-    def __contains__(self, item):
-        return mayContainJID(self, item)
+        """Tells if the list contains the given item.
 
-
-class JIDDict(dict):
-    """JID dict implementation for Pyjamas (a dict with JID keys)"""
-
-    def __contains__(self, item):
-        return mayContainJID(self, item)
-
-    def keys(self):
-        return JIDSet(dict.keys(self))
+        @param item (object): element to check
+        @return: bool
+        """
+        # Since our JID doesn't inherit from str/unicode, without this method
+        # the test would return True only when the objects references are the
+        # same. Tests have shown that the other iterable "set" and "dict" don't
+        # need this hack to reproduce the Twisted's behavior.
+        for other in self:
+            if other == item:
+                return True
+        return False
--- a/src/browser/sat_browser/list_manager.py	Mon Feb 09 21:56:30 2015 +0100
+++ b/src/browser/sat_browser/list_manager.py	Mon Feb 09 21:58:49 2015 +0100
@@ -20,15 +20,11 @@
 from sat.core.log import getLogger
 log = getLogger(__name__)
 from pyjamas.ui.Button import Button
-from pyjamas.ui.ListBox import ListBox
 from pyjamas.ui.FlowPanel import FlowPanel
 from pyjamas.ui.AutoComplete import AutoCompleteTextBox
 from pyjamas.ui.KeyboardListener import KEY_ENTER
-from pyjamas.ui.MouseListener import MouseHandler
-from pyjamas.ui.FocusListener import FocusHandler
-from pyjamas.ui.DropWidget import DropWidget
+from pyjamas.ui.DragWidget import DragWidget
 from pyjamas.Timer import Timer
-from pyjamas import DOM
 
 import base_panels
 import base_widget
@@ -80,7 +76,6 @@
         self.style = {"keyItem": "itemKey",
                       "popupMenuItem": "itemKey",
                       "buttonCell": "itemButtonCell",
-                      "dragoverPanel": "itemPanel-dragover",
                       "keyPanel": "itemPanel",
                       "textBox": "itemTextBox",
                       "textBox-invalid": "itemTextBox-invalid",
@@ -258,9 +253,8 @@
         self.popup_menu = base_panels.PopupMenuPanel(entries, hide, callback, style={"item": self.style["popupMenuItem"]})
 
 
-class DragAutoCompleteTextBox(AutoCompleteTextBox, base_widget.DragLabel, MouseHandler, FocusHandler):
+class DragAutoCompleteTextBox(AutoCompleteTextBox, DragWidget):
     """A draggable AutoCompleteTextBox which is used for representing an item."""
-    # XXX: this class is NOT generic because of the onDragEnd method which calls methods from ListPanel. It's probably not reusable for another scenario.
 
     def __init__(self, list_panel, event_cbs, style):
         """
@@ -270,21 +264,80 @@
         @param style (dict)
         """
         AutoCompleteTextBox.__init__(self)
-        base_widget.DragLabel.__init__(self, '', 'CONTACT_TEXTBOX')  # The group prefix "@" is already in text so we use only the "CONTACT_TEXTBOX" type
+        DragWidget.__init__(self)
         self.list_panel = list_panel
         self.event_cbs = event_cbs
         self.style = style
+        self.addStyleName(style["textBox"])
+        self.reset()
+
+        # Parent classes already init self as an handler for these events
         self.addMouseListener(self)
         self.addFocusListener(self)
         self.addChangeListener(self)
-        self.addStyleName(style["textBox"])
-        self.reset()
+
+    def onDragStart(self, event):
+        """The user starts dragging the text box."""
+        self.list_panel.manager.target_drop_cell = None
+        self.setSelectionRange(len(self.getText()), 0)
+
+        dt = event.dataTransfer
+        dt.setData('text/plain', "%s\n%s" % (self.getText(), "CONTACT_TEXTBOX"))
+        dt.setDragImage(self.getElement(), 15, 15)
+
+    def onDragEnd(self, event):
+        """The user dropped the text box."""
+        target = self.list_panel.manager.target_drop_cell  # parent or another ListPanel
+        if self.getText() == "" or target is None:
+            return
+        self.event_cbs["drop"](self, target)
+
+    def onClick(self, sender):
+        """The choices list is clicked"""
+        assert sender == self.choices
+        AutoCompleteTextBox.onClick(self, sender)
+        self.validate()
+
+    def onChange(self, sender):
+        """The list selection or the text has been changed"""
+        assert sender == self.choices or sender == self
+        if sender == self.choices:
+            AutoCompleteTextBox.onChange(self, sender)
+        self.validate()
+
+    def onKeyUp(self, sender, keycode, modifiers):
+        """Listen for key stroke"""
+        assert sender == self
+        AutoCompleteTextBox.onKeyUp(self, sender, keycode, modifiers)
+        if keycode == KEY_ENTER:
+            self.validate()
+
+    def onMouseMove(self, sender):
+        """Mouse enters the area of a DragAutoCompleteTextBox."""
+        assert sender == self
+        if hasattr(sender, "remove_btn"):
+            sender.remove_btn.setVisible(True)
+
+    def onMouseLeave(self, sender):
+        """Mouse leaves the area of a DragAutoCompleteTextBox."""
+        assert sender == self
+        if hasattr(sender, "remove_btn"):
+            Timer(1500, lambda timer: sender.remove_btn.setVisible(False))
+
+    def onFocus(self, sender):
+        """The DragAutoCompleteTextBox has the focus."""
+        assert sender == self
+        # FIXME: this raises runtime JS error "Permission denied to access property..." when you drag the object
+        #sender.setSelectionRange(0, len(sender.getText()))
+        sender.event_cbs["focus"](sender)
 
     def reset(self):
+        """Reset the text box"""
         self.setText("")
         self.setValid()
 
     def setValid(self, valid=True):
+        """Change the style according to the text validity."""
         if self.getText() == "":
             valid = True
         if valid:
@@ -293,19 +346,13 @@
             self.addStyleName(self.style["textBox-invalid"])
         self.valid = valid
 
-    def onDragStart(self, event):
-        self._text = self.getText()
-        base_widget.DragLabel.onDragStart(self, event)
-        self.list_panel.manager.target_drop_cell = None
+    def validate(self):
+        """Check if the text is valid, update the style."""
         self.setSelectionRange(len(self.getText()), 0)
-
-    def onDragEnd(self, event):
-        target = self.list_panel.manager.target_drop_cell  # parent or another ListPanel
-        if self.getText() == "" or target is None:
-            return
-        self.event_cbs["drop"](self, target)
+        self.event_cbs["validate"](self)
 
     def setRemoveButton(self):
+        """Add the remove button after the text box."""
 
         def remove_cb(sender):
             """Callback for the button to remove this item."""
@@ -318,98 +365,22 @@
         self.list_panel.add(self.remove_btn)
 
     def removeOrReset(self):
+        """Remove the text box if the remove button exists, or reset the text box."""
         if hasattr(self, "remove_btn"):
             self.remove_btn.click()
         else:
             self.reset()
 
-    def onMouseMove(self, sender):
-        """Mouse enters the area of a DragAutoCompleteTextBox."""
-        if hasattr(sender, "remove_btn"):
-            sender.remove_btn.setVisible(True)
-
-    def onMouseLeave(self, sender):
-        """Mouse leaves the area of a DragAutoCompleteTextBox."""
-        if hasattr(sender, "remove_btn"):
-            Timer(1500, lambda timer: sender.remove_btn.setVisible(False))
-
-    def onFocus(self, sender):
-        sender.setSelectionRange(0, len(self.getText()))
-        self.event_cbs["focus"](sender)
-
-    def validate(self):
-        self.setSelectionRange(len(self.getText()), 0)
-        self.event_cbs["validate"](self)
-
-    def onChange(self, sender):
-        """The textbox or list selection is changed"""
-        if isinstance(sender, ListBox):
-            AutoCompleteTextBox.onChange(self, sender)
-        self.validate()
-
-    def onClick(self, sender):
-        """The list is clicked"""
-        AutoCompleteTextBox.onClick(self, sender)
-        self.validate()
-
-    def onKeyUp(self, sender, keycode, modifiers):
-        """Listen for ENTER key stroke"""
-        AutoCompleteTextBox.onKeyUp(self, sender, keycode, modifiers)
-        if keycode == KEY_ENTER:
-            self.validate()
-
-
-class DropCell(DropWidget):
-    """A cell where you can drop widgets. This class is NOT generic because of
-    onDrop which uses methods from ListPanel. It has been created to
-    separate the drag and drop methods from the others and add a bit of
-    readability, but it's probably not reusable for another scenario.
-    """
-
-    def __init__(self, drop_cbs):
-        """
-
-        @param drop_cbs (list[callable])
-        """
-        DropWidget.__init__(self)
-        self.drop_cbs = drop_cbs
-
-    def onDragEnter(self, event):
-        self.addStyleName(self.style["dragoverPanel"])
-        DOM.eventPreventDefault(event)
-
-    def onDragLeave(self, event):
-        if event.clientX <= self.getAbsoluteLeft() or event.clientY <= self.getAbsoluteTop()\
-            or event.clientX >= self.getAbsoluteLeft() + self.getOffsetWidth() - 1\
-            or event.clientY >= self.getAbsoluteTop() + self.getOffsetHeight() - 1:
-            # We check that we are inside widget's box, and we don't remove the style in this case because
-            # if the mouse is over a widget inside the DropWidget, we don't want the style to be removed
-            self.removeStyleName(self.style["dragoverPanel"])
-
-    def onDragOver(self, event):
-        DOM.eventPreventDefault(event)
-
-    def onDrop(self, event):
-        DOM.eventPreventDefault(event)
-        dt = event.dataTransfer
-        # 'text', 'text/plain', and 'Text' are equivalent.
-        item, item_type = dt.getData("text/plain").split('\n')  # Workaround for webkit, only text/plain seems to be managed
-        if item_type and item_type[-1] == '\0':  # Workaround for what looks like a pyjamas bug: the \0 should not be there, and
-            item_type = item_type[:-1]           # .strip('\0') and .replace('\0','') don't work. TODO: check this and fill a bug report
-        if item_type in self.drop_cbs.keys():
-            self.drop_cbs[item_type](self, item)
-        self.removeStyleName(self.style["dragoverPanel"])
-
 
 VALID = 1
 INVALID = 2
 DELETE = 3
 
 
-class ListPanel(FlowPanel, DropCell):
+class ListPanel(FlowPanel, base_widget.DropCell):
     """Panel used for listing items sharing the same key. The key is showed as
     a Button to which you can bind a popup menu and the items are represented
-    with a sequence of DragAutoCompleteTextBoxeditable."""
+    with a sequence of DragAutoCompleteTextBox."""
     # XXX: beware that pyjamas.ui.FlowPanel is not fully implemented yet and can not be used with pyjamas.ui.Label
 
     def __init__(self, manager, data, style={}):
@@ -421,22 +392,29 @@
         """
         FlowPanel.__init__(self, Visible=(False if data["optional"] else True))
 
-        def setTargetDropCell(panel, item):
-            self.manager.target_drop_cell = panel
+        def setTargetDropCell(host, item):
+            self.manager.target_drop_cell = self
 
         # FIXME: dirty magic strings '@' and '@@'
-        drop_cbs = {"GROUP": lambda panel, item: self.addItem("@%s" % item),
-                    "CONTACT": lambda panel, item: self.addItem(tryJID(item)),
-                    "CONTACT_TITLE": lambda panel, item: self.addItem('@@'),
+        drop_cbs = {"GROUP": lambda host, item: self.addItem("@%s" % item),
+                    "CONTACT": lambda host, item: self.addItem(tryJID(item)),
+                    "CONTACT_TITLE": lambda host, item: self.addItem('@@'),
                     "CONTACT_TEXTBOX": setTargetDropCell
                     }
-        DropCell.__init__(self, drop_cbs)
+        base_widget.DropCell.__init__(self, None)
+        self.drop_keys = drop_cbs
         self.style = style
         self.addStyleName(self.style["keyPanel"])
         self.manager = manager
         self.key = data["title"]
         self._addTextBox()
 
+    def onDrop(self, event):
+        try:
+            base_widget.DropCell.onDrop(self, event)
+        except base_widget.NoLiberviaWidgetException:
+            pass
+
     def _addTextBox(self, switchPrevious=False):
         """Add an empty text box to the last position.
 
@@ -508,12 +486,13 @@
         return VALID if item in self.manager.items else INVALID
 
     def addItem(self, item, sender=None):
-        """
+        """Try to add an item. It will be added if it's a valid one.
 
         @param item (object): item to be added
         @param (DragAutoCompleteTextBox): widget triggering the event
         @param sender: if True, the item will be "written" to the last textbox
             and a new text box will be added afterward.
+        @return: True if the item has been added.
         """
         valid = self._checkItem(item, sender is not None)
         item_s = unicode(item)
--- a/src/server/server.py	Mon Feb 09 21:56:30 2015 +0100
+++ b/src/server/server.py	Mon Feb 09 21:58:49 2015 +0100
@@ -1108,6 +1108,7 @@
                 root.putChild(path, EncodingResourceWrapper(resource, [server.GzipEncoderFactory()]))
 
             putChild('', Redirect('libervia.html'))
+            putChild('test', Redirect('libervia_test.html'))
             putChild('json_signal_api', self.signal_handler)
             putChild('json_api', MethodHandler(self))
             putChild('register_api', _register)