# HG changeset patch # User Goffi # Date 1423515529 -3600 # Node ID ea27925ef2a8ab4e4e1e44af4fa79971d76c74a3 # Parent 537649f6a2d09260489eb8dfd90d8262de0da000# Parent c22b47d63fe29e02df60f1a7d21e0174e52b0fe9 merges souliane changes diff -r 537649f6a2d0 -r ea27925ef2a8 setup.py --- 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 diff -r 537649f6a2d0 -r ea27925ef2a8 src/browser/libervia_test.py --- /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 + +# 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 . + + +# Just visit /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() diff -r 537649f6a2d0 -r ea27925ef2a8 src/browser/public/libervia.css --- 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; } diff -r 537649f6a2d0 -r ea27925ef2a8 src/browser/sat_browser/base_widget.py --- 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 diff -r 537649f6a2d0 -r ea27925ef2a8 src/browser/sat_browser/contact_list.py --- 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 diff -r 537649f6a2d0 -r ea27925ef2a8 src/browser/sat_browser/list_manager.py --- 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) diff -r 537649f6a2d0 -r ea27925ef2a8 src/server/server.py --- 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)