Mercurial > libervia-web
changeset 449:981ed669d3b3
/!\ reorganize all the file hierarchy, move the code and launching script to src:
- browser_side --> src/browser
- public --> src/browser_side/public
- libervia.py --> src/browser/libervia_main.py
- libervia_server --> src/server
- libervia_server/libervia.sh --> src/libervia.sh
- twisted --> src/twisted
- new module src/common
- split constants.py in 3 files:
- src/common/constants.py
- src/browser/constants.py
- src/server/constants.py
- output --> html (generated by pyjsbuild during the installation)
- new option/parameter "data_dir" (-d) to indicates the directory containing html and server_css
- setup.py installs libervia to the following paths:
- src/common --> <LIB>/libervia/common
- src/server --> <LIB>/libervia/server
- src/twisted --> <LIB>/twisted
- html --> <SHARE>/libervia/html
- server_side --> <SHARE>libervia/server_side
- LIBERVIA_INSTALL environment variable takes 2 new options with prompt confirmation:
- clean: remove previous installation directories
- purge: remove building and previous installation directories
You may need to update your sat.conf and/or launching script to update the following options/parameters:
- ssl_certificate
- data_dir
author | souliane <souliane@mailoo.org> |
---|---|
date | Tue, 20 May 2014 06:41:16 +0200 |
parents | 14c35f7f1ef5 |
children | 41aae13cab2b |
files | browser_side/__init__.py browser_side/base_panels.py browser_side/base_widget.py browser_side/card_game.py browser_side/contact.py browser_side/contact_group.py browser_side/dialog.py browser_side/file_tools.py browser_side/html_tools.py browser_side/jid.py browser_side/list_manager.py browser_side/logging.py browser_side/menu.py browser_side/nativedom.py browser_side/notification.py browser_side/panels.py browser_side/plugin_xep_0085.py browser_side/radiocol.py browser_side/register.py browser_side/richtext.py browser_side/xmlui.py constants.py libervia.py libervia_server/__init__.py libervia_server/blog.py libervia_server/html_tools.py libervia_server/libervia.sh public/contrat_social.html public/libervia.css public/libervia.html public/sat_logo_16.png setup.py src/__init__.py src/browser/__init__.py src/browser/base_panels.py src/browser/base_widget.py src/browser/card_game.py src/browser/constants.py src/browser/contact.py src/browser/contact_group.py src/browser/dialog.py src/browser/file_tools.py src/browser/html_tools.py src/browser/jid.py src/browser/libervia_main.py src/browser/list_manager.py src/browser/logging.py src/browser/menu.py src/browser/nativedom.py src/browser/notification.py src/browser/panels.py src/browser/plugin_xep_0085.py src/browser/public/contrat_social.html src/browser/public/libervia.css src/browser/public/libervia.html src/browser/public/sat_logo_16.png src/browser/radiocol.py src/browser/register.py src/browser/richtext.py src/browser/xmlui.py src/common/__init__.py src/common/constants.py src/libervia.sh src/server/__init__.py src/server/blog.py src/server/constants.py src/server/html_tools.py src/server/server.py src/twisted/plugins/libervia_server.py tools/__init__.py twisted/plugins/libervia.py |
diffstat | 65 files changed, 11796 insertions(+), 11601 deletions(-) [+] |
line wrap: on
line diff
--- a/browser_side/base_panels.py Fri May 16 11:51:10 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,609 +0,0 @@ -#!/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/>. - -import pyjd # this is dummy in pyjs -from sat.core.log import getLogger -log = getLogger(__name__) -from pyjamas.ui.AbsolutePanel import AbsolutePanel -from pyjamas.ui.VerticalPanel import VerticalPanel -from pyjamas.ui.HorizontalPanel import HorizontalPanel -from pyjamas.ui.HTMLPanel import HTMLPanel -from pyjamas.ui.Button import Button -from pyjamas.ui.HTML import HTML -from pyjamas.ui.SimplePanel import SimplePanel -from pyjamas.ui.PopupPanel import PopupPanel -from pyjamas.ui.StackPanel import StackPanel -from pyjamas.ui.TextArea import TextArea -from pyjamas.ui.Event import BUTTON_LEFT, BUTTON_MIDDLE, BUTTON_RIGHT -from pyjamas.ui.KeyboardListener import KEY_ENTER, KEY_SHIFT, KeyboardHandler -from pyjamas.ui.FocusListener import FocusHandler -from pyjamas.ui.ClickListener import ClickHandler -from pyjamas import DOM - -from datetime import datetime -from time import time - -from html_tools import html_sanitize, html_strip, inlineRoot, convertNewLinesToXHTML - -from constants import Const as C -from sat_frontends.tools.strings import addURLToText, addURLToImage -from sat.core.i18n import _ - - -class ChatText(HTMLPanel): - - def __init__(self, timestamp, nick, mymess, msg, xhtml=None): - _date = datetime.fromtimestamp(float(timestamp or time())) - _msg_class = ["chat_text_msg"] - if mymess: - _msg_class.append("chat_text_mymess") - HTMLPanel.__init__(self, "<span class='chat_text_timestamp'>%(timestamp)s</span> <span class='chat_text_nick'>%(nick)s</span> <span class='%(msg_class)s'>%(msg)s</span>" % - {"timestamp": _date.strftime("%H:%M"), - "nick": "[%s]" % html_sanitize(nick), - "msg_class": ' '.join(_msg_class), - "msg": addURLToText(html_sanitize(msg)) if not xhtml else inlineRoot(xhtml)} # FIXME: images and external links must be removed according to preferences - ) - self.setStyleName('chatText') - - -class Occupant(HTML): - """Occupant of a MUC room""" - - def __init__(self, nick, state=None, special=""): - """ - @param nick: the user nickname - @param state: the user chate state (XEP-0085) - @param special: a string of symbols (e.g: for activities) - """ - HTML.__init__(self) - self.nick = nick - self._state = state - self.special = special - self._refresh() - - def __str__(self): - return self.nick - - def setState(self, state): - self._state = state - self._refresh() - - def addSpecial(self, special): - """@param special: unicode""" - if special not in self.special: - self.special += special - self._refresh() - - def removeSpecials(self, special): - """@param special: unicode or list""" - if not isinstance(special, list): - special = [special] - for symbol in special: - self.special = self.special.replace(symbol, "") - self._refresh() - - def _refresh(self): - state = (' %s' % C.MUC_USER_STATES[self._state]) if self._state else '' - special = "" if len(self.special) == 0 else " %s" % self.special - self.setHTML("<div class='occupant'>%s%s%s</div>" % (html_sanitize(self.nick), special, state)) - - -class OccupantsList(AbsolutePanel): - """Panel user to show occupants of a room""" - - def __init__(self): - AbsolutePanel.__init__(self) - self.occupants_list = {} - self.setStyleName('occupantsList') - - def addOccupant(self, nick): - _occupant = Occupant(nick) - self.occupants_list[nick] = _occupant - self.add(_occupant) - - def removeOccupant(self, nick): - try: - self.remove(self.occupants_list[nick]) - except KeyError: - log.error("trying to remove an unexisting nick") - - def clear(self): - self.occupants_list.clear() - AbsolutePanel.clear(self) - - def updateSpecials(self, occupants=[], html=""): - """Set the specified html "symbol" to the listed occupants, - and eventually remove it from the others (if they got it). - This is used for example to visualize who is playing a game. - @param occupants: list of the occupants that need the symbol - @param html: unicode symbol (actually one character or more) - or a list to assign different symbols of the same family. - """ - index = 0 - special = html - for occupant in self.occupants_list.keys(): - if occupant in occupants: - if isinstance(html, list): - special = html[index] - index = (index + 1) % len(html) - self.occupants_list[occupant].addSpecial(special) - else: - self.occupants_list[occupant].removeSpecials(html) - - -class PopupMenuPanel(PopupPanel): - """This implementation of a popup menu (context menu) allow you to assign - two special methods which are common to all the items, in order to hide - certain items and also easily define their callbacks. The menu can be - bound to any of the mouse button (left, middle, right). - """ - def __init__(self, entries, hide=None, callback=None, vertical=True, style=None, **kwargs): - """ - @param entries: a dict of dicts, where each sub-dict is representing - one menu item: the sub-dict key can be used as the item text and - description, but optional "title" and "desc" entries would be used - if they exists. The sub-dicts may be extended later to do - more complicated stuff or overwrite the common methods. - @param hide: function with 2 args: widget, key as string and - returns True if that item should be hidden from the context menu. - @param callback: function with 2 args: sender, key as string - @param vertical: True or False, to set the direction - @param item_style: alternative CSS class for the menu items - @param menu_style: supplementary CSS class for the sender widget - """ - PopupPanel.__init__(self, autoHide=True, **kwargs) - self._entries = entries - self._hide = hide - self._callback = callback - self.vertical = vertical - self.style = {"selected": None, "menu": "recipientTypeMenu", "item": "popupMenuItem"} - if isinstance(style, dict): - self.style.update(style) - self._senders = {} - - def _show(self, sender): - """Popup the menu relative to this sender's position. - @param sender: the widget that has been clicked - """ - menu = VerticalPanel() if self.vertical is True else HorizontalPanel() - menu.setStyleName(self.style["menu"]) - - def button_cb(item): - """You can not put that method in the loop and rely - on _key, because it is overwritten by each step. - You can rely on item.key instead, which is copied - from _key after the item creation. - @param item: the menu item that has been clicked - """ - if self._callback is not None: - self._callback(sender=sender, key=item.key) - self.hide(autoClosed=True) - - for _key in self._entries.keys(): - entry = self._entries[_key] - if self._hide is not None and self._hide(sender=sender, key=_key) is True: - continue - title = entry["title"] if "title" in entry.keys() else _key - item = Button(title, button_cb) - item.key = _key - item.setStyleName(self.style["item"]) - item.setTitle(entry["desc"] if "desc" in entry.keys() else title) - menu.add(item) - if len(menu.getChildren()) == 0: - return - self.add(menu) - if self.vertical is True: - x = sender.getAbsoluteLeft() + sender.getOffsetWidth() - y = sender.getAbsoluteTop() - else: - x = sender.getAbsoluteLeft() - y = sender.getAbsoluteTop() + sender.getOffsetHeight() - self.setPopupPosition(x, y) - self.show() - if self.style["selected"]: - sender.addStyleDependentName(self.style["selected"]) - - def _onHide(popup): - if self.style["selected"]: - sender.removeStyleDependentName(self.style["selected"]) - return PopupPanel.onHideImpl(self, popup) - - self.onHideImpl = _onHide - - def registerClickSender(self, sender, button=BUTTON_LEFT): - """Bind the menu to the specified sender. - @param sender: the widget to which the menu should be bound - @param: BUTTON_LEFT, BUTTON_MIDDLE or BUTTON_RIGHT - """ - self._senders.setdefault(sender, []) - self._senders[sender].append(button) - - if button == BUTTON_RIGHT: - # WARNING: to disable the context menu is a bit tricky... - # The following seems to work on Firefox 24.0, but: - # TODO: find a cleaner way to disable the context menu - sender.getElement().setAttribute("oncontextmenu", "return false") - - def _onBrowserEvent(event): - button = DOM.eventGetButton(event) - if DOM.eventGetType(event) == "mousedown" and button in self._senders[sender]: - self._show(sender) - return sender.__class__.onBrowserEvent(sender, event) - - sender.onBrowserEvent = _onBrowserEvent - - def registerMiddleClickSender(self, sender): - self.registerClickSender(sender, BUTTON_MIDDLE) - - def registerRightClickSender(self, sender): - self.registerClickSender(sender, BUTTON_RIGHT) - - -class ToggleStackPanel(StackPanel): - """This is a pyjamas.ui.StackPanel with modified behavior. All sub-panels ca be - visible at the same time, clicking a sub-panel header will not display it and hide - the others but only toggle its own visibility. The argument 'visibleStack' is ignored. - Note that the argument 'visible' has been added to listener's 'onStackChanged' method. - """ - - def __init__(self, **kwargs): - StackPanel.__init__(self, **kwargs) - - def onBrowserEvent(self, event): - if DOM.eventGetType(event) == "click": - index = self.getDividerIndex(DOM.eventGetTarget(event)) - if index != -1: - self.toggleStack(index) - - def add(self, widget, stackText="", asHTML=False, visible=False): - StackPanel.add(self, widget, stackText, asHTML) - self.setStackVisible(self.getWidgetCount() - 1, visible) - - def toggleStack(self, index): - if index >= self.getWidgetCount(): - return - visible = not self.getWidget(index).getVisible() - self.setStackVisible(index, visible) - for listener in self.stackListeners: - listener.onStackChanged(self, index, visible) - - -class TitlePanel(ToggleStackPanel): - """A toggle panel to set the message title""" - def __init__(self): - ToggleStackPanel.__init__(self, Width="100%") - self.text_area = TextArea() - self.add(self.text_area, _("Title")) - self.addStackChangeListener(self) - - def onStackChanged(self, sender, index, visible=None): - if visible is None: - visible = sender.getWidget(index).getVisible() - text = self.text_area.getText() - suffix = "" if (visible or not text) else (": %s" % text) - sender.setStackText(index, _("Title") + suffix) - - def getText(self): - return self.text_area.getText() - - def setText(self, text): - self.text_area.setText(text) - - -class BaseTextEditor(object): - """Basic definition of a text editor. The method edit gets a boolean parameter which - should be set to True when you want to edit the text and False to only display it.""" - - def __init__(self, content=None, strproc=None, modifiedCb=None, afterEditCb=None): - """ - Remark when inheriting this class: since the setContent method could be - overwritten by the child class, you should consider calling this __init__ - after all the parameters affecting this setContent method have been set. - @param content: dict with at least a 'text' key - @param strproc: method to be applied on strings to clean the content - @param modifiedCb: method to be called when the text has been modified. - If this method returns: - - True: the modification will be saved and afterEditCb called; - - False: the modification won't be saved and afterEditCb called; - - None: the modification won't be saved and afterEditCb not called. - @param afterEditCb: method to be called when the edition is done - """ - if content is None: - content = {'text': ''} - assert('text' in content) - if strproc is None: - def strproc(text): - try: - return text.strip() - except (TypeError, AttributeError): - return text - self.strproc = strproc - self.__modifiedCb = modifiedCb - self._afterEditCb = afterEditCb - self.initialized = False - self.edit_listeners = [] - self.setContent(content) - - def setContent(self, content=None): - """Set the editable content. The displayed content, which is set from the child class, could differ. - @param content: dict with at least a 'text' key - """ - if content is None: - content = {'text': ''} - elif not isinstance(content, dict): - content = {'text': content} - assert('text' in content) - self._original_content = {} - for key in content: - self._original_content[key] = self.strproc(content[key]) - - def getContent(self): - """Get the current edited or editable content. - @return: dict with at least a 'text' key - """ - raise NotImplementedError - - def setOriginalContent(self, content): - """Use this method with care! Content initialization should normally be - done with self.setContent. This method exists to let you trick the editor, - e.g. for self.modified to return True also when nothing has been modified. - @param content: dict - """ - self._original_content = content - - def getOriginalContent(self): - """ - @return the original content before modification (dict) - """ - return self._original_content - - def modified(self, content=None): - """Check if the content has been modified. - Remark: we don't use the direct comparison because we want to ignore empty elements - @content: content to be check against the original content or None to use the current content - @return: True if the content has been modified. - """ - if content is None: - content = self.getContent() - # the following method returns True if one non empty element exists in a but not in b - diff1 = lambda a, b: [a[key] for key in set(a.keys()).difference(b.keys()) if a[key]] != [] - # the following method returns True if the values for the common keys are not equals - diff2 = lambda a, b: [1 for key in set(a.keys()).intersection(b.keys()) if a[key] != b[key]] != [] - # finally the combination of both to return True if a difference is found - diff = lambda a, b: diff1(a, b) or diff1(b, a) or diff2(a, b) - - return diff(content, self._original_content) - - def edit(self, edit, abort=False, sync=False): - """ - Remark: the editor must be visible before you call this method. - @param edit: set to True to edit the content or False to only display it - @param abort: set to True to cancel the edition and loose the changes. - If edit and abort are both True, self.abortEdition can be used to ask for a - confirmation. When edit is False and abort is True, abortion is actually done. - @param sync: set to True to cancel the edition after the content has been saved somewhere else - """ - if edit: - if not self.initialized: - self.syncToEditor() # e.g.: use the selected target and unibox content - self.setFocus(True) - if abort: - content = self.getContent() - if not self.modified(content) or self.abortEdition(content): # e.g: ask for confirmation - self.edit(False, True, sync) - return - if sync: - self.syncFromEditor(content) # e.g.: save the content to unibox - return - else: - if not self.initialized: - return - content = self.getContent() - if abort: - self._afterEditCb(content) - return - if self.__modifiedCb and self.modified(content): - result = self.__modifiedCb(content) # e.g.: send a message or update something - if result is not None: - if self._afterEditCb: - self._afterEditCb(content) # e.g.: restore the display mode - if result is True: - self.setContent(content) - elif self._afterEditCb: - self._afterEditCb(content) - - self.initialized = True - - def setFocus(self, focus): - """ - @param focus: set to True to focus the editor - """ - raise NotImplementedError - - def syncToEditor(self): - pass - - def syncFromEditor(self, content): - pass - - def abortEdition(self, content): - return True - - def addEditListener(self, listener): - """Add a method to be called whenever the text is edited. - @param listener: method taking two arguments: sender, keycode""" - self.edit_listeners.append(listener) - - -class SimpleTextEditor(BaseTextEditor, FocusHandler, KeyboardHandler, ClickHandler): - """Base class for manage a simple text editor.""" - - def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None): - """ - @param content - @param modifiedCb - @param afterEditCb - @param options: dict with the following value: - - no_xhtml: set to True to clean any xhtml content. - - enhance_display: if True, the display text will be enhanced with addURLToText - - listen_keyboard: set to True to terminate the edition with <enter> or <escape>. - - listen_focus: set to True to terminate the edition when the focus is lost. - - listen_click: set to True to start the edition when you click on the widget. - """ - self.options = {'no_xhtml': False, - 'enhance_display': True, - 'listen_keyboard': True, - 'listen_focus': False, - 'listen_click': False - } - if options: - self.options.update(options) - self.__shift_down = False - if self.options['listen_focus']: - FocusHandler.__init__(self) - if self.options['listen_click']: - ClickHandler.__init__(self) - KeyboardHandler.__init__(self) - strproc = lambda text: html_sanitize(html_strip(text)) if self.options['no_xhtml'] else html_strip(text) - BaseTextEditor.__init__(self, content, strproc, modifiedCb, afterEditCb) - self.textarea = self.display = None - - def setContent(self, content=None): - BaseTextEditor.setContent(self, content) - - def getContent(self): - raise NotImplementedError - - def edit(self, edit, abort=False, sync=False): - BaseTextEditor.edit(self, edit) - if edit: - if self.options['listen_focus'] and self not in self.textarea._focusListeners: - self.textarea.addFocusListener(self) - if self.options['listen_click']: - self.display.clearClickListener() - if self not in self.textarea._keyboardListeners: - self.textarea.addKeyboardListener(self) - else: - self.setDisplayContent() - if self.options['listen_focus']: - try: - self.textarea.removeFocusListener(self) - except ValueError: - pass - if self.options['listen_click'] and self not in self.display._clickListeners: - self.display.addClickListener(self) - try: - self.textarea.removeKeyboardListener(self) - except ValueError: - pass - - def setDisplayContent(self): - text = self._original_content['text'] - if not self.options['no_xhtml']: - text = addURLToImage(text) - if self.options['enhance_display']: - text = addURLToText(text) - self.display.setHTML(convertNewLinesToXHTML(text)) - - def setFocus(self, focus): - raise NotImplementedError - - def onKeyDown(self, sender, keycode, modifiers): - for listener in self.edit_listeners: - listener(self.textarea, keycode) - if not self.options['listen_keyboard']: - return - if keycode == KEY_SHIFT or self.__shift_down: # allow input a new line with <shift> + <enter> - self.__shift_down = True - return - if keycode == KEY_ENTER: # finish the edition - self.textarea.setFocus(False) - if not self.options['listen_focus']: - self.edit(False) - - def onKeyUp(self, sender, keycode, modifiers): - if keycode == KEY_SHIFT: - self.__shift_down = False - - def onLostFocus(self, sender): - """Finish the edition when focus is lost""" - if self.options['listen_focus']: - self.edit(False) - - def onClick(self, sender=None): - """Start the edition when the widget is clicked""" - if self.options['listen_click']: - self.edit(True) - - def onBrowserEvent(self, event): - if self.options['listen_focus']: - FocusHandler.onBrowserEvent(self, event) - if self.options['listen_click']: - ClickHandler.onBrowserEvent(self, event) - KeyboardHandler.onBrowserEvent(self, event) - - -class HTMLTextEditor(SimpleTextEditor, HTML, FocusHandler, KeyboardHandler): - """Manage a simple text editor with the HTML 5 "contenteditable" property.""" - - def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None): - HTML.__init__(self) - SimpleTextEditor.__init__(self, content, modifiedCb, afterEditCb, options) - self.textarea = self.display = self - - def getContent(self): - text = DOM.getInnerHTML(self.getElement()) - return {'text': self.strproc(text) if text else ''} - - def edit(self, edit, abort=False, sync=False): - if edit: - self.textarea.setHTML(self._original_content['text']) - self.getElement().setAttribute('contenteditable', 'true' if edit else 'false') - SimpleTextEditor.edit(self, edit, abort, sync) - - def setFocus(self, focus): - if focus: - self.getElement().focus() - else: - self.getElement().blur() - - -class LightTextEditor(SimpleTextEditor, SimplePanel, FocusHandler, KeyboardHandler): - """Manage a simple text editor with a TextArea for editing, HTML for display.""" - - def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None): - SimplePanel.__init__(self) - SimpleTextEditor.__init__(self, content, modifiedCb, afterEditCb, options) - self.textarea = TextArea() - self.display = HTML() - - def getContent(self): - text = self.textarea.getText() - return {'text': self.strproc(text) if text else ''} - - def edit(self, edit, abort=False, sync=False): - if edit: - self.textarea.setText(self._original_content['text']) - self.setWidget(self.textarea if edit else self.display) - SimpleTextEditor.edit(self, edit, abort, sync) - - def setFocus(self, focus): - if focus: - self.textarea.setCursorPos(len(self.textarea.getText())) - self.textarea.setFocus(focus)
--- a/browser_side/base_widget.py Fri May 16 11:51:10 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,730 +0,0 @@ -#!/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/>. - -import pyjd # this is dummy in pyjs -from sat.core.log import getLogger -log = getLogger(__name__) -from pyjamas.ui.SimplePanel import SimplePanel -from pyjamas.ui.AbsolutePanel import AbsolutePanel -from pyjamas.ui.VerticalPanel import VerticalPanel -from pyjamas.ui.HorizontalPanel import HorizontalPanel -from pyjamas.ui.ScrollPanel import ScrollPanel -from pyjamas.ui.FlexTable import FlexTable -from pyjamas.ui.TabPanel import TabPanel -from pyjamas.ui.HTMLPanel import HTMLPanel -from pyjamas.ui.Label import Label -from pyjamas.ui.Button import Button -from pyjamas.ui.Image import Image -from pyjamas.ui.Widget import Widget -from pyjamas.ui.DragWidget import DragWidget -from pyjamas.ui.DropWidget import DropWidget -from pyjamas.ui.ClickListener import ClickHandler -from pyjamas.ui import HasAlignment -from pyjamas import DOM -from pyjamas import Window -from __pyjamas__ import doc - -from browser_side import dialog - - -class DragLabel(DragWidget): - - def __init__(self, text, _type): - DragWidget.__init__(self) - self._text = text - self._type = _type - - def onDragStart(self, event): - dt = event.dataTransfer - dt.setData('text/plain', "%s\n%s" % (self._text, self._type)) - dt.setDragImage(self.getElement(), 15, 15) - - -class LiberviaDragWidget(DragLabel): - """ A DragLabel which keep the widget being dragged as class value """ - current = None # widget currently dragged - - def __init__(self, text, _type, widget): - DragLabel.__init__(self, text, _type) - self.widget = widget - - def onDragStart(self, event): - LiberviaDragWidget.current = self.widget - DragLabel.onDragStart(self, event) - - def onDragEnd(self, event): - LiberviaDragWidget.current = None - - -class DropCell(DropWidget): - """Cell in the middle grid which replace itself with the dropped widget on DnD""" - drop_keys = {} - - def __init__(self, host): - DropWidget.__init__(self) - self.host = host - self.setStyleName('dropCell') - - @classmethod - def addDropKey(cls, key, callback): - DropCell.drop_keys[key] = callback - - def onDragEnter(self, event): - if self == LiberviaDragWidget.current: - return - self.addStyleName('dragover') - 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, if will leave the DropWidget, and we - # don't want that - self.removeStyleName('dragover') - - def onDragOver(self, event): - DOM.eventPreventDefault(event) - - def _getCellAndRow(self, grid, event): - """Return cell and row index where the event is occuring""" - cell = grid.getEventTargetCell(event) - row = DOM.getParent(cell) - return (row.rowIndex, cell.cellIndex) - - def onDrop(self, event): - self.removeStyleName('dragover') - DOM.eventPreventDefault(event) - dt = event.dataTransfer - # 'text', 'text/plain', and 'Text' are equivalent. - try: - 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 - # item_type = dt.getData("type") - log.debug("message: %s" % item) - log.debug("type: %s" % item_type) - except: - log.debug("no message found") - item = ' ' - item_type = None - if item_type == "WIDGET": - if not LiberviaDragWidget.current: - log.error("No widget registered in LiberviaDragWidget !") - return - _new_panel = LiberviaDragWidget.current - if self == _new_panel: # We can't drop on ourself - return - # we need to remove the widget from the panel as it will be inserted elsewhere - widgets_panel = _new_panel.getWidgetsPanel() - wid_row = widgets_panel.getWidgetCoords(_new_panel)[0] - row_wids = widgets_panel.getLiberviaRowWidgets(wid_row) - if len(row_wids) == 1 and wid_row == widgets_panel.getWidgetCoords(self)[0]: - # the dropped widget is the only one in the same row - # as the target widget (self), we don't do anything - return - widgets_panel.removeWidget(_new_panel) - elif item_type in self.drop_keys: - _new_panel = self.drop_keys[item_type](self.host, item) - else: - log.warning("unmanaged item type") - return - if isinstance(self, LiberviaWidget): - self.host.unregisterWidget(self) - self.onQuit() - if not isinstance(_new_panel, LiberviaWidget): - log.warning("droping an object which is not a class of LiberviaWidget") - _flextable = self.getParent() - _widgetspanel = _flextable.getParent().getParent() - row_idx, cell_idx = self._getCellAndRow(_flextable, event) - if self.host.getSelected == self: - self.host.setSelected(None) - _widgetspanel.changeWidget(row_idx, cell_idx, _new_panel) - """_unempty_panels = filter(lambda wid:not isinstance(wid,EmptyWidget),list(_flextable)) - _width = 90/float(len(_unempty_panels) or 1) - #now we resize all the cell of the column - for panel in _unempty_panels: - td_elt = panel.getElement().parentNode - DOM.setStyleAttribute(td_elt, "width", "%s%%" % _width)""" - #FIXME: delete object ? Check the right way with pyjamas - - -class WidgetHeader(AbsolutePanel, LiberviaDragWidget): - - def __init__(self, parent, title): - AbsolutePanel.__init__(self) - self.add(title) - button_group_wrapper = SimplePanel() - button_group_wrapper.setStyleName('widgetHeader_buttonsWrapper') - button_group = HorizontalPanel() - button_group.setStyleName('widgetHeader_buttonGroup') - setting_button = Image("media/icons/misc/settings.png") - setting_button.setStyleName('widgetHeader_settingButton') - setting_button.addClickListener(parent.onSetting) - close_button = Image("media/icons/misc/close.png") - close_button.setStyleName('widgetHeader_closeButton') - close_button.addClickListener(parent.onClose) - button_group.add(setting_button) - button_group.add(close_button) - button_group_wrapper.setWidget(button_group) - self.add(button_group_wrapper) - self.addStyleName('widgetHeader') - LiberviaDragWidget.__init__(self, "", "WIDGET", parent) - - -class LiberviaWidget(DropCell, VerticalPanel, ClickHandler): - """Libervia's widget which can replace itself with a dropped widget on DnD""" - - def __init__(self, host, title='', selectable=False): - """Init the widget - @param host: SatWebFrontend object - @param title: title show in the header of the widget - @param selectable: True is widget can be selected by user""" - VerticalPanel.__init__(self) - DropCell.__init__(self, host) - ClickHandler.__init__(self) - self.__selectable = selectable - self.__title_id = HTMLPanel.createUniqueId() - self.__setting_button_id = HTMLPanel.createUniqueId() - self.__close_button_id = HTMLPanel.createUniqueId() - self.__title = Label(title) - self.__title.setStyleName('widgetHeader_title') - self._close_listeners = [] - header = WidgetHeader(self, self.__title) - self.add(header) - self.setSize('100%', '100%') - self.addStyleName('widget') - if self.__selectable: - self.addClickListener(self) - - def onClose(sender): - """Check dynamically if the unibox is enable or not""" - if self.host.uni_box: - self.host.uni_box.onWidgetClosed(sender) - - self.addCloseListener(onClose) - self.host.registerWidget(self) - - def getDebugName(self): - return "%s (%s)" % (self, self.__title.getText()) - - def getWidgetsPanel(self, verbose=True): - return self.getParent(WidgetsPanel, verbose) - - def getParent(self, class_=None, verbose=True): - """ - Note: this method overrides pyjamas.ui.Widget.getParent - @param class_: class of the ancestor to look for or None to return the first parent - @param verbose: set to True to log error messages # FIXME: must be removed - @return: the parent/ancestor or None if it has not been found - """ - current = Widget.getParent(self) - if class_ is None: - return current # this is the default behavior - while current is not None and not isinstance(current, class_): - current = Widget.getParent(current) - if current is None and verbose: - log.debug("Can't find parent %s for %s" % (class_, self)) - return current - - def onClick(self, sender): - self.host.setSelected(self) - - def onClose(self, sender): - """ Called when the close button is pushed """ - _widgetspanel = self.getWidgetsPanel() - _widgetspanel.removeWidget(self) - for callback in self._close_listeners: - callback(self) - self.onQuit() - - def onQuit(self): - """ Called when the widget is actually ending """ - pass - - def addCloseListener(self, callback): - """Add a close listener to this widget - @param callback: function to be called from self.onClose""" - self._close_listeners.append(callback) - - def refresh(self): - """This can be overwritten by a child class to refresh the display when, - instead of creating a new one, an existing widget is found and reused. - """ - pass - - def onSetting(self, sender): - widpanel = self.getWidgetsPanel() - row, col = widpanel.getIndex(self) - body = VerticalPanel() - - # colspan & rowspan - colspan = widpanel.getColSpan(row, col) - rowspan = widpanel.getRowSpan(row, col) - - def onColSpanChange(value): - widpanel.setColSpan(row, col, value) - - def onRowSpanChange(value): - widpanel.setRowSpan(row, col, value) - colspan_setter = dialog.IntSetter("Columns span", colspan) - colspan_setter.addValueChangeListener(onColSpanChange) - colspan_setter.setWidth('100%') - rowspan_setter = dialog.IntSetter("Rows span", rowspan) - rowspan_setter.addValueChangeListener(onRowSpanChange) - rowspan_setter.setWidth('100%') - body.add(colspan_setter) - body.add(rowspan_setter) - - # size - width_str = self.getWidth() - if width_str.endswith('px'): - width = int(width_str[:-2]) - else: - width = 0 - height_str = self.getHeight() - if height_str.endswith('px'): - height = int(height_str[:-2]) - else: - height = 0 - - def onWidthChange(value): - if not value: - self.setWidth('100%') - else: - self.setWidth('%dpx' % value) - - def onHeightChange(value): - if not value: - self.setHeight('100%') - else: - self.setHeight('%dpx' % value) - width_setter = dialog.IntSetter("width (0=auto)", width) - width_setter.addValueChangeListener(onWidthChange) - width_setter.setWidth('100%') - height_setter = dialog.IntSetter("height (0=auto)", height) - height_setter.addValueChangeListener(onHeightChange) - height_setter.setHeight('100%') - body.add(width_setter) - body.add(height_setter) - - # reset - def onReset(sender): - colspan_setter.setValue(1) - rowspan_setter.setValue(1) - width_setter.setValue(0) - height_setter.setValue(0) - - reset_bt = Button("Reset", onReset) - body.add(reset_bt) - body.setCellHorizontalAlignment(reset_bt, HasAlignment.ALIGN_CENTER) - - _dialog = dialog.GenericDialog("Widget setting", body) - _dialog.show() - - def setTitle(self, text): - """change the title in the header of the widget - @param text: text of the new title""" - self.__title.setText(text) - - def isSelectable(self): - return self.__selectable - - def setSelectable(self, selectable): - if not self.__selectable: - try: - self.removeClickListener(self) - except ValueError: - pass - if self.selectable and not self in self._clickListeners: - self.addClickListener(self) - self.__selectable = selectable - - def getWarningData(self): - """ Return exposition warning level when this widget is selected and something is sent to it - This method should be overriden by children - @return: tuple (warning level type/HTML msg). Type can be one of: - - PUBLIC - - GROUP - - ONE2ONE - - MISC - - NONE - """ - if not self.__selectable: - log.error("getWarningLevel must not be called for an unselectable widget") - raise Exception - # TODO: cleaner warning types (more general constants) - return ("NONE", None) - - def setWidget(self, widget, scrollable=True): - """Set the widget that will be in the body of the LiberviaWidget - @param widget: widget to put in the body - @param scrollable: if true, the widget will be in a ScrollPanelWrapper""" - if scrollable: - _scrollpanelwrapper = ScrollPanelWrapper() - _scrollpanelwrapper.setStyleName('widgetBody') - _scrollpanelwrapper.setWidget(widget) - body_wid = _scrollpanelwrapper - else: - body_wid = widget - self.add(body_wid) - self.setCellHeight(body_wid, '100%') - - def doDetachChildren(self): - # We need to force the use of a panel subclass method here, - # for the same reason as doAttachChildren - VerticalPanel.doDetachChildren(self) - - def doAttachChildren(self): - # We need to force the use of a panel subclass method here, else - # the event will not propagate to children - VerticalPanel.doAttachChildren(self) - - def matchEntity(self, entity): - """This method should be overwritten by child classes.""" - raise NotImplementedError - - -class ScrollPanelWrapper(SimplePanel): - """Scroll Panel like component, wich use the full available space - to work around percent size issue, it use some of the ideas found - here: http://code.google.com/p/google-web-toolkit/issues/detail?id=316 - specially in code given at comment #46, thanks to Stefan Bachert""" - - def __init__(self, *args, **kwargs): - SimplePanel.__init__(self) - self.spanel = ScrollPanel(*args, **kwargs) - SimplePanel.setWidget(self, self.spanel) - DOM.setStyleAttribute(self.getElement(), "position", "relative") - DOM.setStyleAttribute(self.getElement(), "top", "0px") - DOM.setStyleAttribute(self.getElement(), "left", "0px") - DOM.setStyleAttribute(self.getElement(), "width", "100%") - DOM.setStyleAttribute(self.getElement(), "height", "100%") - DOM.setStyleAttribute(self.spanel.getElement(), "position", "absolute") - DOM.setStyleAttribute(self.spanel.getElement(), "width", "100%") - DOM.setStyleAttribute(self.spanel.getElement(), "height", "100%") - - def setWidget(self, widget): - self.spanel.setWidget(widget) - - def setScrollPosition(self, position): - self.spanel.setScrollPosition(position) - - def scrollToBottom(self): - self.setScrollPosition(self.spanel.getElement().scrollHeight) - - -class EmptyWidget(DropCell, SimplePanel): - """Empty dropable panel""" - - def __init__(self, host): - SimplePanel.__init__(self) - DropCell.__init__(self, host) - #self.setWidget(HTML('')) - self.setSize('100%', '100%') - - -class BorderWidget(EmptyWidget): - def __init__(self, host): - EmptyWidget.__init__(self, host) - self.addStyleName('borderPanel') - - -class LeftBorderWidget(BorderWidget): - def __init__(self, host): - BorderWidget.__init__(self, host) - self.addStyleName('leftBorderWidget') - - -class RightBorderWidget(BorderWidget): - def __init__(self, host): - BorderWidget.__init__(self, host) - self.addStyleName('rightBorderWidget') - - -class BottomBorderWidget(BorderWidget): - def __init__(self, host): - BorderWidget.__init__(self, host) - self.addStyleName('bottomBorderWidget') - - -class WidgetsPanel(ScrollPanelWrapper): - - def __init__(self, host, locked=False): - ScrollPanelWrapper.__init__(self) - self.setSize('100%', '100%') - self.host = host - self.locked = locked # if True: tab will not be removed when there are no more widgets inside - self.selected = None - self.flextable = FlexTable() - self.flextable.setSize('100%', '100%') - self.setWidget(self.flextable) - self.setStyleName('widgetsPanel') - _bottom = BottomBorderWidget(self.host) - self.flextable.setWidget(0, 0, _bottom) # There will be always an Empty widget on the last row, - # dropping a widget there will add a new row - td_elt = _bottom.getElement().parentNode - DOM.setStyleAttribute(td_elt, "height", "1px") # needed so the cell adapt to the size of the border (specially in webkit) - self._max_cols = 1 # give the maximum number of columns i a raw - - def isLocked(self): - return self.locked - - def changeWidget(self, row, col, wid): - """Change the widget in the given location, add row or columns when necessary""" - log.debug("changing widget: %s %s %s" % (wid.getDebugName(), row, col)) - last_row = max(0, self.flextable.getRowCount() - 1) - try: - prev_wid = self.flextable.getWidget(row, col) - except: - log.error("Trying to change an unexisting widget !") - return - - cellFormatter = self.flextable.getFlexCellFormatter() - - if isinstance(prev_wid, BorderWidget): - # We are on a border, we must create a row and/or columns - log.debug("BORDER WIDGET") - prev_wid.removeStyleName('dragover') - - if isinstance(prev_wid, BottomBorderWidget): - # We are on the bottom border, we create a new row - self.flextable.insertRow(last_row) - self.flextable.setWidget(last_row, 0, LeftBorderWidget(self.host)) - self.flextable.setWidget(last_row, 1, wid) - self.flextable.setWidget(last_row, 2, RightBorderWidget(self.host)) - cellFormatter.setHorizontalAlignment(last_row, 2, HasAlignment.ALIGN_RIGHT) - row = last_row - - elif isinstance(prev_wid, LeftBorderWidget): - if col != 0: - log.error("LeftBorderWidget must be on the first column !") - return - self.flextable.insertCell(row, col + 1) - self.flextable.setWidget(row, 1, wid) - - elif isinstance(prev_wid, RightBorderWidget): - if col != self.flextable.getCellCount(row) - 1: - log.error("RightBorderWidget must be on the last column !") - return - self.flextable.insertCell(row, col) - self.flextable.setWidget(row, col, wid) - - else: - prev_wid.removeFromParent() - self.flextable.setWidget(row, col, wid) - - _max_cols = max(self._max_cols, self.flextable.getCellCount(row)) - if _max_cols != self._max_cols: - self._max_cols = _max_cols - self._sizesAdjust() - - def _sizesAdjust(self): - cellFormatter = self.flextable.getFlexCellFormatter() - width = 100.0 / max(1, self._max_cols - 2) # we don't count the borders - - for row_idx in xrange(self.flextable.getRowCount()): - for col_idx in xrange(self.flextable.getCellCount(row_idx)): - _widget = self.flextable.getWidget(row_idx, col_idx) - if not isinstance(_widget, BorderWidget): - td_elt = _widget.getElement().parentNode - DOM.setStyleAttribute(td_elt, "width", "%.2f%%" % width) - - last_row = max(0, self.flextable.getRowCount() - 1) - cellFormatter.setColSpan(last_row, 0, self._max_cols) - - def addWidget(self, wid): - """Add a widget to a new cell on the next to last row""" - last_row = max(0, self.flextable.getRowCount() - 1) - log.debug("putting widget %s at %d, %d" % (wid.getDebugName(), last_row, 0)) - self.changeWidget(last_row, 0, wid) - - def removeWidget(self, wid): - """Remove a widget and the cell where it is""" - _row, _col = self.flextable.getIndex(wid) - self.flextable.remove(wid) - self.flextable.removeCell(_row, _col) - if not self.getLiberviaRowWidgets(_row): # we have no more widgets, we remove the row - self.flextable.removeRow(_row) - _max_cols = 1 - for row_idx in xrange(self.flextable.getRowCount()): - _max_cols = max(_max_cols, self.flextable.getCellCount(row_idx)) - if _max_cols != self._max_cols: - self._max_cols = _max_cols - self._sizesAdjust() - current = self - - blank_page = self.getLiberviaWidgetsCount() == 0 # do we still have widgets on the page ? - - if blank_page and not self.isLocked(): - # we now notice the MainTabPanel that the WidgetsPanel is empty and need to be removed - while current is not None: - if isinstance(current, MainTabPanel): - current.onWidgetPanelRemove(self) - return - current = current.getParent() - log.error("no MainTabPanel found !") - - def getWidgetCoords(self, wid): - return self.flextable.getIndex(wid) - - def getLiberviaRowWidgets(self, row): - """ Return all the LiberviaWidget in the row """ - return [wid for wid in self.getRowWidgets(row) if isinstance(wid, LiberviaWidget)] - - def getRowWidgets(self, row): - """ Return all the widgets in the row """ - widgets = [] - cols = self.flextable.getCellCount(row) - for col in xrange(cols): - widgets.append(self.flextable.getWidget(row, col)) - return widgets - - def getLiberviaWidgetsCount(self): - """ Get count of contained widgets """ - return len([wid for wid in self.flextable if isinstance(wid, LiberviaWidget)]) - - def getIndex(self, wid): - return self.flextable.getIndex(wid) - - def getColSpan(self, row, col): - cellFormatter = self.flextable.getFlexCellFormatter() - return cellFormatter.getColSpan(row, col) - - def setColSpan(self, row, col, value): - cellFormatter = self.flextable.getFlexCellFormatter() - return cellFormatter.setColSpan(row, col, value) - - def getRowSpan(self, row, col): - cellFormatter = self.flextable.getFlexCellFormatter() - return cellFormatter.getRowSpan(row, col) - - def setRowSpan(self, row, col, value): - cellFormatter = self.flextable.getFlexCellFormatter() - return cellFormatter.setRowSpan(row, col, value) - - -class DropTab(Label, DropWidget): - - def __init__(self, tab_panel, text): - Label.__init__(self, text) - DropWidget.__init__(self, tab_panel) - self.tab_panel = tab_panel - self.setStyleName('dropCell') - self.setWordWrap(False) - DOM.setStyleAttribute(self.getElement(), "min-width", "30px") - - def _getIndex(self): - """ get current index of the DropTab """ - # XXX: awful hack, but seems the only way to get index - return self.tab_panel.tabBar.panel.getWidgetIndex(self.getParent().getParent()) - 1 - - def onDragEnter(self, event): - #if self == LiberviaDragWidget.current: - # return - self.addStyleName('dragover') - DOM.eventPreventDefault(event) - - def onDragLeave(self, event): - self.removeStyleName('dragover') - - def onDragOver(self, event): - DOM.eventPreventDefault(event) - - def onDrop(self, event): - DOM.eventPreventDefault(event) - self.removeStyleName('dragover') - if self._getIndex() == self.tab_panel.tabBar.getSelectedTab(): - # the widget come from the DragTab, so nothing to do, we let it there - return - - # FIXME: quite the same stuff as in DropCell, need some factorisation - dt = event.dataTransfer - # 'text', 'text/plain', and 'Text' are equivalent. - try: - 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 - # item_type = dt.getData("type") - log.debug("message: %s" % item) - log.debug("type: %s" % item_type) - except: - log.debug("no message found") - item = ' ' - item_type = None - if item_type == "WIDGET": - if not LiberviaDragWidget.current: - log.error("No widget registered in LiberviaDragWidget !") - return - _new_panel = LiberviaDragWidget.current - _new_panel.getWidgetsPanel().removeWidget(_new_panel) - elif item_type in DropCell.drop_keys: - _new_panel = DropCell.drop_keys[item_type](self.tab_panel.host, item) - else: - log.warning("unmanaged item type") - return - - widgets_panel = self.tab_panel.getWidget(self._getIndex()) - widgets_panel.addWidget(_new_panel) - - -class MainTabPanel(TabPanel): - - def __init__(self, host): - TabPanel.__init__(self) - self.host = host - self.tabBar.setVisible(False) - self.setStyleName('liberviaTabPanel') - self.addStyleName('mainTabPanel') - Window.addWindowResizeListener(self) - - def getCurrentPanel(self): - """ Get the panel of the currently selected tab """ - return self.deck.visibleWidget - - def onWindowResized(self, width, height): - tab_panel_elt = self.getElement() - _elts = doc().getElementsByClassName('gwt-TabBar') - if not _elts.length: - log.error("no TabBar found, it should exist !") - tab_bar_h = 0 - else: - tab_bar_h = _elts.item(0).offsetHeight - ideal_height = height - DOM.getAbsoluteTop(tab_panel_elt) - tab_bar_h - 5 - ideal_width = width - DOM.getAbsoluteLeft(tab_panel_elt) - 5 - self.setWidth("%s%s" % (ideal_width, "px")) - self.setHeight("%s%s" % (ideal_height, "px")) - - def add(self, widget, text=''): - tab = DropTab(self, text) - TabPanel.add(self, widget, tab, False) - if self.getWidgetCount() > 1: - self.tabBar.setVisible(True) - self.host.resize() - - def onWidgetPanelRemove(self, panel): - """ Called when a child WidgetsPanel is empty and need to be removed """ - self.remove(panel) - widgets_count = self.getWidgetCount() - if widgets_count == 1: - self.tabBar.setVisible(False) - self.host.resize() - self.selectTab(0) - else: - self.selectTab(widgets_count - 1)
--- a/browser_side/card_game.py Fri May 16 11:51:10 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,386 +0,0 @@ -#!/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/>. - -import pyjd # this is dummy in pyjs -from sat.core.log import getLogger -log = getLogger(__name__) -from pyjamas.ui.AbsolutePanel import AbsolutePanel -from pyjamas.ui.DockPanel import DockPanel -from pyjamas.ui.SimplePanel import SimplePanel -from pyjamas.ui.Image import Image -from pyjamas.ui.Label import Label -from pyjamas.ui.ClickListener import ClickHandler -from pyjamas.ui.MouseListener import MouseHandler -from pyjamas.ui import HasAlignment -from pyjamas import Window -from pyjamas import DOM - -from dialog import ConfirmDialog, GenericDialog -from xmlui import XMLUI -from sat_frontends.tools.games import TarotCard -from sat.core.i18n import _ - - -CARD_WIDTH = 74 -CARD_HEIGHT = 136 -CARD_DELTA_Y = 30 -MIN_WIDTH = 950 # Minimum size of the panel -MIN_HEIGHT = 500 - - -class CardWidget(TarotCard, Image, MouseHandler): - """This class is used to represent a card, graphically and logically""" - - def __init__(self, parent, file_): - """@param file: path of the PNG file""" - self._parent = parent - Image.__init__(self, file_) - root_name = file_[file_.rfind("/") + 1:-4] - suit, value = root_name.split('_') - TarotCard.__init__(self, (suit, value)) - MouseHandler.__init__(self) - self.addMouseListener(self) - - def onMouseEnter(self, sender): - if self._parent.state == "ecart" or self._parent.state == "play": - DOM.setStyleAttribute(self.getElement(), "top", "0px") - - def onMouseLeave(self, sender): - if not self in self._parent.hand: - return - if not self in list(self._parent.selected): # FIXME: Workaround pyjs bug, must report it - DOM.setStyleAttribute(self.getElement(), "top", "%dpx" % CARD_DELTA_Y) - - def onMouseUp(self, sender, x, y): - if self._parent.state == "ecart": - if self not in list(self._parent.selected): - self._parent.addToSelection(self) - else: - self._parent.removeFromSelection(self) - elif self._parent.state == "play": - self._parent.playCard(self) - - -class CardPanel(DockPanel, ClickHandler): - - def __init__(self, parent, referee, player_nick, players): - DockPanel.__init__(self) - ClickHandler.__init__(self) - self._parent = parent - self._autoplay = None # XXX: use 0 to activate fake play, None else - self.referee = referee - self.players = players - self.player_nick = player_nick - self.bottom_nick = self.player_nick - idx = self.players.index(self.player_nick) - idx = (idx + 1) % len(self.players) - self.right_nick = self.players[idx] - idx = (idx + 1) % len(self.players) - self.top_nick = self.players[idx] - idx = (idx + 1) % len(self.players) - self.left_nick = self.players[idx] - self.bottom_nick = player_nick - self.selected = set() # Card choosed by the player (e.g. during ecart) - self.hand_size = 13 # number of cards in a hand - self.hand = [] - self.to_show = [] - self.state = None - self.setSize("%dpx" % MIN_WIDTH, "%dpx" % MIN_HEIGHT) - self.setStyleName("cardPanel") - - # Now we set up the layout - _label = Label(self.top_nick) - _label.setStyleName('cardGamePlayerNick') - self.add(_label, DockPanel.NORTH) - self.setCellWidth(_label, '100%') - self.setCellHorizontalAlignment(_label, HasAlignment.ALIGN_CENTER) - - self.hand_panel = AbsolutePanel() - self.add(self.hand_panel, DockPanel.SOUTH) - self.setCellWidth(self.hand_panel, '100%') - self.setCellHorizontalAlignment(self.hand_panel, HasAlignment.ALIGN_CENTER) - - _label = Label(self.left_nick) - _label.setStyleName('cardGamePlayerNick') - self.add(_label, DockPanel.WEST) - self.setCellHeight(_label, '100%') - self.setCellVerticalAlignment(_label, HasAlignment.ALIGN_MIDDLE) - - _label = Label(self.right_nick) - _label.setStyleName('cardGamePlayerNick') - self.add(_label, DockPanel.EAST) - self.setCellHeight(_label, '100%') - self.setCellHorizontalAlignment(_label, HasAlignment.ALIGN_RIGHT) - self.setCellVerticalAlignment(_label, HasAlignment.ALIGN_MIDDLE) - - self.center_panel = DockPanel() - self.inner_left = SimplePanel() - self.inner_left.setSize("%dpx" % CARD_WIDTH, "%dpx" % CARD_HEIGHT) - self.center_panel.add(self.inner_left, DockPanel.WEST) - self.center_panel.setCellHeight(self.inner_left, '100%') - self.center_panel.setCellHorizontalAlignment(self.inner_left, HasAlignment.ALIGN_RIGHT) - self.center_panel.setCellVerticalAlignment(self.inner_left, HasAlignment.ALIGN_MIDDLE) - - self.inner_right = SimplePanel() - self.inner_right.setSize("%dpx" % CARD_WIDTH, "%dpx" % CARD_HEIGHT) - self.center_panel.add(self.inner_right, DockPanel.EAST) - self.center_panel.setCellHeight(self.inner_right, '100%') - self.center_panel.setCellVerticalAlignment(self.inner_right, HasAlignment.ALIGN_MIDDLE) - - self.inner_top = SimplePanel() - self.inner_top.setSize("%dpx" % CARD_WIDTH, "%dpx" % CARD_HEIGHT) - self.center_panel.add(self.inner_top, DockPanel.NORTH) - self.center_panel.setCellHorizontalAlignment(self.inner_top, HasAlignment.ALIGN_CENTER) - self.center_panel.setCellVerticalAlignment(self.inner_top, HasAlignment.ALIGN_BOTTOM) - - self.inner_bottom = SimplePanel() - self.inner_bottom.setSize("%dpx" % CARD_WIDTH, "%dpx" % CARD_HEIGHT) - self.center_panel.add(self.inner_bottom, DockPanel.SOUTH) - self.center_panel.setCellHorizontalAlignment(self.inner_bottom, HasAlignment.ALIGN_CENTER) - self.center_panel.setCellVerticalAlignment(self.inner_bottom, HasAlignment.ALIGN_TOP) - - self.inner_center = SimplePanel() - self.center_panel.add(self.inner_center, DockPanel.CENTER) - self.center_panel.setCellHorizontalAlignment(self.inner_center, HasAlignment.ALIGN_CENTER) - self.center_panel.setCellVerticalAlignment(self.inner_center, HasAlignment.ALIGN_MIDDLE) - - self.add(self.center_panel, DockPanel.CENTER) - self.setCellWidth(self.center_panel, '100%') - self.setCellHeight(self.center_panel, '100%') - self.setCellVerticalAlignment(self.center_panel, HasAlignment.ALIGN_MIDDLE) - self.setCellHorizontalAlignment(self.center_panel, HasAlignment.ALIGN_CENTER) - - self.loadCards() - self.mouse_over_card = None # contain the card to highlight - self.visible_size = CARD_WIDTH / 2 # number of pixels visible for cards - self.addClickListener(self) - - def loadCards(self): - """Load all the cards in memory""" - def _getTarotCardsPathsCb(paths): - log.debug("_getTarotCardsPathsCb") - for file_ in paths: - log.debug("path:", file_) - card = CardWidget(self, file_) - log.debug("card:", card) - self.cards[(card.suit, card.value)] = card - self.deck.append(card) - self._parent.host.bridge.call('tarotGameReady', None, self.player_nick, self.referee) - self.cards = {} - self.deck = [] - self.cards["atout"] = {} # As Tarot is a french game, it's more handy & logical to keep french names - self.cards["pique"] = {} # spade - self.cards["coeur"] = {} # heart - self.cards["carreau"] = {} # diamond - self.cards["trefle"] = {} # club - self._parent.host.bridge.call('getTarotCardsPaths', _getTarotCardsPathsCb) - - def onClick(self, sender): - if self.state == "chien": - self.to_show = [] - self.state = "wait" - self.updateToShow() - elif self.state == "wait_for_ecart": - self.state = "ecart" - self.hand.extend(self.to_show) - self.hand.sort() - self.to_show = [] - self.updateToShow() - self.updateHand() - - def tarotGameNew(self, hand): - """Start a new game, with given hand""" - if hand is []: # reset the display after the scores have been showed - self.selected.clear() - del self.hand[:] - del self.to_show[:] - self.state = None - #empty hand - self.updateHand() - #nothing on the table - self.updateToShow() - for pos in ['top', 'left', 'bottom', 'right']: - getattr(self, "inner_%s" % pos).setWidget(None) - self._parent.host.bridge.call('tarotGameReady', None, self.player_nick, self.referee) - return - for suit, value in hand: - self.hand.append(self.cards[(suit, value)]) - self.hand.sort() - self.state = "init" - self.updateHand() - - def updateHand(self): - """Show the cards in the hand in the hand_panel (SOUTH panel)""" - self.hand_panel.clear() - self.hand_panel.setSize("%dpx" % (self.visible_size * (len(self.hand) + 1)), "%dpx" % (CARD_HEIGHT + CARD_DELTA_Y + 10)) - x_pos = 0 - y_pos = CARD_DELTA_Y - for card in self.hand: - self.hand_panel.add(card, x_pos, y_pos) - x_pos += self.visible_size - - def updateToShow(self): - """Show cards in the center panel""" - if not self.to_show: - _widget = self.inner_center.getWidget() - if _widget: - self.inner_center.remove(_widget) - return - panel = AbsolutePanel() - panel.setSize("%dpx" % ((CARD_WIDTH + 5) * len(self.to_show) - 5), "%dpx" % (CARD_HEIGHT)) - x_pos = 0 - y_pos = 0 - for card in self.to_show: - panel.add(card, x_pos, y_pos) - x_pos += CARD_WIDTH + 5 - self.inner_center.setWidget(panel) - - def _ecartConfirm(self, confirm): - if not confirm: - return - ecart = [] - for card in self.selected: - ecart.append((card.suit, card.value)) - self.hand.remove(card) - self.selected.clear() - self._parent.host.bridge.call('tarotGamePlayCards', None, self.player_nick, self.referee, ecart) - self.state = "wait" - self.updateHand() - - def addToSelection(self, card): - self.selected.add(card) - if len(self.selected) == 6: - ConfirmDialog(self._ecartConfirm, "Put these cards into chien ?").show() - - def tarotGameInvalidCards(self, phase, played_cards, invalid_cards): - """Invalid cards have been played - @param phase: phase of the game - @param played_cards: all the cards played - @param invalid_cards: cards which are invalid""" - - if phase == "play": - self.state = "play" - elif phase == "ecart": - self.state = "ecart" - else: - log.error("INTERNAL ERROR: unmanaged game phase") # FIXME: raise an exception here - - for suit, value in played_cards: - self.hand.append(self.cards[(suit, value)]) - - self.hand.sort() - self.updateHand() - if self._autoplay == None: # No dialog if there is autoplay - Window.alert('Cards played are invalid !') - self.__fakePlay() - - def removeFromSelection(self, card): - self.selected.remove(card) - if len(self.selected) == 6: - ConfirmDialog(self._ecartConfirm, "Put these cards into chien ?").show() - - def tarotGameChooseContrat(self, xml_data): - """Called when the player has to select his contrat - @param xml_data: SàT xml representation of the form""" - body = XMLUI(self._parent.host, xml_data, flags=['NO_CANCEL']) - _dialog = GenericDialog(_('Please choose your contrat'), body, options=['NO_CLOSE']) - body.setCloseCb(_dialog.close) - _dialog.show() - - def tarotGameShowCards(self, game_stage, cards, data): - """Display cards in the middle of the game (to show for e.g. chien ou poignée)""" - self.to_show = [] - for suit, value in cards: - self.to_show.append(self.cards[(suit, value)]) - self.updateToShow() - if game_stage == "chien" and data['attaquant'] == self.player_nick: - self.state = "wait_for_ecart" - else: - self.state = "chien" - - def getPlayerLocation(self, nick): - """return player location (top,bottom,left or right)""" - for location in ['top', 'left', 'bottom', 'right']: - if getattr(self, '%s_nick' % location) == nick: - return location - log.error("This line should not be reached") - - def tarotGameCardsPlayed(self, player, cards): - """A card has been played by player""" - if not len(cards): - log.warning("cards should not be empty") - return - if len(cards) > 1: - log.error("can't manage several cards played") - if self.to_show: - self.to_show = [] - self.updateToShow() - suit, value = cards[0] - player_pos = self.getPlayerLocation(player) - player_panel = getattr(self, "inner_%s" % player_pos) - - if player_panel.getWidget() != None: - #We have already cards on the table, we remove them - for pos in ['top', 'left', 'bottom', 'right']: - getattr(self, "inner_%s" % pos).setWidget(None) - - card = self.cards[(suit, value)] - DOM.setElemAttribute(card.getElement(), "style", "") - player_panel.setWidget(card) - - def tarotGameYourTurn(self): - """Called when we have to play :)""" - if self.state == "chien": - self.to_show = [] - self.updateToShow() - self.state = "play" - self.__fakePlay() - - def __fakePlay(self): - """Convenience method for stupid autoplay - /!\ don't forgot to comment any interactive dialog for invalid card""" - if self._autoplay == None: - return - if self._autoplay >= len(self.hand): - self._autoplay = 0 - card = self.hand[self._autoplay] - self._parent.host.bridge.call('tarotGamePlayCards', None, self.player_nick, self.referee, [(card.suit, card.value)]) - del self.hand[self._autoplay] - self.state = "wait" - self._autoplay += 1 - - def playCard(self, card): - self.hand.remove(card) - self._parent.host.bridge.call('tarotGamePlayCards', None, self.player_nick, self.referee, [(card.suit, card.value)]) - self.state = "wait" - self.updateHand() - - def tarotGameScore(self, xml_data, winners, loosers): - """Show score at the end of a round""" - if not winners and not loosers: - title = "Draw game" - else: - if self.player_nick in winners: - title = "You <b>win</b> !" - else: - title = "You <b>loose</b> :(" - body = XMLUI(self._parent.host, xml_data, title=title, flags=['NO_CANCEL']) - _dialog = GenericDialog(title, body, options=['NO_CLOSE']) - body.setCloseCb(_dialog.close) - _dialog.show()
--- a/browser_side/contact.py Fri May 16 11:51:10 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,414 +0,0 @@ -#!/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/>. - -import pyjd # this is dummy in pyjs -from sat.core.log import getLogger -log = getLogger(__name__) -from pyjamas.ui.SimplePanel import SimplePanel -from pyjamas.ui.ScrollPanel import ScrollPanel -from pyjamas.ui.VerticalPanel import VerticalPanel -from pyjamas.ui.ClickListener import ClickHandler -from pyjamas.ui.Label import Label -from pyjamas.ui.HTML import HTML -from jid import JID -from pyjamas import Window -from pyjamas import DOM - -from browser_side.base_panels import PopupMenuPanel -from browser_side.base_widget import DragLabel -from browser_side.panels import ChatPanel, MicroblogPanel, WebPanel, UniBoxPanel -from browser_side.html_tools import html_sanitize -from __pyjamas__ import doc - - -def setPresenceStyle(element, presence, base_style="contact"): - """ - Set the CSS style of a contact's element according to its presence. - @param item: the UI element of the contact - @param presence: a value in ("", "chat", "away", "dnd", "xa"). - @param base_style: the base name of the style to apply - """ - if not hasattr(element, 'presence_style'): - element.presence_style = None - style = '%s-%s' % (base_style, presence or 'connected') - if style == element.presence_style: - return - if element.presence_style is not None: - element.removeStyleName(element.presence_style) - element.addStyleName(style) - element.presence_style = style - - -class GroupLabel(DragLabel, Label, ClickHandler): - def __init__(self, host, group): - self.group = group - self.host = host - Label.__init__(self, group) #, Element=DOM.createElement('div') - self.setStyleName('group') - DragLabel.__init__(self, group, "GROUP") - ClickHandler.__init__(self) - self.addClickListener(self) - - def onClick(self, sender): - self.host.getOrCreateLiberviaWidget(MicroblogPanel, self.group) - - -class ContactLabel(DragLabel, HTML, ClickHandler): - def __init__(self, host, jid, name=None, handleClick=True): - HTML.__init__(self) - self.host = host - self.name = name or jid - self.waiting = False - self.jid = jid - self._fill() - self.setStyleName('contact') - DragLabel.__init__(self, jid, "CONTACT") - if handleClick: - ClickHandler.__init__(self) - self.addClickListener(self) - - def _fill(self): - if self.waiting: - _wait_html = "<b>(*)</b> " - self.setHTML("%(wait)s%(name)s" % {'wait': _wait_html, - 'name': html_sanitize(self.name)}) - - def setMessageWaiting(self, waiting): - """Show a visual indicator if message are waiting - @param waiting: True if message are waiting""" - self.waiting = waiting - self._fill() - - def onClick(self, sender): - self.host.getOrCreateLiberviaWidget(ChatPanel, self.jid) - - -class GroupList(VerticalPanel): - - def __init__(self, parent): - VerticalPanel.__init__(self) - self.setStyleName('groupList') - self._parent = parent - - def add(self, group): - _item = GroupLabel(self._parent.host, group) - _item.addMouseListener(self._parent) - DOM.setStyleAttribute(_item.getElement(), "cursor", "pointer") - index = 0 - for group_ in [group.group for group in self.getChildren()]: - if group_ > group: - break - index += 1 - VerticalPanel.insert(self, _item, index) - - def remove(self, group): - for wid in self: - if isinstance(wid, GroupLabel) and wid.group == group: - VerticalPanel.remove(self, wid) - - -class GenericContactList(VerticalPanel): - """Class that can be used to represent a contact list, but not necessarily - the one that is displayed on the left side. Special features like popup menu - panel or changing the contact states must be done in a sub-class.""" - - def __init__(self, host, handleClick=False): - VerticalPanel.__init__(self) - self.host = host - self.contacts = [] - self.handleClick = handleClick - - def add(self, jid, name=None, item_cb=None): - if jid in self.contacts: - return - index = 0 - for contact_ in self.contacts: - if contact_ > jid: - break - index += 1 - self.contacts.insert(index, jid) - _item = ContactLabel(self.host, jid, name, handleClick=self.handleClick) - DOM.setStyleAttribute(_item.getElement(), "cursor", "pointer") - VerticalPanel.insert(self, _item, index) - if item_cb is not None: - item_cb(_item) - - def remove(self, jid): - wid = self.getContactLabel(jid) - if not wid: - return - VerticalPanel.remove(self, wid) - self.contacts.remove(jid) - - def isContactPresent(self, contact_jid): - """Return True if a contact is present in the panel""" - return contact_jid in self.contacts - - def getContacts(self): - return self.contacts - - def getContactLabel(self, contact_jid): - """get contactList widget of a contact - @return: ContactLabel item if present, else None""" - for wid in self: - if isinstance(wid, ContactLabel) and wid.jid == contact_jid: - return wid - return None - - -class ContactList(GenericContactList): - """The contact list that is displayed on the left side.""" - - def __init__(self, host): - GenericContactList.__init__(self, host, handleClick=True) - self.menu_entries = {"blog": {"title": "Public blog..."}} - self.context_menu = PopupMenuPanel(entries=self.menu_entries, - hide=self.contextMenuHide, - callback=self.contextMenuCallback, - vertical=False, style={"selected": "menu-selected"}) - - def contextMenuHide(self, sender, key): - """Return True if the item for that sender should be hidden.""" - # TODO: enable the blogs of users that are on another server - return JID(sender.jid).domain != self.host._defaultDomain - - def contextMenuCallback(self, sender, key): - if key == "blog": - # TODO: use the bare when all blogs can be retrieved - node = JID(sender.jid).node - web_panel = WebPanel(self.host, "/blog/%s" % node) - self.host.addTab("%s's blog" % node, web_panel) - else: - sender.onClick(sender) - - def add(self, jid, name=None): - def item_cb(item): - self.context_menu.registerRightClickSender(item) - GenericContactList.add(self, jid, name, item_cb) - - def setState(self, jid, type_, state): - """Change the appearance of the contact, according to the state - @param jid: jid which need to change state - @param type_: one of availability, messageWaiting - @param state: - - for messageWaiting type: - True if message are waiting - - for availability type: - 'unavailable' if not connected, else presence like RFC6121 #4.7.2.1""" - _item = self.getContactLabel(jid) - if _item: - if type_ == 'availability': - setPresenceStyle(_item, state) - elif type_ == 'messageWaiting': - _item.setMessageWaiting(state) - - -class ContactTitleLabel(DragLabel, Label, ClickHandler): - def __init__(self, host, text): - Label.__init__(self, text) #, Element=DOM.createElement('div') - self.host = host - self.setStyleName('contactTitle') - DragLabel.__init__(self, text, "CONTACT_TITLE") - ClickHandler.__init__(self) - self.addClickListener(self) - - def onClick(self, sender): - self.host.getOrCreateLiberviaWidget(MicroblogPanel, None) - - -class ContactPanel(SimplePanel): - """Manage the contacts and groups""" - - def __init__(self, host): - SimplePanel.__init__(self) - - self.scroll_panel = ScrollPanel() - - self.host = host - self.groups = {} - self.connected = {} # jid connected as key and their status - - self.vPanel = VerticalPanel() - _title = ContactTitleLabel(host, 'Contacts') - DOM.setStyleAttribute(_title.getElement(), "cursor", "pointer") - - self._contact_list = ContactList(host) - self._contact_list.setStyleName('contactList') - self._groupList = GroupList(self) - self._groupList.setStyleName('groupList') - - self.vPanel.add(_title) - self.vPanel.add(self._groupList) - self.vPanel.add(self._contact_list) - self.scroll_panel.add(self.vPanel) - self.add(self.scroll_panel) - self.setStyleName('contactBox') - Window.addWindowResizeListener(self) - - def onWindowResized(self, width, height): - contact_panel_elt = self.getElement() - classname = 'widgetsPanel' if isinstance(self.getParent().getParent(), UniBoxPanel) else'gwt-TabBar' - _elts = doc().getElementsByClassName(classname) - if not _elts.length: - log.error("no element of class %s found, it should exist !" % classname) - tab_bar_h = height - else: - tab_bar_h = DOM.getAbsoluteTop(_elts.item(0)) or height # getAbsoluteTop can be 0 if tabBar is hidden - - ideal_height = tab_bar_h - DOM.getAbsoluteTop(contact_panel_elt) - 5 - self.scroll_panel.setHeight("%s%s" % (ideal_height, "px")) - - def updateContact(self, jid, attributes, groups): - """Add a contact to the panel if it doesn't exist, update it else - @param jid: jid userhost as unicode - @attributes: cf SàT Bridge API's newContact - @param groups: list of groups""" - _current_groups = self.getContactGroups(jid) - _new_groups = set(groups) - _key = "@%s: " - - for group in _current_groups.difference(_new_groups): - # We remove the contact from the groups where he isn't anymore - self.groups[group].remove(jid) - if not self.groups[group]: - # The group is now empty, we must remove it - del self.groups[group] - self._groupList.remove(group) - if self.host.uni_box: - self.host.uni_box.removeKey(_key % group) - - for group in _new_groups.difference(_current_groups): - # We add the contact to the groups he joined - if not group in self.groups.keys(): - self.groups[group] = set() - self._groupList.add(group) - if self.host.uni_box: - self.host.uni_box.addKey(_key % group) - self.groups[group].add(jid) - - # We add the contact to contact list, it will check if contact already exists - self._contact_list.add(jid) - - def removeContact(self, jid): - """Remove contacts from groups where he is and contact list""" - self.updateContact(jid, {}, []) # we remove contact from every group - self._contact_list.remove(jid) - - def setConnected(self, jid, resource, availability, priority, statuses): - """Set connection status - @param jid: JID userhost as unicode - """ - if availability == 'unavailable': - if jid in self.connected: - if resource in self.connected[jid]: - del self.connected[jid][resource] - if not self.connected[jid]: - del self.connected[jid] - else: - if not jid in self.connected: - self.connected[jid] = {} - self.connected[jid][resource] = (availability, priority, statuses) - - # check if the contact is connected with another resource, use the one with highest priority - if jid in self.connected: - max_resource = max_priority = None - for tmp_resource in self.connected[jid]: - if max_priority is None or self.connected[jid][tmp_resource][1] >= max_priority: - max_resource = tmp_resource - max_priority = self.connected[jid][tmp_resource][1] - if availability == "unavailable": # do not check the priority here, because 'unavailable' has a dummy one - priority = max_priority - availability = self.connected[jid][max_resource][0] - if jid not in self.connected or priority >= max_priority: - # case 1: jid not in self.connected means all resources are disconnected, update with 'unavailable' - # case 2: update (or confirm) with the values of the resource which takes precedence - self._contact_list.setState(jid, "availability", availability) - - # update the connected contacts chooser live - if hasattr(self.host, "room_contacts_chooser") and self.host.room_contacts_chooser is not None: - self.host.room_contacts_chooser.resetContacts() - - def setContactMessageWaiting(self, jid, waiting): - """Show an visual indicator that contact has send a message - @param jid: jid of the contact - @param waiting: True if message are waiting""" - self._contact_list.setState(jid, "messageWaiting", waiting) - - def getConnected(self, filter_muc=False): - """return a list of all jid (bare jid) connected - @param filter_muc: if True, remove the groups from the list - """ - contacts = self.connected.keys() - contacts.sort() - return contacts if not filter_muc else list(set(contacts).intersection(set(self.getContacts()))) - - def getContactGroups(self, contact_jid): - """Get groups where contact is - @param group: string of single group, or list of string - @param contact_jid: jid to test - """ - result = set() - for group in self.groups: - if self.isContactInGroup(group, contact_jid): - result.add(group) - return result - - def isContactInGroup(self, group, contact_jid): - """Test if the contact_jid is in the group - @param group: string of single group, or list of string - @param contact_jid: jid to test - @return: True if contact_jid is in on of the groups""" - if group in self.groups and contact_jid in self.groups[group]: - return True - return False - - def isContactInRoster(self, contact_jid): - """Test if the contact is in our roster list""" - for _contact_label in self._contact_list: - if contact_jid == _contact_label.jid: - return True - return False - - def getContacts(self): - return self._contact_list.getContacts() - - def getGroups(self): - return self.groups.keys() - - def onMouseMove(self, sender, x, y): - pass - - def onMouseDown(self, sender, x, y): - pass - - def onMouseUp(self, sender, x, y): - pass - - def onMouseEnter(self, sender): - if isinstance(sender, GroupLabel): - for contact in self._contact_list: - if contact.jid in self.groups[sender.group]: - contact.addStyleName("selected") - - def onMouseLeave(self, sender): - if isinstance(sender, GroupLabel): - for contact in self._contact_list: - if contact.jid in self.groups[sender.group]: - contact.removeStyleName("selected") -
--- a/browser_side/contact_group.py Fri May 16 11:51:10 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,236 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2013, 2014 Adrien Cossa <souliane@mailoo.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/>. - -from pyjamas.ui.FlexTable import FlexTable -from pyjamas.ui.DockPanel import DockPanel -from pyjamas.Timer import Timer -from pyjamas.ui.Button import Button -from pyjamas.ui.HorizontalPanel import HorizontalPanel -from pyjamas.ui.VerticalPanel import VerticalPanel -from pyjamas.ui.DialogBox import DialogBox -from pyjamas.ui import HasAlignment -from browser_side.dialog import ConfirmDialog, InfoDialog -from list_manager import ListManager -from browser_side import dialog -from browser_side import contact - - -class ContactGroupManager(ListManager): - """A manager for sub-panels to assign contacts to each group.""" - - def __init__(self, parent, keys_dict, contact_list, offsets, style): - ListManager.__init__(self, parent, keys_dict, contact_list, offsets, style) - self.registerPopupMenuPanel(entries={"Remove group": {}}, - callback=lambda sender, key: Timer(5, lambda timer: self.removeContactKey(sender, key))) - - def removeContactKey(self, sender, key): - key = sender.getText() - - def confirm_cb(answer): - if answer: - ListManager.removeContactKey(self, key) - self._parent.removeKeyFromAddGroupPanel(key) - - _dialog = ConfirmDialog(confirm_cb, text="Do you really want to delete the group '%s'?" % key) - _dialog.show() - - def removeFromRemainingList(self, contacts): - ListManager.removeFromRemainingList(self, contacts) - self._parent.updateContactList(contacts=contacts) - - def addToRemainingList(self, contacts, ignore_key=None): - ListManager.addToRemainingList(self, contacts, ignore_key) - self._parent.updateContactList(contacts=contacts) - - -class ContactGroupEditor(DockPanel): - """Panel for the contact groups manager.""" - - def __init__(self, host, parent=None, onCloseCallback=None): - DockPanel.__init__(self) - self.host = host - - # eventually display in a popup - if parent is None: - parent = DialogBox(autoHide=False, centered=True) - parent.setHTML("Manage contact groups") - self._parent = parent - self._on_close_callback = onCloseCallback - self.all_contacts = self.host.contact_panel.getContacts() - - groups_list = self.host.contact_panel.groups.keys() - groups_list.sort() - - self.add_group_panel = self.getAddGroupPanel(groups_list) - south_panel = self.getCloseSaveButtons() - center_panel = self.getContactGroupManager(groups_list) - east_panel = self.getContactList() - - self.add(self.add_group_panel, DockPanel.CENTER) - self.add(east_panel, DockPanel.EAST) - self.add(center_panel, DockPanel.NORTH) - self.add(south_panel, DockPanel.SOUTH) - - self.setCellHorizontalAlignment(center_panel, HasAlignment.ALIGN_LEFT) - self.setCellVerticalAlignment(center_panel, HasAlignment.ALIGN_TOP) - self.setCellHorizontalAlignment(east_panel, HasAlignment.ALIGN_RIGHT) - self.setCellVerticalAlignment(east_panel, HasAlignment.ALIGN_TOP) - self.setCellVerticalAlignment(self.add_group_panel, HasAlignment.ALIGN_BOTTOM) - self.setCellHorizontalAlignment(self.add_group_panel, HasAlignment.ALIGN_LEFT) - self.setCellVerticalAlignment(south_panel, HasAlignment.ALIGN_BOTTOM) - self.setCellHorizontalAlignment(south_panel, HasAlignment.ALIGN_CENTER) - - # need to be done after the contact list has been initialized - self.groups.setContacts(self.host.contact_panel.groups) - self.toggleContacts(showAll=True) - - # Hide the contacts list from the main panel to not confuse the user - self.restore_contact_panel = False - if self.host.contact_panel.getVisible(): - self.restore_contact_panel = True - self.host.panel._contactsSwitch() - - parent.add(self) - parent.setVisible(True) - if isinstance(parent, DialogBox): - parent.center() - - def getContactGroupManager(self, groups_list): - """Set the list manager for the groups""" - flex_table = FlexTable(len(groups_list), 2) - flex_table.addStyleName('contactGroupEditor') - # overwrite the default style which has been set for rich text editor - style = { - "keyItem": "group", - "popupMenuItem": "popupMenuItem", - "removeButton": "contactGroupRemoveButton", - "buttonCell": "contactGroupButtonCell", - "keyPanel": "contactGroupPanel" - } - self.groups = ContactGroupManager(flex_table, groups_list, self.all_contacts, style=style) - self.groups.createWidgets() # widgets are automatically added to FlexTable - # FIXME: clean that part which is dangerous - flex_table.updateContactList = self.updateContactList - flex_table.removeKeyFromAddGroupPanel = self.add_group_panel.groups.remove - return flex_table - - def getAddGroupPanel(self, groups_list): - """Add the 'Add group' panel to the FlexTable""" - - def add_group_cb(text): - self.groups.addContactKey(text) - self.add_group_panel.textbox.setFocus(True) - - add_group_panel = dialog.AddGroupPanel(groups_list, add_group_cb) - add_group_panel.addStyleName("addContactGroupPanel") - return add_group_panel - - def getCloseSaveButtons(self): - """Add the buttons to close the dialog / save the groups""" - buttons = HorizontalPanel() - buttons.addStyleName("marginAuto") - buttons.add(Button("Save", listener=self.closeAndSave)) - buttons.add(Button("Cancel", listener=self.cancelWithoutSaving)) - return buttons - - def getContactList(self): - """Add the contact list to the DockPanel""" - self.toggle = Button("", self.toggleContacts) - self.toggle.addStyleName("toggleAssignedContacts") - self.contacts = contact.GenericContactList(self.host) - for contact_ in self.all_contacts: - self.contacts.add(contact_) - contact_panel = VerticalPanel() - contact_panel.add(self.toggle) - contact_panel.add(self.contacts) - return contact_panel - - def toggleContacts(self, sender=None, showAll=None): - """Callback for the toggle button""" - if sender is None: - sender = self.toggle - sender.showAll = showAll if showAll is not None else not sender.showAll - if sender.showAll: - sender.setText("Hide assigned") - else: - sender.setText("Show assigned") - self.updateContactList(sender) - - def updateContactList(self, sender=None, contacts=None): - """Update the contact list regarding the toggle button""" - if not hasattr(self, "toggle") or not hasattr(self.toggle, "showAll"): - return - sender = self.toggle - if contacts is not None: - if not isinstance(contacts, list): - contacts = [contacts] - for contact_ in contacts: - if contact_ not in self.all_contacts: - contacts.remove(contact_) - else: - contacts = self.all_contacts - for contact_ in contacts: - if sender.showAll: - self.contacts.getContactLabel(contact_).setVisible(True) - else: - if contact_ in self.groups.remaining_list: - self.contacts.getContactLabel(contact_).setVisible(True) - else: - self.contacts.getContactLabel(contact_).setVisible(False) - - def __close(self): - """Remove the widget from parent or close the popup.""" - if isinstance(self._parent, DialogBox): - self._parent.hide() - self._parent.remove(self) - if self._on_close_callback is not None: - self._on_close_callback() - if self.restore_contact_panel: - self.host.panel._contactsSwitch() - - def cancelWithoutSaving(self): - """Ask for confirmation before closing the dialog.""" - def confirm_cb(answer): - if answer: - self.__close() - - _dialog = ConfirmDialog(confirm_cb, text="Do you really want to cancel without saving?") - _dialog.show() - - def closeAndSave(self): - """Call bridge methods to save the changes and close the dialog""" - map_ = {} - for contact_ in self.all_contacts: - map_[contact_] = set() - contacts = self.groups.getContacts() - for group in contacts.keys(): - for contact_ in contacts[group]: - try: - map_[contact_].add(group) - except KeyError: - InfoDialog("Invalid contact", - "The contact '%s' is not your contact list but it has been assigned to the group '%s'." % (contact_, group) + - "Your changes could not be saved: please check your assignments and save again.", Width="400px").center() - return - for contact_ in map_.keys(): - groups = map_[contact_] - current_groups = self.host.contact_panel.getContactGroups(contact_) - if groups != current_groups: - self.host.bridge.call('updateContact', None, contact_, '', list(groups)) - self.__close()
--- a/browser_side/dialog.py Fri May 16 11:51:10 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,546 +0,0 @@ -#!/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/>. - -from sat.core.log import getLogger -log = getLogger(__name__) -from pyjamas.ui.VerticalPanel import VerticalPanel -from pyjamas.ui.Grid import Grid -from pyjamas.ui.HorizontalPanel import HorizontalPanel -from pyjamas.ui.PopupPanel import PopupPanel -from pyjamas.ui.DialogBox import DialogBox -from pyjamas.ui.ListBox import ListBox -from pyjamas.ui.Button import Button -from pyjamas.ui.TextBox import TextBox -from pyjamas.ui.Label import Label -from pyjamas.ui.HTML import HTML -from pyjamas.ui.RadioButton import RadioButton -from pyjamas.ui import HasAlignment -from pyjamas.ui.KeyboardListener import KEY_ESCAPE, KEY_ENTER -from pyjamas.ui.MouseListener import MouseWheelHandler -from pyjamas import Window - -from base_panels import ToggleStackPanel - -from sat_frontends.tools.misc import DEFAULT_MUC - -# List here the patterns that are not allowed in contact group names -FORBIDDEN_PATTERNS_IN_GROUP = () - - -class RoomChooser(Grid): - """Select a room from the rooms you already joined, or create a new one""" - - GENERATE_MUC = "<use random name>" - - def __init__(self, host, default_room=DEFAULT_MUC): - Grid.__init__(self, 2, 2, Width='100%') - self.host = host - - self.new_radio = RadioButton("room", "Discussion room:") - self.new_radio.setChecked(True) - self.box = TextBox(Width='95%') - self.box.setText(self.GENERATE_MUC if default_room == "" else default_room) - self.exist_radio = RadioButton("room", "Already joined:") - self.rooms_list = ListBox(Width='95%') - - self.add(self.new_radio, 0, 0) - self.add(self.box, 0, 1) - self.add(self.exist_radio, 1, 0) - self.add(self.rooms_list, 1, 1) - - self.box.addFocusListener(self) - self.rooms_list.addFocusListener(self) - - self.exist_radio.setVisible(False) - self.rooms_list.setVisible(False) - self.setRooms() - - def onFocus(self, sender): - if sender == self.rooms_list: - self.exist_radio.setChecked(True) - elif sender == self.box: - if self.box.getText() == self.GENERATE_MUC: - self.box.setText("") - self.new_radio.setChecked(True) - - def onLostFocus(self, sender): - if sender == self.box: - if self.box.getText() == "": - self.box.setText(self.GENERATE_MUC) - - def setRooms(self): - for room in self.host.room_list: - self.rooms_list.addItem(room.bare) - if len(self.host.room_list) > 0: - self.exist_radio.setVisible(True) - self.rooms_list.setVisible(True) - self.exist_radio.setChecked(True) - - def getRoom(self): - if self.exist_radio.getChecked(): - values = self.rooms_list.getSelectedValues() - return "" if values == [] else values[0] - value = self.box.getText() - return "" if value == self.GENERATE_MUC else value - - -class ContactsChooser(VerticalPanel): - """Select one or several connected contacts""" - - def __init__(self, host, nb_contact=None, ok_button=None): - """ - @param host: SatWebFrontend instance - @param nb_contact: number of contacts that have to be selected, None for no limit - If a tuple is given instead of an integer, nb_contact[0] is the minimal and - nb_contact[1] is the maximal number of contacts to be chosen. - """ - self.host = host - if isinstance(nb_contact, tuple): - if len(nb_contact) == 0: - nb_contact = None - elif len(nb_contact) == 1: - nb_contact = (nb_contact[0], nb_contact[0]) - elif nb_contact is not None: - nb_contact = (nb_contact, nb_contact) - if nb_contact is None: - log.warning("Need to select as many contacts as you want") - else: - log.warning("Need to select between %d and %d contacts" % nb_contact) - self.nb_contact = nb_contact - self.ok_button = ok_button - VerticalPanel.__init__(self, Width='100%') - self.contacts_list = ListBox() - self.contacts_list.setMultipleSelect(True) - self.contacts_list.setWidth("95%") - self.contacts_list.addStyleName('contactsChooser') - self.contacts_list.addChangeListener(self.onChange) - self.add(self.contacts_list) - self.setContacts() - self.onChange() - - def onChange(self, sender=None): - if self.ok_button is None: - return - if self.nb_contact: - selected = len(self.contacts_list.getSelectedValues(True)) - if selected >= self.nb_contact[0] and selected <= self.nb_contact[1]: - self.ok_button.setEnabled(True) - else: - self.ok_button.setEnabled(False) - - def setContacts(self, selected=[]): - """Fill the list with the connected contacts - @param select: list of the contacts to select by default - """ - self.contacts_list.clear() - contacts = self.host.contact_panel.getConnected(filter_muc=True) - self.contacts_list.setVisibleItemCount(10 if len(contacts) > 5 else 5) - self.contacts_list.addItem("") - for contact in contacts: - if contact not in [room.bare for room in self.host.room_list]: - self.contacts_list.addItem(contact) - self.contacts_list.setItemTextSelection(selected) - - def getContacts(self): - return self.contacts_list.getSelectedValues(True) - - -class RoomAndContactsChooser(DialogBox): - """Select a room and some users to invite in""" - - def __init__(self, host, callback, nb_contact=None, ok_button="OK", title="Discussion groups", - title_room="Join room", title_invite="Invite contacts", visible=(True, True)): - DialogBox.__init__(self, centered=True) - self.host = host - self.callback = callback - self.title_room = title_room - self.title_invite = title_invite - - button_panel = HorizontalPanel() - button_panel.addStyleName("marginAuto") - ok_button = Button("OK", self.onOK) - button_panel.add(ok_button) - button_panel.add(Button("Cancel", self.onCancel)) - - self.room_panel = RoomChooser(host, "" if visible == (False, True) else DEFAULT_MUC) - self.contact_panel = ContactsChooser(host, nb_contact, ok_button) - - self.stack_panel = ToggleStackPanel(Width="100%") - self.stack_panel.add(self.room_panel, visible=visible[0]) - self.stack_panel.add(self.contact_panel, visible=visible[1]) - self.stack_panel.addStackChangeListener(self) - self.onStackChanged(self.stack_panel, 0, visible[0]) - self.onStackChanged(self.stack_panel, 1, visible[1]) - - main_panel = VerticalPanel() - main_panel.setStyleName("room-contact-chooser") - main_panel.add(self.stack_panel) - main_panel.add(button_panel) - - self.setWidget(main_panel) - self.setHTML(title) - self.show() - - # needed to update the contacts list when someone logged in/out - self.host.room_contacts_chooser = self - - def getRoom(self, asSuffix=False): - room = self.room_panel.getRoom() - if asSuffix: - return room if room == "" else ": %s" % room - else: - return room - - def getContacts(self, asSuffix=False): - contacts = self.contact_panel.getContacts() - if asSuffix: - return "" if contacts == [] else ": %s" % ", ".join(contacts) - else: - return contacts - - def onStackChanged(self, sender, index, visible=None): - if visible is None: - visible = sender.getWidget(index).getVisible() - if index == 0: - sender.setStackText(0, self.title_room + ("" if visible else self.getRoom(True))) - elif index == 1: - sender.setStackText(1, self.title_invite + ("" if visible else self.getContacts(True))) - - def resetContacts(self): - """Called when someone log in/out to update the list""" - self.contact_panel.setContacts(self.getContacts()) - - def onOK(self, sender): - room_jid = self.getRoom() - if room_jid != "" and "@" not in room_jid: - Window.alert('You must enter a room jid in the form room@chat.%s' % self.host._defaultDomain) - return - self.hide() - self.callback(room_jid, self.getContacts()) - - def onCancel(self, sender): - self.hide() - - def hide(self): - self.host.room_contacts_chooser = None - DialogBox.hide(self, autoClosed=True) - - -class GenericConfirmDialog(DialogBox): - - def __init__(self, widgets, callback, title='Confirmation', prompt=None, **kwargs): - """ - Dialog to confirm an action - @param widgets: widgets to attach - @param callback: method to call when a button is clicked - @param title: title of the dialog - @param prompt: textbox from which to retrieve the string value to be passed to the callback when - OK button is pressed. If None, OK button will return "True". Cancel button always returns "False". - """ - self.callback = callback - DialogBox.__init__(self, centered=True, **kwargs) - - content = VerticalPanel() - content.setWidth('100%') - for wid in widgets: - content.add(wid) - if wid == prompt: - wid.setWidth('100%') - button_panel = HorizontalPanel() - button_panel.addStyleName("marginAuto") - self.confirm_button = Button("OK", self.onConfirm) - button_panel.add(self.confirm_button) - self.cancel_button = Button("Cancel", self.onCancel) - button_panel.add(self.cancel_button) - content.add(button_panel) - self.setHTML(title) - self.setWidget(content) - self.prompt = prompt - - def onConfirm(self, sender): - self.hide() - self.callback(self.prompt.getText() if self.prompt else True) - - def onCancel(self, sender): - self.hide() - self.callback(False) - - def show(self): - DialogBox.show(self) - if self.prompt: - self.prompt.setFocus(True) - - -class ConfirmDialog(GenericConfirmDialog): - - def __init__(self, callback, text='Are you sure ?', title='Confirmation', **kwargs): - GenericConfirmDialog.__init__(self, [HTML(text)], callback, title, **kwargs) - - -class GenericDialog(DialogBox): - """Dialog which just show a widget and a close button""" - - def __init__(self, title, main_widget, callback=None, options=None, **kwargs): - """Simple notice dialog box - @param title: HTML put in the header - @param main_widget: widget put in the body - @param callback: method to call on closing - @param options: one or more of the following options: - - NO_CLOSE: don't add a close button""" - DialogBox.__init__(self, centered=True, **kwargs) - self.callback = callback - if not options: - options = [] - _body = VerticalPanel() - _body.setSize('100%','100%') - _body.setSpacing(4) - _body.add(main_widget) - _body.setCellWidth(main_widget, '100%') - _body.setCellHeight(main_widget, '100%') - if not 'NO_CLOSE' in options: - _close_button = Button("Close", self.onClose) - _body.add(_close_button) - _body.setCellHorizontalAlignment(_close_button, HasAlignment.ALIGN_CENTER) - self.setHTML(title) - self.setWidget(_body) - self.panel.setSize('100%', '100%') #Need this hack to have correct size in Gecko & Webkit - - def close(self): - """Same effect as clicking the close button""" - self.onClose(None) - - def onClose(self, sender): - self.hide() - if self.callback: - self.callback() - - -class InfoDialog(GenericDialog): - - def __init__(self, title, body, callback=None, options=None, **kwargs): - GenericDialog.__init__(self, title, HTML(body), callback, options, **kwargs) - - -class PromptDialog(GenericConfirmDialog): - - def __init__(self, callback, text='', title='User input', **kwargs): - prompt = TextBox() - prompt.setText(text) - GenericConfirmDialog.__init__(self, [prompt], callback, title, prompt, **kwargs) - - -class PopupPanelWrapper(PopupPanel): - """This wrapper catch Escape event to avoid request cancellation by Firefox""" - - def onEventPreview(self, event): - if event.type in ["keydown", "keypress", "keyup"] and event.keyCode == KEY_ESCAPE: - #needed to prevent request cancellation in Firefox - event.preventDefault() - return PopupPanel.onEventPreview(self, event) - - -class ExtTextBox(TextBox): - """Extended TextBox""" - - def __init__(self, *args, **kwargs): - if 'enter_cb' in kwargs: - self.enter_cb = kwargs['enter_cb'] - del kwargs['enter_cb'] - TextBox.__init__(self, *args, **kwargs) - self.addKeyboardListener(self) - - def onKeyUp(self, sender, keycode, modifiers): - pass - - def onKeyDown(self, sender, keycode, modifiers): - pass - - def onKeyPress(self, sender, keycode, modifiers): - if self.enter_cb and keycode == KEY_ENTER: - self.enter_cb(self) - - -class GroupSelector(DialogBox): - - def __init__(self, top_widgets, initial_groups, selected_groups, - ok_title="OK", ok_cb=None, cancel_cb=None): - DialogBox.__init__(self, centered=True) - main_panel = VerticalPanel() - self.ok_cb = ok_cb - self.cancel_cb = cancel_cb - - for wid in top_widgets: - main_panel.add(wid) - - main_panel.add(Label('Select in which groups your contact is:')) - self.list_box = ListBox() - self.list_box.setMultipleSelect(True) - self.list_box.setVisibleItemCount(5) - self.setAvailableGroups(initial_groups) - self.setGroupsSelected(selected_groups) - main_panel.add(self.list_box) - - def cb(text): - self.list_box.addItem(text) - self.list_box.setItemSelected(self.list_box.getItemCount() - 1, "selected") - - main_panel.add(AddGroupPanel(initial_groups, cb)) - - button_panel = HorizontalPanel() - button_panel.addStyleName("marginAuto") - button_panel.add(Button(ok_title, self.onOK)) - button_panel.add(Button("Cancel", self.onCancel)) - main_panel.add(button_panel) - - self.setWidget(main_panel) - - def getSelectedGroups(self): - """Return a list of selected groups""" - return self.list_box.getSelectedValues() - - def setAvailableGroups(self, groups): - _groups = list(set(groups)) - _groups.sort() - self.list_box.clear() - for group in _groups: - self.list_box.addItem(group) - - def setGroupsSelected(self, selected_groups): - self.list_box.setItemTextSelection(selected_groups) - - def onOK(self, sender): - self.hide() - if self.ok_cb: - self.ok_cb(self) - - def onCancel(self, sender): - self.hide() - if self.cancel_cb: - self.cancel_cb(self) - - -class AddGroupPanel(HorizontalPanel): - def __init__(self, groups, cb=None): - """ - @param groups: list of the already existing groups - """ - HorizontalPanel.__init__(self) - self.groups = groups - self.add(Label('Add group:')) - self.textbox = ExtTextBox(enter_cb=self.onGroupInput) - self.add(self.textbox) - self.add(Button("add", lambda sender: self.onGroupInput(self.textbox))) - self.cb = cb - - def onGroupInput(self, sender): - text = sender.getText() - if text == "": - return - for group in self.groups: - if text == group: - Window.alert("The group '%s' already exists." % text) - return - for pattern in FORBIDDEN_PATTERNS_IN_GROUP: - if pattern in text: - Window.alert("The pattern '%s' is not allowed in group names." % pattern) - return - sender.setText('') - self.groups.append(text) - if self.cb is not None: - self.cb(text) - - -class WheelTextBox(TextBox, MouseWheelHandler): - - def __init__(self, *args, **kwargs): - TextBox.__init__(self, *args, **kwargs) - MouseWheelHandler.__init__(self) - - -class IntSetter(HorizontalPanel): - """This class show a bar with button to set an int value""" - - def __init__(self, label, value=0, value_max=None, visible_len=3): - """initialize the intSetter - @param label: text shown in front of the setter - @param value: initial value - @param value_max: limit value, None or 0 for unlimited""" - HorizontalPanel.__init__(self) - self.value = value - self.value_max = value_max - _label = Label(label) - self.add(_label) - self.setCellWidth(_label, "100%") - minus_button = Button("-", self.onMinus) - self.box = WheelTextBox() - self.box.setVisibleLength(visible_len) - self.box.setText(str(value)) - self.box.addInputListener(self) - self.box.addMouseWheelListener(self) - plus_button = Button("+", self.onPlus) - self.add(minus_button) - self.add(self.box) - self.add(plus_button) - self.valueChangedListener = [] - - def addValueChangeListener(self, listener): - self.valueChangedListener.append(listener) - - def removeValueChangeListener(self, listener): - if listener in self.valueChangedListener: - self.valueChangedListener.remove(listener) - - def _callListeners(self): - for listener in self.valueChangedListener: - listener(self.value) - - def setValue(self, value): - """Change the value and fire valueChange listeners""" - self.value = value - self.box.setText(str(value)) - self._callListeners() - - def onMinus(self, sender, step=1): - self.value = max(0, self.value - step) - self.box.setText(str(self.value)) - self._callListeners() - - def onPlus(self, sender, step=1): - self.value += step - if self.value_max: - self.value = min(self.value, self.value_max) - self.box.setText(str(self.value)) - self._callListeners() - - def onInput(self, sender): - """Accept only valid integer && normalize print (no leading 0)""" - try: - self.value = int(self.box.getText()) if self.box.getText() else 0 - except ValueError: - pass - if self.value_max: - self.value = min(self.value, self.value_max) - self.box.setText(str(self.value)) - self._callListeners() - - def onMouseWheel(self, sender, velocity): - if velocity > 0: - self.onMinus(sender, 10) - else: - self.onPlus(sender, 10)
--- a/browser_side/file_tools.py Fri May 16 11:51:10 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,148 +0,0 @@ -#!/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/>. - -from sat.core.log import getLogger -log = getLogger(__name__) -from pyjamas.ui.FileUpload import FileUpload -from pyjamas.ui.FormPanel import FormPanel -from pyjamas import Window -from pyjamas import DOM -from pyjamas.ui.VerticalPanel import VerticalPanel -from pyjamas.ui.HTML import HTML -from pyjamas.ui.HorizontalPanel import HorizontalPanel -from pyjamas.ui.Button import Button -from pyjamas.ui.Label import Label - - -class FilterFileUpload(FileUpload): - - def __init__(self, name, max_size, types=None): - """ - @param name: the input element name and id - @param max_size: maximum file size in MB - @param types: allowed types as a list of couples (x, y, z): - - x: MIME content type e.g. "audio/ogg" - - y: file extension e.g. "*.ogg" - - z: description for the user e.g. "Ogg Vorbis Audio" - If types is None, all file format are accepted - """ - FileUpload.__init__(self) - self.setName(name) - while DOM.getElementById(name): - name = "%s_" % name - self.setID(name) - self._id = name - self.max_size = max_size - self.types = types - - def getFileInfo(self): - from __pyjamas__ import JS - JS("var file = top.document.getElementById(this._id).files[0]; return [file.size, file.type]") - - def check(self): - if self.getFilename() == "": - return False - (size, filetype) = self.getFileInfo() - if self.types and filetype not in [x for (x, y, z) in self.types]: - types = [] - for type_ in ["- %s (%s)" % (z, y) for (x, y, z) in self.types]: - if type_ not in types: - types.append(type_) - Window.alert('This file type is not accepted.\nAccepted file types are:\n\n%s' % "\n".join(types)) - return False - if size > self.max_size * pow(2, 20): - Window.alert('This file is too big!\nMaximum file size: %d MB.' % self.max_size) - return False - return True - - -class FileUploadPanel(FormPanel): - - def __init__(self, action_url, input_id, max_size, texts=None, close_cb=None): - """Build a form panel to upload a file. - @param action_url: the form action URL - @param input_id: the input element name and id - @param max_size: maximum file size in MB - @param texts: a dict to ovewrite the default textual values - @param close_cb: the close button callback method - """ - FormPanel.__init__(self) - self.texts = {'ok_button': 'Upload file', - 'cancel_button': 'Cancel', - 'body': 'Please select a file.', - 'submitting': '<strong>Submitting, please wait...</strong>', - 'errback': "Your file has been rejected...", - 'body_errback': 'Please select another file.', - 'callback': "Your file has been accepted!"} - if isinstance(texts, dict): - self.texts.update(texts) - self.close_cb = close_cb - self.setEncoding(FormPanel.ENCODING_MULTIPART) - self.setMethod(FormPanel.METHOD_POST) - self.setAction(action_url) - self.vPanel = VerticalPanel() - self.message = HTML(self.texts['body']) - self.vPanel.add(self.message) - - hPanel = HorizontalPanel() - hPanel.setSpacing(5) - hPanel.setStyleName('marginAuto') - self.file_upload = FilterFileUpload(input_id, max_size) - self.vPanel.add(self.file_upload) - - self.upload_btn = Button(self.texts['ok_button'], getattr(self, "onSubmitBtnClick")) - hPanel.add(self.upload_btn) - hPanel.add(Button(self.texts['cancel_button'], getattr(self, "onCloseBtnClick"))) - - self.status = Label() - hPanel.add(self.status) - - self.vPanel.add(hPanel) - - self.add(self.vPanel) - self.addFormHandler(self) - - def setCloseCb(self, close_cb): - self.close_cb = close_cb - - def onCloseBtnClick(self): - if self.close_cb: - self.close_cb() - else: - log.warning("no close method defined") - - def onSubmitBtnClick(self): - if not self.file_upload.check(): - return - self.message.setHTML(self.texts['submitting']) - self.upload_btn.setEnabled(False) - self.submit() - - def onSubmit(self, event): - pass - - def onSubmitComplete(self, event): - result = event.getResults() - if result != "OK": - Window.alert(self.texts['errback']) - self.message.setHTML(self.texts['body_errback']) - self.upload_btn.setEnabled(True) - else: - Window.alert(self.texts['callback']) - self.close_cb()
--- a/browser_side/html_tools.py Fri May 16 11:51:10 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,46 +0,0 @@ -#!/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/>. - -from nativedom import NativeDOM -from sat_frontends.tools import xmltools -import re - -dom = NativeDOM() - - -def html_sanitize(html): - """Naive sanitization of HTML""" - return html.replace('<', '<').replace('>', '>') - - -def html_strip(html): - """Strip leading/trailing white spaces, HTML line breaks and sequences.""" - cleaned = re.sub(r"^(<br/?>| |\s)+", "", html) - cleaned = re.sub(r"(<br/?>| |\s)+$", "", cleaned) - return cleaned - - -def inlineRoot(xhtml): - """ make root element inline """ - doc = dom.parseString(xhtml) - return xmltools.inlineRoot(doc) - - -def convertNewLinesToXHTML(text): - return text.replace('\n', '<br/>')
--- a/browser_side/jid.py Fri May 16 11:51:10 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,54 +0,0 @@ -#!/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/>. - - -class JID(object): - """This class help manage JID (Node@Domaine/Resource)""" - - def __init__(self, jid): - self.__raw = str(jid) - self.__parse() - - def __parse(self): - """find node domaine and resource""" - node_end = self.__raw.find('@') - if node_end < 0: - node_end = 0 - domain_end = self.__raw.find('/') - if domain_end < 1: - domain_end = len(self.__raw) - self.node = self.__raw[:node_end] - self.domain = self.__raw[(node_end + 1) if node_end else 0:domain_end] - self.resource = self.__raw[domain_end + 1:] - if not node_end: - self.bare = self.__raw - else: - self.bare = self.node + '@' + self.domain - - def __str__(self): - return self.__raw.__str__() - - def is_valid(self): - """return True if the jid is xmpp compliant""" - #FIXME: always return True for the moment - return True - - def __eq__(self, other): - """Redefine equality operator to implement the naturally expected test""" - return self.node == other.node and self.domain == other.domain and self.resource == other.resource
--- a/browser_side/list_manager.py Fri May 16 11:51:10 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,608 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2013, 2014 Adrien Cossa <souliane@mailoo.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/>. - -from sat.core.log import getLogger -log = getLogger(__name__) -from pyjamas.ui.Grid import Grid -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.Label import Label -from pyjamas.ui.HorizontalPanel import HorizontalPanel -from pyjamas.ui.VerticalPanel import VerticalPanel -from pyjamas.ui.DialogBox import DialogBox -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.Timer import Timer -from pyjamas import DOM - -from base_panels import PopupMenuPanel -from base_widget import DragLabel - -# HTML content for the removal button (image or text) -REMOVE_BUTTON = '<span class="recipientRemoveIcon">x</span>' - -# Item to be considered for an empty list box selection. -# Could be whatever which doesn't look like a JID or a group name. -EMPTY_SELECTION_ITEM = "" - - -class ListManager(): - """A manager for sub-panels to assign elements to lists.""" - - def __init__(self, parent, keys_dict={}, contact_list=[], offsets={}, style={}): - """ - @param parent: FlexTable parent widget for the manager - @param keys_dict: dict with the contact keys mapped to data - @param contact_list: list of string (the contact JID userhosts) - @param offsets: dict to set widget positions offset within parent - - "x_first": the x offset for the first widget's row on the grid - - "x": the x offset for all widgets rows, except the first one if "x_first" is defined - - "y": the y offset for all widgets columns on the grid - """ - self._parent = parent - if isinstance(keys_dict, set) or isinstance(keys_dict, list): - tmp = {} - for key in keys_dict: - tmp[key] = {} - keys_dict = tmp - self.__keys_dict = keys_dict - if isinstance(contact_list, set): - contact_list = list(contact_list) - self.__list = contact_list - self.__list.sort() - # store the list of contacts that are not assigned yet - self.__remaining_list = [] - self.__remaining_list.extend(self.__list) - # mark a change to sort the list before it's used - self.__remaining_list_sorted = True - - self.offsets = {"x_first": 0, "x": 0, "y": 0} - if "x" in offsets and not "x_first" in offsets: - offsets["x_first"] = offsets["x"] - self.offsets.update(offsets) - - self.style = { - "keyItem": "recipientTypeItem", - "popupMenuItem": "recipientTypeItem", - "buttonCell": "recipientButtonCell", - "dragoverPanel": "dragover-recipientPanel", - "keyPanel": "recipientPanel", - "textBox": "recipientTextBox", - "textBox-invalid": "recipientTextBox-invalid", - "removeButton": "recipientRemoveButton", - } - self.style.update(style) - - def createWidgets(self, title_format="%s"): - """Fill the parent grid with all the widgets (some may be hidden during the initialization).""" - self.__children = {} - for key in self.__keys_dict: - self.addContactKey(key, title_format=title_format) - - def addContactKey(self, key, dict_={}, title_format="%s"): - if key not in self.__keys_dict: - self.__keys_dict[key] = dict_ - # copy the key to its associated sub-map - self.__keys_dict[key]["title"] = key - self._addChild(self.__keys_dict[key], title_format) - - def removeContactKey(self, key): - """Remove a list panel and all its associated data.""" - contacts = self.__children[key]["panel"].getContacts() - (y, x) = self._parent.getIndex(self.__children[key]["button"]) - self._parent.removeRow(y) - del self.__children[key] - del self.__keys_dict[key] - self.addToRemainingList(contacts) - - def _addChild(self, entry, title_format): - """Add a button and FlowPanel for the corresponding map entry.""" - button = Button(title_format % entry["title"]) - button.setStyleName(self.style["keyItem"]) - if hasattr(entry, "desc"): - button.setTitle(entry["desc"]) - if not "optional" in entry: - entry["optional"] = False - button.setVisible(not entry["optional"]) - y = len(self.__children) + self.offsets["y"] - x = self.offsets["x_first"] if y == self.offsets["y"] else self.offsets["x"] - - self._parent.insertRow(y) - self._parent.setWidget(y, x, button) - self._parent.getCellFormatter().setStyleName(y, x, self.style["buttonCell"]) - - _child = ListPanel(self, entry, self.style) - self._parent.setWidget(y, x + 1, _child) - - self.__children[entry["title"]] = {} - self.__children[entry["title"]]["button"] = button - self.__children[entry["title"]]["panel"] = _child - - if hasattr(self, "popup_menu"): - # this is done if self.registerPopupMenuPanel has been called yet - self.popup_menu.registerClickSender(button) - - def _refresh(self, visible=True): - """Set visible the sub-panels that are non optional or non empty, hide the rest.""" - for key in self.__children: - self.setContactPanelVisible(key, False) - if not visible: - return - _map = self.getContacts() - for key in _map: - if len(_map[key]) > 0 or not self.__keys_dict[key]["optional"]: - self.setContactPanelVisible(key, True) - - def setVisible(self, visible): - self._refresh(visible) - - def setContactPanelVisible(self, key, visible=True, sender=None): - """Do not remove the "sender" param as it is needed for the context menu.""" - self.__children[key]["button"].setVisible(visible) - self.__children[key]["panel"].setVisible(visible) - - @property - def list(self): - """Return the full list of potential contacts.""" - return self.__list - - @property - def keys(self): - return self.__keys_dict.keys() - - @property - def keys_dict(self): - return self.__keys_dict - - @property - def remaining_list(self): - """Return the contacts that have not been selected yet.""" - if not self.__remaining_list_sorted: - self.__remaining_list_sorted = True - self.__remaining_list.sort() - return self.__remaining_list - - def setRemainingListUnsorted(self): - """Mark a change (deletion) so the list will be sorted before it's used.""" - self.__remaining_list_sorted = False - - def removeFromRemainingList(self, contacts): - """Remove contacts after they have been added to a sub-panel.""" - if not isinstance(contacts, list): - contacts = [contacts] - for contact_ in contacts: - if contact_ in self.__remaining_list: - self.__remaining_list.remove(contact_) - - def addToRemainingList(self, contacts, ignore_key=None): - """Add contacts after they have been removed from a sub-panel.""" - if not isinstance(contacts, list): - contacts = [contacts] - assigned_contacts = set() - assigned_map = self.getContacts() - for key_ in assigned_map.keys(): - if ignore_key is not None and key_ == ignore_key: - continue - assigned_contacts.update(assigned_map[key_]) - for contact_ in contacts: - if contact_ not in self.__list or contact_ in self.__remaining_list: - continue - if contact_ in assigned_contacts: - continue # the contact is assigned somewhere else - self.__remaining_list.append(contact_) - self.setRemainingListUnsorted() - - def setContacts(self, _map={}): - """Set the contacts for each contact key.""" - for key in self.__keys_dict: - if key in _map: - self.__children[key]["panel"].setContacts(_map[key]) - else: - self.__children[key]["panel"].setContacts([]) - self._refresh() - - def getContacts(self): - """Get the contacts for all the lists. - @return: a mapping between keys and contact lists.""" - _map = {} - for key in self.__children: - _map[key] = self.__children[key]["panel"].getContacts() - return _map - - @property - def target_drop_cell(self): - """@return: the panel where something has been dropped.""" - return self._target_drop_cell - - def setTargetDropCell(self, target_drop_cell): - """@param: target_drop_cell: the panel where something has been dropped.""" - self._target_drop_cell = target_drop_cell - - def registerPopupMenuPanel(self, entries, hide, callback): - "Register a popup menu panel that will be bound to all contact keys elements." - self.popup_menu = PopupMenuPanel(entries=entries, hide=hide, callback=callback, style={"item": self.style["popupMenuItem"]}) - - -class DragAutoCompleteTextBox(AutoCompleteTextBox, DragLabel, MouseHandler, FocusHandler): - """A draggable AutoCompleteTextBox which is used for representing a contact. - This class is NOT generic because of the onDragEnd method which call methods - from ListPanel. It's probably not reusable for another scenario. - """ - - def __init__(self, parent, event_cbs, style): - AutoCompleteTextBox.__init__(self) - DragLabel.__init__(self, '', 'CONTACT_TEXTBOX') # The group prefix "@" is already in text so we use only the "CONTACT_TEXTBOX" type - self._parent = parent - self.event_cbs = event_cbs - self.style = style - self.addMouseListener(self) - self.addFocusListener(self) - self.addChangeListener(self) - self.addStyleName(style["textBox"]) - self.reset() - - def reset(self): - self.setText("") - self.setValid() - - def setValid(self, valid=True): - if self.getText() == "": - valid = True - if valid: - self.removeStyleName(self.style["textBox-invalid"]) - else: - self.addStyleName(self.style["textBox-invalid"]) - self.valid = valid - - def onDragStart(self, event): - self._text = self.getText() - DragLabel.onDragStart(self, event) - self._parent.setTargetDropCell(None) - self.setSelectionRange(len(self.getText()), 0) - - def onDragEnd(self, event): - target = self._parent.target_drop_cell # parent or another ListPanel - if self.getText() == "" or target is None: - return - self.event_cbs["drop"](self, target) - - def setRemoveButton(self): - - def remove_cb(sender): - """Callback for the button to remove this contact.""" - self._parent.remove(self) - self._parent.remove(self.remove_btn) - self.event_cbs["remove"](self) - - self.remove_btn = Button(REMOVE_BUTTON, remove_cb, Visible=False) - self.remove_btn.setStyleName(self.style["removeButton"]) - self._parent.add(self.remove_btn) - - def removeOrReset(self): - 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 - lisibility, but it's probably not reusable for another scenario. - """ - - def __init__(self, drop_cbs): - 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): - """Sub-panel used for each contact key. Beware that pyjamas.ui.FlowPanel - is not fully implemented yet and can not be used with pyjamas.ui.Label.""" - - def __init__(self, parent, entry, style={}): - """Initialization with a button and a DragAutoCompleteTextBox.""" - FlowPanel.__init__(self, Visible=(False if entry["optional"] else True)) - drop_cbs = {"GROUP": lambda panel, item: self.addContact("@%s" % item), - "CONTACT": lambda panel, item: self.addContact(item), - "CONTACT_TITLE": lambda panel, item: self.addContact('@@'), - "CONTACT_TEXTBOX": lambda panel, item: self.setTargetDropCell(panel) - } - DropCell.__init__(self, drop_cbs) - self.style = style - self.addStyleName(self.style["keyPanel"]) - self._parent = parent - self.key = entry["title"] - self._addTextBox() - - def _addTextBox(self, switchPrevious=False): - """Add a text box to the last position. If switchPrevious is True, simulate - an insertion before the current last textbox by copying the text and valid state. - @return: the created textbox or the previous one if switchPrevious is True. - """ - if hasattr(self, "_last_textbox"): - if self._last_textbox.getText() == "": - return - self._last_textbox.setRemoveButton() - else: - switchPrevious = False - - def focus_cb(sender): - if sender != self._last_textbox: - # save the current value before it's being modified - self._parent.addToRemainingList(sender.getText(), ignore_key=self.key) - sender.setCompletionItems(self._parent.remaining_list) - - def remove_cb(sender): - """Callback for the button to remove this contact.""" - self._parent.addToRemainingList(sender.getText()) - self._parent.setRemainingListUnsorted() - self._last_textbox.setFocus(True) - - def drop_cb(sender, target): - """Callback when the textbox is drag-n-dropped.""" - parent = sender._parent - if target != parent and target.addContact(sender.getText()): - sender.removeOrReset() - else: - parent._parent.removeFromRemainingList(sender.getText()) - - events_cbs = {"focus": focus_cb, "validate": self.addContact, "remove": remove_cb, "drop": drop_cb} - textbox = DragAutoCompleteTextBox(self, events_cbs, self.style) - self.add(textbox) - if switchPrevious: - textbox.setText(self._last_textbox.getText()) - textbox.setValid(self._last_textbox.valid) - self._last_textbox.reset() - previous = self._last_textbox - self._last_textbox = textbox - return previous if switchPrevious else textbox - - def _checkContact(self, contact, modify): - """ - @param contact: the contact to check - @param modify: True if the contact is being modified - @return: - - VALID if the contact is valid - - INVALID if the contact is not valid but can be displayed - - DELETE if the contact should not be displayed at all - """ - def countItemInList(list_, item): - """For some reason the built-in count function doesn't work...""" - count = 0 - for elem in list_: - if elem == item: - count += 1 - return count - if contact is None or contact == "": - return DELETE - if countItemInList(self.getContacts(), contact) > (1 if modify else 0): - return DELETE - return VALID if contact in self._parent.list else INVALID - - def addContact(self, contact, sender=None): - """The first parameter type is checked, so it is also possible to call addContact(sender). - If contact is not defined, sender.getText() is used. If sender is not defined, contact will - be written to the last textbox and a new textbox is added afterward. - @param contact: unicode - @param sender: DragAutoCompleteTextBox instance - """ - if isinstance(contact, DragAutoCompleteTextBox): - sender = contact - contact = sender.getText() - valid = self._checkContact(contact, sender is not None) - if sender is None: - # method has been called to modify but to add a contact - if valid == VALID: - # eventually insert before the last textbox if it's not empty - sender = self._addTextBox(True) if self._last_textbox.getText() != "" else self._last_textbox - sender.setText(contact) - else: - sender.setValid(valid == VALID) - if valid != VALID: - if sender is not None and valid == DELETE: - sender.removeOrReset() - return False - if sender == self._last_textbox: - self._addTextBox() - try: - sender.setVisibleLength(len(contact)) - except: - # IndexSizeError: Index or size is negative or greater than the allowed amount - log.warning("FIXME: len(%s) returns %d... javascript bug?" % (contact, len(contact))) - self._parent.removeFromRemainingList(contact) - self._last_textbox.setFocus(True) - return True - - def emptyContacts(self): - """Empty the list of contacts.""" - for child in self.getChildren(): - if hasattr(child, "remove_btn"): - child.remove_btn.click() - - def setContacts(self, tab): - """Set the contacts.""" - self.emptyContacts() - if isinstance(tab, set): - tab = list(tab) - tab.sort() - for contact in tab: - self.addContact(contact) - - def getContacts(self): - """Get the contacts - @return: an array of string""" - tab = [] - for widget in self.getChildren(): - if isinstance(widget, DragAutoCompleteTextBox): - # not to be mixed with EMPTY_SELECTION_ITEM - if widget.getText() != "": - tab.append(widget.getText()) - return tab - - @property - def target_drop_cell(self): - """@return: the panel where something has been dropped.""" - return self._parent.target_drop_cell - - def setTargetDropCell(self, target_drop_cell): - """ - XXX: Property setter here would not make it, you need a proper method! - @param target_drop_cell: the panel where something has been dropped.""" - self._parent.setTargetDropCell(target_drop_cell) - - -class ContactChooserPanel(DialogBox): - """Display the contacts chooser dialog. This has been implemented while - prototyping and is currently not used. Left for an eventual later use. - Replaced by the popup menu which allows to add a panel for Cc or Bcc. - """ - - def __init__(self, manager, **kwargs): - """Display a listbox for each contact key""" - DialogBox.__init__(self, autoHide=False, centered=True, **kwargs) - self.setHTML("Select contacts") - self.manager = manager - self.listboxes = {} - self.contacts = manager.getContacts() - - container = VerticalPanel(Visible=True) - container.addStyleName("marginAuto") - - grid = Grid(2, len(self.manager.keys_dict)) - index = -1 - for key in self.manager.keys_dict: - index += 1 - grid.add(Label("%s:" % self.manager.keys_dict[key]["desc"]), 0, index) - listbox = ListBox() - listbox.setMultipleSelect(True) - listbox.setVisibleItemCount(15) - listbox.addItem(EMPTY_SELECTION_ITEM) - for element in manager.list: - listbox.addItem(element) - self.listboxes[key] = listbox - grid.add(listbox, 1, index) - self._reset() - - buttons = HorizontalPanel() - buttons.addStyleName("marginAuto") - btn_close = Button("Cancel", self.hide) - buttons.add(btn_close) - btn_reset = Button("Reset", self._reset) - buttons.add(btn_reset) - btn_ok = Button("OK", self._validate) - buttons.add(btn_ok) - - container.add(grid) - container.add(buttons) - - self.add(container) - self.center() - - def _reset(self): - """Reset the selections.""" - for key in self.manager.keys_dict: - listbox = self.listboxes[key] - for i in xrange(0, listbox.getItemCount()): - if listbox.getItemText(i) in self.contacts[key]: - listbox.setItemSelected(i, "selected") - else: - listbox.setItemSelected(i, "") - - def _validate(self): - """Sets back the selected contacts to the good sub-panels.""" - _map = {} - for key in self.manager.keys_dict: - selections = self.listboxes[key].getSelectedItemText() - if EMPTY_SELECTION_ITEM in selections: - selections.remove(EMPTY_SELECTION_ITEM) - _map[key] = selections - self.manager.setContacts(_map) - self.hide()
--- a/browser_side/logging.py Fri May 16 11:51:10 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,50 +0,0 @@ -#!/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/>. -"""This module configure logs for Libervia browser side""" - -from __pyjamas__ import console -from constants import Const as C -from sat.core import log # XXX: we don't use core.log_config here to avoid the impossible imports in pyjamas - - -class LiberviaLogger(log.Logger): - - def out(self, message, level=None): - if level == C.LOG_LVL_DEBUG: - console.debug(message) - elif level == C.LOG_LVL_INFO: - console.info(message) - elif level == C.LOG_LVL_WARNING: - console.warn(message) - else: - console.error(message) - - -def configure(): - fmt = '[%(name)s] %(message)s' - log.configure(C.LOG_BACKEND_CUSTOM, - logger_class = LiberviaLogger, - level = C.LOG_LVL_DEBUG, - fmt = fmt, - output = None, - logger = None, - colors = False, - force_colors = False) - # FIXME: workaround for Pyjamas, need to be removed when Pyjamas is fixed - LiberviaLogger.fmt = fmt
--- a/browser_side/menu.py Fri May 16 11:51:10 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,267 +0,0 @@ -#!/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/>. - -import pyjd # this is dummy in pyjs -from sat.core.log import getLogger -log = getLogger(__name__) -from pyjamas.ui.SimplePanel import SimplePanel -from pyjamas.ui.MenuBar import MenuBar -from pyjamas.ui.MenuItem import MenuItem -from pyjamas.ui.HTML import HTML -from pyjamas.ui.Frame import Frame -from pyjamas import Window -from jid import JID -from file_tools import FileUploadPanel -from xmlui import XMLUI -from browser_side import panels -from browser_side import dialog -from contact_group import ContactGroupEditor -from sat.core.i18n import _ - - -class MenuCmd: - - def __init__(self, object_, handler): - self._object = object_ - self._handler = handler - - def execute(self): - handler = getattr(self._object, self._handler) - handler() - - -class PluginMenuCmd: - - def __init__(self, host, action_id): - self.host = host - self.action_id = action_id - - def execute(self): - self.host.launchAction(self.action_id, None) - - -class LiberviaMenuBar(MenuBar): - - def __init__(self): - MenuBar.__init__(self, vertical=False) - self.setStyleName('gwt-MenuBar-horizontal') # XXX: workaround for the Pyjamas' class name fix (it's now "gwt-MenuBar gwt-MenuBar-horizontal") - # TODO: properly adapt CSS to the new class name - - def doItemAction(self, item, fireCommand): - MenuBar.doItemAction(self, item, fireCommand) - if item == self.items[-1] and self.popup: - self.popup.setPopupPosition(Window.getClientWidth() - - self.popup.getOffsetWidth() - 22, - self.getAbsoluteTop() + - self.getOffsetHeight() - 1) - self.popup.addStyleName('menuLastPopup') - - -class AvatarUpload(FileUploadPanel): - def __init__(self): - texts = {'ok_button': 'Upload avatar', - 'body': 'Please select an image to show as your avatar...<br>Your picture must be a square and will be resized to 64x64 pixels if necessary.', - 'errback': "Can't open image... did you actually submit an image?", - 'body_errback': 'Please select another image file.', - 'callback': "Your new profile picture has been set!"} - FileUploadPanel.__init__(self, 'upload_avatar', 'avatar_path', 2, texts) - - -class Menu(SimplePanel): - - def __init__(self, host): - self.host = host - SimplePanel.__init__(self) - self.setStyleName('menuContainer') - - def createMenus(self, add_menus): - _item_tpl = "<img src='media/icons/menu/%s_menu_red.png' />%s" - menus_dict = {} - menus_order = [] - - def addMenu(menu_name, menu_name_i18n, item_name_i18n, icon, menu_cmd): - """ add a menu to menu_dict """ - log.info("addMenu: %s %s %s %s %s" % (menu_name, menu_name_i18n, item_name_i18n, icon, menu_cmd)) - try: - menu_bar = menus_dict[menu_name] - except KeyError: - menu_bar = menus_dict[menu_name] = MenuBar(vertical=True) - menus_order.append((menu_name, menu_name_i18n, icon)) - if item_name_i18n and menu_cmd: - menu_bar.addItem(item_name_i18n, menu_cmd) - - addMenu("General", _("General"), _("Web widget"), 'home', MenuCmd(self, "onWebWidget")) - addMenu("General", _("General"), _("Disconnect"), 'home', MenuCmd(self, "onDisconnect")) - addMenu("Contacts", _("Contacts"), None, 'social', None) - addMenu("Groups", _("Groups"), _("Discussion"), 'social', MenuCmd(self, "onJoinRoom")) - addMenu("Groups", _("Groups"), _("Collective radio"), 'social', MenuCmd(self, "onCollectiveRadio")) - addMenu("Games", _("Games"), _("Tarot"), 'games', MenuCmd(self, "onTarotGame")) - addMenu("Games", _("Games"), _("Xiangqi"), 'games', MenuCmd(self, "onXiangqiGame")) - - # additional menus - for action_id, type_, path, path_i18n in add_menus: - if not path: - log.warning("skipping menu without path") - continue - if len(path) != len(path_i18n): - log.error("inconsistency between menu paths") - continue - menu_name = path[0] - menu_name_i18n = path_i18n[0] - item_name = path[1:] - if not item_name: - log.warning("skipping menu with a path of lenght 1 [%s]" % path[0]) - continue - item_name_i18n = ' | '.join(path_i18n[1:]) - addMenu(menu_name, menu_name_i18n, item_name_i18n, 'plugins', PluginMenuCmd(self.host, action_id)) - - # menu items that should be displayed after the automatically added ones - addMenu("Contacts", _("Contacts"), _("Manage groups"), 'social', MenuCmd(self, "onManageContactGroups")) - - menus_order.append(None) # we add separator - - addMenu("Help", _("Help"), _("Social contract"), 'help', MenuCmd(self, "onSocialContract")) - addMenu("Help", _("Help"), _("About"), 'help', MenuCmd(self, "onAbout")) - addMenu("Settings", _("Settings"), _("Account"), 'settings', MenuCmd(self, "onAccount")) - addMenu("Settings", _("Settings"), _("Parameters"), 'settings', MenuCmd(self, "onParameters")) - - # XXX: temporary, will change when a full profile will be managed in SàT - addMenu("Settings", _("Settings"), _("Upload avatar"), 'settings', MenuCmd(self, "onAvatarUpload")) - - menubar = LiberviaMenuBar() - - for menu_data in menus_order: - if menu_data is None: - _separator = MenuItem('', None) - _separator.setStyleName('menuSeparator') - menubar.addItem(_separator, None) - else: - menu_name, menu_name_i18n, icon = menu_data - menubar.addItem(MenuItem(_item_tpl % (icon, menu_name_i18n), True, menus_dict[menu_name])) - - self.add(menubar) - - #General menu - def onWebWidget(self): - web_panel = panels.WebPanel(self.host, "http://www.goffi.org") - self.host.addWidget(web_panel) - self.host.setSelected(web_panel) - - def onDisconnect(self): - def confirm_cb(answer): - if answer: - log.info("disconnection") - self.host.bridge.call('disconnect', None) - _dialog = dialog.ConfirmDialog(confirm_cb, text="Do you really want to disconnect ?") - _dialog.show() - - def onSocialContract(self): - _frame = Frame('contrat_social.html') - _frame.setStyleName('infoFrame') - _dialog = dialog.GenericDialog("Contrat Social", _frame) - _dialog.setSize('80%', '80%') - _dialog.show() - - def onAbout(self): - _about = HTML("""<b>Libervia</b>, a Salut à Toi project<br /> -<br /> -You can contact the author at <a href="mailto:goffi@goffi.org">goffi@goffi.org</a><br /> -Blog available (mainly in french) at <a href="http://www.goffi.org" target="_blank">http://www.goffi.org</a><br /> -Project page: <a href="http://sat.goffi.org"target="_blank">http://sat.goffi.org</a><br /> -<br /> -Any help welcome :) -<p style='font-size:small;text-align:center'>This project is dedicated to Roger Poisson</p> -""") - _dialog = dialog.GenericDialog("About", _about) - _dialog.show() - - #Contact menu - def onManageContactGroups(self): - """Open the contact groups manager.""" - - def onCloseCallback(): - pass - - ContactGroupEditor(self.host, None, onCloseCallback) - - #Group menu - def onJoinRoom(self): - - def invite(room_jid, contacts): - for contact in contacts: - self.host.bridge.call('inviteMUC', None, contact, room_jid) - - def join(room_jid, contacts): - if self.host.whoami: - nick = self.host.whoami.node - if room_jid not in [room.bare for room in self.host.room_list]: - self.host.bridge.call('joinMUC', lambda room_jid: invite(room_jid, contacts), room_jid, nick) - else: - self.host.getOrCreateLiberviaWidget(panels.ChatPanel, (room_jid, "group"), True, JID(room_jid).bare) - invite(room_jid, contacts) - - dialog.RoomAndContactsChooser(self.host, join, ok_button="Join", visible=(True, False)) - - def onCollectiveRadio(self): - def callback(room_jid, contacts): - self.host.bridge.call('launchRadioCollective', None, contacts, room_jid) - dialog.RoomAndContactsChooser(self.host, callback, ok_button="Choose", title="Collective Radio", visible=(False, True)) - - #Game menu - def onTarotGame(self): - def onPlayersSelected(room_jid, other_players): - self.host.bridge.call('launchTarotGame', None, other_players, room_jid) - dialog.RoomAndContactsChooser(self.host, onPlayersSelected, 3, title="Tarot", title_invite="Please select 3 other players", visible=(False, True)) - - def onXiangqiGame(self): - Window.alert("A Xiangqi game is planed, but not available yet") - - #Settings menu - - def onAccount(self): - def gotUI(xmlui): - if not xmlui: - return - body = XMLUI(self.host, xmlui) - _dialog = dialog.GenericDialog("Manage your XMPP account", body, options=['NO_CLOSE']) - body.setCloseCb(_dialog.close) - _dialog.show() - self.host.bridge.call('getAccountDialogUI', gotUI) - - def onParameters(self): - def gotParams(xmlui): - if not xmlui: - return - body = XMLUI(self.host, xmlui) - _dialog = dialog.GenericDialog("Parameters", body, options=['NO_CLOSE']) - body.setCloseCb(_dialog.close) - _dialog.setSize('80%', '80%') - _dialog.show() - self.host.bridge.call('getParamsUI', gotParams) - - def removeItemParams(self): - """Remove the Parameters item from the Settings menu bar.""" - self.menu_settings.removeItem(self.item_params) - - def onAvatarUpload(self): - body = AvatarUpload() - _dialog = dialog.GenericDialog("Avatar upload", body, options=['NO_CLOSE']) - body.setCloseCb(_dialog.close) - _dialog.setWidth('40%') - _dialog.show()
--- a/browser_side/nativedom.py Fri May 16 11:51:10 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,108 +0,0 @@ -#!/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/>. - -""" -This class provide basic DOM parsing based on native javascript parser -__init__ code comes from Tim Down at http://stackoverflow.com/a/8412989 -""" - -from __pyjamas__ import JS - - -class Node(): - - def __init__(self, js_node): - self._node = js_node - - def _jsNodesList2List(self, js_nodes_list): - ret=[] - for i in range(len(js_nodes_list)): - #ret.append(Element(js_nodes_list.item(i))) - ret.append(self.__class__(js_nodes_list.item(i))) # XXX: Ugly, but used to word around a Pyjamas's bug - return ret - - @property - def nodeName(self): - return self._node.nodeName - - @property - def wholeText(self): - return self._node.wholeText - - @property - def childNodes(self): - return self._jsNodesList2List(self._node.childNodes) - - def getAttribute(self, attr): - return self._node.getAttribute(attr) - - def setAttribute(self, attr, value): - return self._node.setAttribute(attr, value) - - def hasAttribute(self, attr): - return self._node.hasAttribute(attr) - - def toxml(self): - return JS("""this._node.outerHTML || new XMLSerializer().serializeToString(this._node);""") - - -class Element(Node): - - def __init__(self, js_node): - Node.__init__(self, js_node) - - def getElementsByTagName(self, tagName): - return self._jsNodesList2List(self._node.getElementsByTagName(tagName)) - - -class Document(Node): - - def __init__(self, js_document): - Node.__init__(self, js_document) - - @property - def documentElement(self): - return Element(self._node.documentElement) - - -class NativeDOM: - - def __init__(self): - JS(""" - - if (typeof window.DOMParser != "undefined") { - this.parseXml = function(xmlStr) { - return ( new window.DOMParser() ).parseFromString(xmlStr, "text/xml"); - }; - } else if (typeof window.ActiveXObject != "undefined" && - new window.ActiveXObject("Microsoft.XMLDOM")) { - this.parseXml = function(xmlStr) { - var xmlDoc = new window.ActiveXObject("Microsoft.XMLDOM"); - xmlDoc.async = "false"; - xmlDoc.loadXML(xmlStr); - return xmlDoc; - }; - } else { - throw new Error("No XML parser found"); - } - """) - - def parseString(self, xml): - return Document(self.parseXml(xml)) -
--- a/browser_side/notification.py Fri May 16 11:51:10 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,120 +0,0 @@ -from __pyjamas__ import JS, wnd -from sat.core.log import getLogger -log = getLogger(__name__) -from pyjamas import Window -from pyjamas.Timer import Timer -from browser_side import dialog -from sat.core.i18n import _ - -TIMER_DELAY = 5000 - - -class Notification(object): - """ - If the browser supports it, the user allowed it to and the tab is in the - background, send desktop notifications on messages. - - Requires both Web Notifications and Page Visibility API. - """ - - def __init__(self): - self.enabled = False - user_agent = None - notif_permission = None - JS(""" - if (!('hidden' in document)) - document.hidden = false; - - user_agent = navigator.userAgent - - if (!('Notification' in window)) - return; - - notif_permission = Notification.permission - - if (Notification.permission === 'granted') - this.enabled = true; - - else if (Notification.permission === 'default') { - Notification.requestPermission(function(permission){ - if (permission !== 'granted') - return; - - self.enabled = true; //need to use self instead of this - }); - } - """) - - if "Chrome" in user_agent and notif_permission not in ['granted', 'denied']: - self.user_agent = user_agent - self._installChromiumWorkaround() - - wnd().onfocus = self.onFocus - # wnd().onblur = self.onBlur - self._notif_count = 0 - self._orig_title = Window.getTitle() - - def _installChromiumWorkaround(self): - # XXX: Workaround for Chromium behaviour, it's doens't manage requestPermission on onLoad event - # see https://code.google.com/p/chromium/issues/detail?id=274284 - # FIXME: need to be removed if Chromium behaviour changes - try: - version_full = [s for s in self.user_agent.split() if "Chrome" in s][0].split('/')[1] - version = int(version_full.split('.')[0]) - except (IndexError, ValueError): - log.warning("Can't find Chromium version") - version = 0 - log.info("Chromium version: %d" % (version,)) - if version < 22: - log.info("Notification use the old prefixed version or are unmanaged") - return - if version < 32: - dialog.InfoDialog(_("Notifications activation for Chromium"), _('You need to activate notifications manually for your Chromium version.<br/>To activate notifications, click on the favicon on the left of the address bar')).show() - return - - log.info("==> Installing Chromium notifications request workaround <==") - self._old_click = wnd().onclick - wnd().onclick = self._chromiumWorkaround - - def _chromiumWorkaround(self): - log.info("Activating workaround") - JS(""" - Notification.requestPermission(function(permission){ - if (permission !== 'granted') - return; - self.enabled = true; //need to use self instead of this - }); - """) - wnd().onclick = self._old_click - - def onFocus(self): - Window.setTitle(self._orig_title) - self._notif_count = 0 - - # def onBlur(self): - # pass - - def isHidden(self): - JS("""return document.hidden;""") - - def _notify(self, title, body, icon): - if not self.enabled: - return - notification = None - JS(""" - notification = new Notification(title, {body: body, icon: icon}); - // Probably won’t work, but it doesn’t hurt to try. - notification.addEventListener('click', function() { - window.focus(); - }); - """) - notification.onshow = lambda: Timer(TIMER_DELAY, lambda timer: notification.close()) - - def highlightTab(self): - self._notif_count += 1 - Window.setTitle("%s (%d)" % (self._orig_title, self._notif_count)) - - def notify(self, title, body, icon='/media/icons/apps/48/sat.png'): - if self.isHidden(): - self._notify(title, body, icon) - self.highlightTab()
--- a/browser_side/panels.py Fri May 16 11:51:10 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1408 +0,0 @@ -#!/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/>. - -import pyjd # this is dummy in pyjs -from sat.core.log import getLogger -log = getLogger(__name__) -from pyjamas.ui.SimplePanel import SimplePanel -from pyjamas.ui.AbsolutePanel import AbsolutePanel -from pyjamas.ui.VerticalPanel import VerticalPanel -from pyjamas.ui.HorizontalPanel import HorizontalPanel -from pyjamas.ui.HTMLPanel import HTMLPanel -from pyjamas.ui.Frame import Frame -from pyjamas.ui.TextArea import TextArea -from pyjamas.ui.Label import Label -from pyjamas.ui.Button import Button -from pyjamas.ui.HTML import HTML -from pyjamas.ui.Image import Image -from pyjamas.ui.ClickListener import ClickHandler -from pyjamas.ui.FlowPanel import FlowPanel -from pyjamas.ui.KeyboardListener import KEY_ENTER, KEY_UP, KEY_DOWN, KeyboardHandler -from pyjamas.ui.MouseListener import MouseHandler -from pyjamas.ui.FocusListener import FocusHandler -from pyjamas.Timer import Timer -from pyjamas import DOM -from pyjamas import Window -from __pyjamas__ import doc - -from datetime import datetime -from time import time -from jid import JID - -from html_tools import html_sanitize -from base_panels import ChatText, OccupantsList, PopupMenuPanel, BaseTextEditor, LightTextEditor, HTMLTextEditor -from card_game import CardPanel -from radiocol import RadioColPanel -from menu import Menu -from browser_side import dialog -from browser_side import base_widget -from browser_side import richtext -from browser_side import contact - -from constants import Const as C -from plugin_xep_0085 import ChatStateMachine -from sat_frontends.tools.strings import addURLToText -from sat_frontends.tools.games import SYMBOLS -from sat.core.i18n import _ - - -# TODO: at some point we should decide which behaviors to keep and remove these two constants -TOGGLE_EDITION_USE_ICON = False # set to True to use an icon inside the "toggle syntax" button -NEW_MESSAGE_USE_BUTTON = False # set to True to display the "New message" button instead of an empty entry - - -class UniBoxPanel(HorizontalPanel): - """Panel containing the UniBox""" - - def __init__(self, host): - HorizontalPanel.__init__(self) - self.host = host - self.setStyleName('uniBoxPanel') - self.unibox = None - - def refresh(self): - """Enable or disable this panel. Contained widgets are created when necessary.""" - enable = self.host.params_ui['unibox']['value'] - self.setVisible(enable) - if enable and not self.unibox: - self.button = Button('<img src="media/icons/tango/actions/32/format-text-italic.png" class="richTextIcon"/>') - self.button.setTitle('Open the rich text editor') - self.button.addStyleName('uniBoxButton') - self.add(self.button) - self.unibox = UniBox(self.host) - self.add(self.unibox) - self.setCellWidth(self.unibox, '100%') - self.button.addClickListener(self.openRichMessageEditor) - self.unibox.addKey("@@: ") - self.unibox.onSelectedChange(self.host.getSelected()) - - def openRichMessageEditor(self): - """Open the rich text editor.""" - self.button.setVisible(False) - self.unibox.setVisible(False) - self.setCellWidth(self.unibox, '0px') - self.host.panel._contactsMove(self) - - def afterEditCb(): - Window.removeWindowResizeListener(self) - self.host.panel._contactsMove(self.host.panel._hpanel) - self.setCellWidth(self.unibox, '100%') - self.button.setVisible(True) - self.unibox.setVisible(True) - self.host.resize() - - richtext.RichMessageEditor.getOrCreate(self.host, self, afterEditCb) - Window.addWindowResizeListener(self) - self.host.resize() - - def onWindowResized(self, width, height): - right = self.host.panel.menu.getAbsoluteLeft() + self.host.panel.menu.getOffsetWidth() - left = self.host.panel._contacts.getAbsoluteLeft() + self.host.panel._contacts.getOffsetWidth() - ideal_width = right - left - 40 - self.host.richtext.setWidth("%spx" % ideal_width) - - -class MessageBox(TextArea): - """A basic text area for entering messages""" - - def __init__(self, host): - TextArea.__init__(self) - self.host = host - self.__size = (0, 0) - self.setStyleName('messageBox') - self.addKeyboardListener(self) - MouseHandler.__init__(self) - self.addMouseListener(self) - self._selected_cache = None - - def onBrowserEvent(self, event): - # XXX: woraroung a pyjamas bug: self.currentEvent is not set - # so the TextBox's cancelKey doens't work. This is a workaround - # FIXME: fix the bug upstream - self.currentEvent = event - TextArea.onBrowserEvent(self, event) - - def onKeyPress(self, sender, keycode, modifiers): - _txt = self.getText() - - def history_cb(text): - self.setText(text) - Timer(5, lambda timer: self.setCursorPos(len(text))) - - if keycode == KEY_ENTER: - if _txt: - self._selected_cache.onTextEntered(_txt) - self.host._updateInputHistory(_txt) - self.setText('') - sender.cancelKey() - elif keycode == KEY_UP: - self.host._updateInputHistory(_txt, -1, history_cb) - elif keycode == KEY_DOWN: - self.host._updateInputHistory(_txt, +1, history_cb) - else: - self.__onComposing() - - def __onComposing(self): - """Callback when the user is composing a text.""" - if hasattr(self._selected_cache, "target"): - self._selected_cache.state_machine._onEvent("composing") - - def onMouseUp(self, sender, x, y): - size = (self.getOffsetWidth(), self.getOffsetHeight()) - if size != self.__size: - self.__size = size - self.host.resize() - - def onSelectedChange(self, selected): - self._selected_cache = selected - - -class UniBox(MessageBox, MouseHandler): #AutoCompleteTextBox): - """This text box is used as a main typing point, for message, microblog, etc""" - - def __init__(self, host): - MessageBox.__init__(self, host) - #AutoCompleteTextBox.__init__(self) - self.setStyleName('uniBox') - host.addSelectedListener(self.onSelectedChange) - - def addKey(self, key): - return - #self.getCompletionItems().completions.append(key) - - def removeKey(self, key): - return - # TODO: investigate why AutoCompleteTextBox doesn't work here, - # maybe it can work on a TextBox but no TextArea. Remove addKey - # and removeKey methods if they don't serve anymore. - try: - self.getCompletionItems().completions.remove(key) - except KeyError: - log.warning("trying to remove an unknown key") - - def _getTarget(self, txt): - """ Say who will receive the messsage - @return: a tuple (selected, target_type, target info) with: - - target_hook: None if we use the selected widget, (msg, data) if we have a hook (e.g. "@@: " for a public blog), where msg is the parsed message (i.e. without the "hook key: "@@: bla" become ("bla", None)) - - target_type: one of PUBLIC, GROUP, ONE2ONE, STATUS, MISC - - msg: HTML message which will appear in the privacy warning banner """ - target = self._selected_cache - - def getSelectedOrStatus(): - if target and target.isSelectable(): - _type, msg = target.getWarningData() - target_hook = None # we use the selected widget, not a hook - else: - _type, msg = "STATUS", "This will be your new status message" - target_hook = (txt, None) - return (target_hook, _type, msg) - - if not txt.startswith('@'): - target_hook, _type, msg = getSelectedOrStatus() - elif txt.startswith('@@: '): - _type = "PUBLIC" - msg = MicroblogPanel.warning_msg_public - target_hook = (txt[4:], None) - elif txt.startswith('@'): - _end = txt.find(': ') - if _end == -1: - target_hook, _type, msg = getSelectedOrStatus() - else: - group = txt[1:_end] # only one target group is managed for the moment - if not group or not group in self.host.contact_panel.getGroups(): - # the group doesn't exists, we ignore the key - group = None - target_hook, _type, msg = getSelectedOrStatus() - else: - _type = "GROUP" - msg = MicroblogPanel.warning_msg_group % group - target_hook = (txt[_end + 2:], group) - else: - log.error("Unknown target") - target_hook, _type, msg = getSelectedOrStatus() - - return (target_hook, _type, msg) - - def onKeyPress(self, sender, keycode, modifiers): - _txt = self.getText() - target_hook, type_, msg = self._getTarget(_txt) - - if keycode == KEY_ENTER: - if _txt: - if target_hook: - parsed_txt, data = target_hook - self.host.send([(type_, data)], parsed_txt) - self.host._updateInputHistory(_txt) - self.setText('') - self.host.showWarning(None, None) - else: - self.host.showWarning(type_, msg) - MessageBox.onKeyPress(self, sender, keycode, modifiers) - - def getTargetAndData(self): - """For external use, to get information about the (hypothetical) message - that would be sent if we press Enter right now in the unibox. - @return a tuple (target, data) with: - - data: what would be the content of the message (body) - - target: JID, group with the prefix "@" or the public entity "@@" - """ - _txt = self.getText() - target_hook, _type, _msg = self._getTarget(_txt) - if target_hook: - data, target = target_hook - if target is None: - return target_hook - return (data, "@%s" % (target if target != "" else "@")) - if isinstance(self._selected_cache, MicroblogPanel): - groups = self._selected_cache.accepted_groups - target = "@%s" % (groups[0] if len(groups) > 0 else "@") - if len(groups) > 1: - Window.alert("Sole the first group of the selected panel is taken in consideration: '%s'" % groups[0]) - elif isinstance(self._selected_cache, ChatPanel): - target = self._selected_cache.target - else: - target = None - return (_txt, target) - - def onWidgetClosed(self, lib_wid): - """Called when a libervia widget is closed""" - if self._selected_cache == lib_wid: - self.onSelectedChange(None) - - """def complete(self): - - #self.visible=False #XXX: self.visible is not unset in pyjamas when ENTER is pressed and a completion is done - #XXX: fixed directly on pyjamas, if the patch is accepted, no need to walk around this - return AutoCompleteTextBox.complete(self)""" - - -class WarningPopup(): - - def __init__(self): - self._popup = None - self._timer = Timer(notify=self._timeCb) - - def showWarning(self, type_=None, msg=None, duration=2000): - """Display a popup information message, e.g. to notify the recipient of a message being composed. - If type_ is None, a popup being currently displayed will be hidden. - @type_: a type determining the CSS style to be applied (see __showWarning) - @msg: message to be displayed - """ - if type_ is None: - self.__removeWarning() - return - if not self._popup: - self.__showWarning(type_, msg) - elif (type_, msg) != self._popup.target_data: - self._timeCb(None) # we remove the popup - self.__showWarning(type_, msg) - - self._timer.schedule(duration) - - def __showWarning(self, type_, msg): - """Display a popup information message, e.g. to notify the recipient of a message being composed. - @type_: a type determining the CSS style to be applied. For now the defined styles are - "NONE" (will do nothing), "PUBLIC", "GROUP", "STATUS" and "ONE2ONE". - @msg: message to be displayed - """ - if type_ == "NONE": - return - if not msg: - log.warning("no msg set uniBox warning") - return - if type_ == "PUBLIC": - style = "targetPublic" - elif type_ == "GROUP": - style = "targetGroup" - elif type_ == "STATUS": - style = "targetStatus" - elif type_ == "ONE2ONE": - style = "targetOne2One" - else: - log.error("unknown message type") - return - contents = HTML(msg) - - self._popup = dialog.PopupPanelWrapper(autoHide=False, modal=False) - self._popup.target_data = (type_, msg) - self._popup.add(contents) - self._popup.setStyleName("warningPopup") - if style: - self._popup.addStyleName(style) - - left = 0 - top = 0 # max(0, self.getAbsoluteTop() - contents.getOffsetHeight() - 2) - self._popup.setPopupPosition(left, top) - self._popup.show() - - def _timeCb(self, timer): - if self._popup: - self._popup.hide() - del self._popup - self._popup = None - - def __removeWarning(self): - """Remove the popup""" - self._timeCb(None) - - -class MicroblogItem(): - # XXX: should be moved in a separated module - - def __init__(self, data): - self.id = data['id'] - self.type = data.get('type', 'main_item') - self.empty = data.get('new', False) - self.title = data.get('title', '') - self.title_xhtml = data.get('title_xhtml', '') - self.content = data.get('content', '') - self.content_xhtml = data.get('content_xhtml', '') - self.author = data['author'] - self.updated = float(data.get('updated', 0)) # XXX: int doesn't work here - self.published = float(data.get('published', self.updated)) # XXX: int doesn't work here - self.service = data.get('service', '') - self.node = data.get('node', '') - self.comments = data.get('comments', False) - self.comments_service = data.get('comments_service', '') - self.comments_node = data.get('comments_node', '') - - -class MicroblogEntry(SimplePanel, ClickHandler, FocusHandler, KeyboardHandler): - - def __init__(self, blog_panel, data): - """ - @param blog_panel: the parent panel - @param data: dict containing the blog item data, or a MicroblogItem instance. - """ - self._base_item = data if isinstance(data, MicroblogItem) else MicroblogItem(data) - for attr in ['id', 'type', 'empty', 'title', 'title_xhtml', 'content', 'content_xhtml', - 'author', 'updated', 'published', 'comments', 'service', 'node', - 'comments_service', 'comments_node']: - getter = lambda attr: lambda inst: getattr(inst._base_item, attr) - setter = lambda attr: lambda inst, value: setattr(inst._base_item, attr, value) - setattr(MicroblogEntry, attr, property(getter(attr), setter(attr))) - - SimplePanel.__init__(self) - self._blog_panel = blog_panel - - self.panel = FlowPanel() - self.panel.setStyleName('mb_entry') - - self.header = HTMLPanel('') - self.panel.add(self.header) - - self.entry_actions = VerticalPanel() - self.entry_actions.setStyleName('mb_entry_actions') - self.panel.add(self.entry_actions) - - entry_avatar = SimplePanel() - entry_avatar.setStyleName('mb_entry_avatar') - self.avatar = Image(self._blog_panel.host.getAvatar(self.author)) - entry_avatar.add(self.avatar) - self.panel.add(entry_avatar) - - if TOGGLE_EDITION_USE_ICON: - self.entry_dialog = HorizontalPanel() - else: - self.entry_dialog = VerticalPanel() - self.entry_dialog.setStyleName('mb_entry_dialog') - self.panel.add(self.entry_dialog) - - self.add(self.panel) - ClickHandler.__init__(self) - self.addClickListener(self) - - self.__pub_data = (self.service, self.node, self.id) - self.__setContent() - - def __setContent(self): - """Actually set the entry content (header, icons, bubble...)""" - self.delete_label = self.update_label = self.comment_label = None - self.bubble = self._current_comment = None - self.__setHeader() - self.__setBubble() - self.__setIcons() - - def __setHeader(self): - """Set the entry header""" - if self.empty: - return - update_text = u" — ✍ " + "<span class='mb_entry_timestamp'>%s</span>" % datetime.fromtimestamp(self.updated) - self.header.setHTML("""<div class='mb_entry_header'> - <span class='mb_entry_author'>%(author)s</span> on - <span class='mb_entry_timestamp'>%(published)s</span>%(updated)s - </div>""" % {'author': html_sanitize(self.author), - 'published': datetime.fromtimestamp(self.published), - 'updated': update_text if self.published != self.updated else '' - } - ) - - def __setIcons(self): - """Set the entry icons (delete, update, comment)""" - if self.empty: - return - - def addIcon(label, title): - label = Label(label) - label.setTitle(title) - label.addClickListener(self) - self.entry_actions.add(label) - return label - - if self.comments: - self.comment_label = addIcon(u"↶", "Comment this message") - self.comment_label.setStyleName('mb_entry_action_larger') - is_publisher = self.author == self._blog_panel.host.whoami.bare - if is_publisher: - self.update_label = addIcon(u"✍", "Edit this message") - if is_publisher or str(self.node).endswith(self._blog_panel.host.whoami.bare): - self.delete_label = addIcon(u"✗", "Delete this message") - - def updateAvatar(self, new_avatar): - """Change the avatar of the entry - @param new_avatar: path to the new image""" - self.avatar.setUrl(new_avatar) - - def onClick(self, sender): - if sender == self: - try: # prevent re-selection of the main entry after a comment has been focused - if self.__ignoreNextEvent: - self.__ignoreNextEvent = False - return - except AttributeError: - pass - self._blog_panel.setSelectedEntry(self) - elif sender == self.delete_label: - self._delete() - elif sender == self.update_label: - self.edit(True) - elif sender == self.comment_label: - self.__ignoreNextEvent = True - self._comment() - - def __modifiedCb(self, content): - """Send the new content to the backend - @return: False to restore the original content if a deletion has been cancelled - """ - if not content['text']: # previous content has been emptied - self._delete(True) - return False - extra = {'published': str(self.published)} - if isinstance(self.bubble, richtext.RichTextEditor): - # TODO: if the user change his parameters after the message edition started, - # the message syntax could be different then the current syntax: pass the - # message syntax in extra for the frontend to use it instead of current syntax. - extra.update({'content_rich': content['text'], 'title': content['title']}) - if self.empty: - if self.type == 'main_item': - self._blog_panel.host.bridge.call('sendMblog', None, None, self._blog_panel.accepted_groups, content['text'], extra) - else: - self._blog_panel.host.bridge.call('sendMblogComment', None, self._parent_entry.comments, content['text'], extra) - else: - self._blog_panel.host.bridge.call('updateMblog', None, self.__pub_data, self.comments, content['text'], extra) - return True - - def __afterEditCb(self, content): - """Remove the entry if it was an empty one (used for creating a new blog post). - Data for the actual new blog post will be received from the bridge""" - if self.empty: - self._blog_panel.removeEntry(self.type, self.id) - if self.type == 'main_item': # restore the "New message" button - self._blog_panel.refresh() - else: # allow to create a new comment - self._parent_entry._current_comment = None - self.entry_dialog.setWidth('auto') - try: - self.toggle_syntax_button.removeFromParent() - except TypeError: - pass - - def __setBubble(self, edit=False): - """Set the bubble displaying the initial content.""" - content = {'text': self.content_xhtml if self.content_xhtml else self.content, - 'title': self.title_xhtml if self.title_xhtml else self.title} - if self.content_xhtml: - content.update({'syntax': C.SYNTAX_XHTML}) - if self.author != self._blog_panel.host.whoami.bare: - options = ['read_only'] - else: - options = [] if self.empty else ['update_msg'] - self.bubble = richtext.RichTextEditor(self._blog_panel.host, content, self.__modifiedCb, self.__afterEditCb, options) - else: # assume raw text message have no title - self.bubble = LightTextEditor(content, self.__modifiedCb, self.__afterEditCb, options={'no_xhtml': True}) - self.bubble.addStyleName("bubble") - try: - self.toggle_syntax_button.removeFromParent() - except TypeError: - pass - self.entry_dialog.add(self.bubble) - self.edit(edit) - self.bubble.addEditListener(self.__showWarning) - - def __showWarning(self, sender, keycode): - if keycode == KEY_ENTER: - self._blog_panel.host.showWarning(None, None) - else: - self._blog_panel.host.showWarning(*self._blog_panel.getWarningData(self.type == 'comment')) - - def _delete(self, empty=False): - """Ask confirmation for deletion. - @return: False if the deletion has been cancelled.""" - def confirm_cb(answer): - if answer: - self._blog_panel.host.bridge.call('deleteMblog', None, self.__pub_data, self.comments) - else: # restore the text if it has been emptied during the edition - self.bubble.setContent(self.bubble._original_content) - - if self.empty: - text = _("New ") + (_("message") if self.comments else _("comment")) + _(" without body has been cancelled.") - dialog.InfoDialog(_("Information"), text).show() - return - text = "" - if empty: - text = (_("Message") if self.comments else _("Comment")) + _(" body has been emptied.<br/>") - target = _('message and all its comments') if self.comments else _('comment') - text += _("Do you really want to delete this %s?") % target - dialog.ConfirmDialog(confirm_cb, text=text).show() - - def _comment(self): - """Add an empty entry for a new comment""" - if self._current_comment: - self._current_comment.bubble.setFocus(True) - self._blog_panel.setSelectedEntry(self._current_comment) - return - data = {'id': str(time()), - 'new': True, - 'type': 'comment', - 'author': self._blog_panel.host.whoami.bare, - 'service': self.comments_service, - 'node': self.comments_node - } - entry = self._blog_panel.addEntry(data) - if entry is None: - log.info("The entry of id %s can not be commented" % self.id) - return - entry._parent_entry = self - self._current_comment = entry - self.edit(True, entry) - self._blog_panel.setSelectedEntry(entry) - - def edit(self, edit, entry=None): - """Toggle the bubble between display and edit mode - @edit: boolean value - @entry: MicroblogEntry instance, or None to use self - """ - if entry is None: - entry = self - try: - entry.toggle_syntax_button.removeFromParent() - except TypeError: - pass - entry.bubble.edit(edit) - if edit: - if isinstance(entry.bubble, richtext.RichTextEditor): - image = '<a class="richTextIcon">A</a>' - html = '<a style="color: blue;">raw text</a>' - title = _('Switch to raw text edition') - else: - image = '<img src="media/icons/tango/actions/32/format-text-italic.png" class="richTextIcon"/>' - html = '<a style="color: blue;">rich text</a>' - title = _('Switch to rich text edition') - if TOGGLE_EDITION_USE_ICON: - entry.entry_dialog.setWidth('80%') - entry.toggle_syntax_button = Button(image, entry.toggleContentSyntax) - entry.toggle_syntax_button.setTitle(title) - entry.entry_dialog.add(entry.toggle_syntax_button) - else: - entry.toggle_syntax_button = HTML(html) - entry.toggle_syntax_button.addClickListener(entry.toggleContentSyntax) - entry.toggle_syntax_button.addStyleName('mb_entry_toggle_syntax') - entry.entry_dialog.add(entry.toggle_syntax_button) - entry.toggle_syntax_button.setStyleAttribute('top', '-20px') # XXX: need to force CSS - entry.toggle_syntax_button.setStyleAttribute('left', '-20px') - - def toggleContentSyntax(self): - """Toggle the editor between raw and rich text""" - original_content = self.bubble.getOriginalContent() - rich = not isinstance(self.bubble, richtext.RichTextEditor) - if rich: - original_content['syntax'] = C.SYNTAX_XHTML - - def setBubble(text): - self.content = text - self.content_xhtml = text if rich else '' - self.content_title = self.content_title_xhtml = '' - self.bubble.removeFromParent() - self.__setBubble(True) - self.bubble.setOriginalContent(original_content) - if rich: - self.bubble.setDisplayContent() # needed in case the edition is aborted, to not end with an empty bubble - - text = self.bubble.getContent()['text'] - if not text: - setBubble(' ') # something different than empty string is needed to initialize the rich text editor - return - if not rich: - def confirm_cb(answer): - if answer: - self._blog_panel.host.bridge.call('syntaxConvert', setBubble, text, C.SYNTAX_CURRENT, C.SYNTAX_TEXT) - dialog.ConfirmDialog(confirm_cb, text=_("Do you really want to lose the title and text formatting?")).show() - else: - self._blog_panel.host.bridge.call('syntaxConvert', setBubble, text, C.SYNTAX_TEXT, C.SYNTAX_XHTML) - - -class MicroblogPanel(base_widget.LiberviaWidget): - warning_msg_public = "This message will be PUBLIC and everybody will be able to see it, even people you don't know" - warning_msg_group = "This message will be published for all the people of the group <span class='warningTarget'>%s</span>" - - def __init__(self, host, accepted_groups): - """Panel used to show microblog - @param accepted_groups: groups displayed in this panel, if empty, show all microblogs from all contacts - """ - base_widget.LiberviaWidget.__init__(self, host, ", ".join(accepted_groups), selectable=True) - self.setAcceptedGroup(accepted_groups) - self.host = host - self.entries = {} - self.comments = {} - self.selected_entry = None - self.vpanel = VerticalPanel() - self.vpanel.setStyleName('microblogPanel') - self.setWidget(self.vpanel) - - def refresh(self): - """Refresh the display of this widget. If the unibox is disabled, - display the 'New message' button or an empty bubble on top of the panel""" - if hasattr(self, 'new_button'): - self.new_button.setVisible(self.host.uni_box is None) - return - if self.host.uni_box is None: - def addBox(): - if hasattr(self, 'new_button'): - self.new_button.setVisible(False) - data = {'id': str(time()), - 'new': True, - 'author': self.host.whoami.bare, - } - entry = self.addEntry(data) - entry.edit(True) - if NEW_MESSAGE_USE_BUTTON: - self.new_button = Button("New message", listener=addBox) - self.new_button.setStyleName("microblogNewButton") - self.vpanel.insert(self.new_button, 0) - else: - addBox() - - @classmethod - def registerClass(cls): - base_widget.LiberviaWidget.addDropKey("GROUP", cls.createPanel) - base_widget.LiberviaWidget.addDropKey("CONTACT_TITLE", cls.createMetaPanel) - - @classmethod - def createPanel(cls, host, item): - """Generic panel creation for one, several or all groups (meta). - @parem host: the SatWebFrontend instance - @param item: single group as a string, list of groups - (as an array) or None (for the meta group = "all groups") - @return: the created MicroblogPanel - """ - _items = item if isinstance(item, list) else ([] if item is None else [item]) - _type = 'ALL' if _items == [] else 'GROUP' - # XXX: pyjamas doesn't support use of cls directly - _new_panel = MicroblogPanel(host, _items) - host.FillMicroblogPanel(_new_panel) - host.bridge.call('getMassiveLastMblogs', _new_panel.massiveInsert, _type, _items, 10) - host.setSelected(_new_panel) - _new_panel.refresh() - return _new_panel - - @classmethod - def createMetaPanel(cls, host, item): - """Needed for the drop keys to not be mixed between meta panel and panel for "Contacts" group""" - return MicroblogPanel.createPanel(host, None) - - @property - def accepted_groups(self): - return self._accepted_groups - - def matchEntity(self, entity): - """ - @param entity: single group as a string, list of groups - (as an array) or None (for the meta group = "all groups") - @return: True if self matches the given entity - """ - entity = entity if isinstance(entity, list) else ([] if entity is None else [entity]) - entity.sort() # sort() do not return the sorted list: do it here, not on the "return" line - return self.accepted_groups == entity - - def getWarningData(self, comment=None): - """ - @param comment: True if the composed message is a comment. If None, consider we are - composing from the unibox and guess the message type from self.selected_entry - @return: a couple (type, msg) for calling self.host.showWarning""" - if comment is None: # composing from the unibox - if self.selected_entry and not self.selected_entry.comments: - log.error("an item without comment is selected") - return ("NONE", None) - comment = self.selected_entry is not None - if comment: - return ("PUBLIC", "This is a <span class='warningTarget'>comment</span> and keep the initial post visibility, so it is potentialy public") - elif not self._accepted_groups: - # we have a meta MicroblogPanel, we publish publicly - return ("PUBLIC", self.warning_msg_public) - else: - # we only accept one group at the moment - # FIXME: manage several groups - return ("GROUP", self.warning_msg_group % self._accepted_groups[0]) - - def onTextEntered(self, text): - if self.selected_entry: - # we are entering a comment - comments_url = self.selected_entry.comments - if not comments_url: - raise Exception("ERROR: the comments URL is empty") - target = ("COMMENT", comments_url) - elif not self._accepted_groups: - # we are entering a public microblog - target = ("PUBLIC", None) - else: - # we are entering a microblog restricted to a group - # FIXME: manage several groups - target = ("GROUP", self._accepted_groups[0]) - self.host.send([target], text) - - def accept_all(self): - return not self._accepted_groups # we accept every microblog only if we are not filtering by groups - - def getEntries(self): - """Ask all the entries for the currenly accepted groups, - and fill the panel""" - - def massiveInsert(self, mblogs): - """Insert several microblogs at once - @param mblogs: dictionary of microblogs, as the result of getMassiveLastGroupBlogs - """ - log.debug("Massive insertion of %d microblogs" % len(mblogs)) - for publisher in mblogs: - log.debug("adding blogs for [%s]" % publisher) - for mblog in mblogs[publisher]: - if not "content" in mblog: - log.warning("No content found in microblog [%s]", mblog) - continue - self.addEntry(mblog) - - def mblogsInsert(self, mblogs): - """ Insert several microblogs at once - @param mblogs: list of microblogs - """ - for mblog in mblogs: - if not "content" in mblog: - log.warning("No content found in microblog [%s]", mblog) - continue - self.addEntry(mblog) - - def _chronoInsert(self, vpanel, entry, reverse=True): - """ Insert an entry in chronological order - @param vpanel: VerticalPanel instance - @param entry: MicroblogEntry - @param reverse: more recent entry on top if True, chronological order else""" - if entry.empty: - entry.published = time() - # we look for the right index to insert our entry: - # if reversed, we insert the entry above the first entry - # in the past - idx = 0 - - for child in vpanel.children: - if not isinstance(child, MicroblogEntry): - idx += 1 - continue - if reverse: - if child.published < entry.published: - break - else: - if child.published > entry.published: - break - idx += 1 - - vpanel.insert(entry, idx) - - def addEntry(self, data): - """Add an entry to the panel - @param data: dict containing the item data - @return: the added entry, or None - """ - _entry = MicroblogEntry(self, data) - if _entry.type == "comment": - comments_hash = (_entry.service, _entry.node) - if not comments_hash in self.comments: - # The comments node is not known in this panel - return None - parent = self.comments[comments_hash] - parent_idx = self.vpanel.getWidgetIndex(parent) - # we find or create the panel where the comment must be inserted - try: - sub_panel = self.vpanel.getWidget(parent_idx + 1) - except IndexError: - sub_panel = None - if not sub_panel or not isinstance(sub_panel, VerticalPanel): - sub_panel = VerticalPanel() - 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.id == _entry.id: - # update an existing comment - sub_panel.remove(comment) - sub_panel.insert(_entry, idx) - return _entry - # we want comments to be inserted in chronological order - self._chronoInsert(sub_panel, _entry, reverse=False) - return _entry - - if _entry.id in self.entries: # update - idx = self.vpanel.getWidgetIndex(self.entries[_entry.id]) - self.vpanel.remove(self.entries[_entry.id]) - self.vpanel.insert(_entry, idx) - else: # new entry - self._chronoInsert(self.vpanel, _entry) - self.entries[_entry.id] = _entry - - if _entry.comments: - # entry has comments, we keep the comments service/node as a reference - comments_hash = (_entry.comments_service, _entry.comments_node) - self.comments[comments_hash] = _entry - self.host.bridge.call('getMblogComments', self.mblogsInsert, _entry.comments_service, _entry.comments_node) - - return _entry - - 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': - if child.id == 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.id == id_: - comment.removeFromParent() - self.selected_entry = None - break - - def setSelectedEntry(self, entry): - try: - self.vpanel.getParent().ensureVisible(entry) # scroll to the clicked entry - except AttributeError: - log.warning("FIXME: MicroblogPanel.vpanel should be wrapped in a ScrollPanel!") - removeStyle = lambda entry: entry.removeStyleName('selected_entry') - if not self.host.uni_box or not entry.comments: - entry.addStyleName('selected_entry') # blink the clicked entry - clicked_entry = entry # entry may be None when the timer is done - Timer(500, lambda timer: removeStyle(clicked_entry)) - if not self.host.uni_box: - return # unibox is disabled - # from here the previous behavior (toggle main item selection) is conserved - entry = entry if entry.comments else None - if self.selected_entry == entry: - entry = None - if self.selected_entry: - removeStyle(self.selected_entry) - if entry: - log.debug("microblog entry selected (author=%s)" % entry.author) - entry.addStyleName('selected_entry') - self.selected_entry = entry - - def updateValue(self, type_, jid, value): - """Update a jid value in entries - @param type_: one of 'avatar', 'nick' - @param jid: jid concerned - @param value: new value""" - def updateVPanel(vpanel): - for child in vpanel.children: - if isinstance(child, MicroblogEntry) and child.author == jid: - child.updateAvatar(value) - elif isinstance(child, VerticalPanel): - updateVPanel(child) - if type_ == 'avatar': - updateVPanel(self.vpanel) - - def setAcceptedGroup(self, group): - """Add one or more group(s) which can be displayed in this panel. - Prevent from duplicate values and keep the list sorted. - @param group: string of the group, or list of string - """ - if not hasattr(self, "_accepted_groups"): - self._accepted_groups = [] - groups = group if isinstance(group, list) else [group] - for _group in groups: - if _group not in self._accepted_groups: - self._accepted_groups.append(_group) - self._accepted_groups.sort() - - def isJidAccepted(self, jid): - """Tell if a jid is actepted and shown in this panel - @param jid: jid - @return: True if the jid is accepted""" - if self.accept_all(): - return True - for group in self._accepted_groups: - if self.host.contact_panel.isContactInGroup(group, jid): - return True - return False - - -class StatusPanel(HTMLTextEditor): - - EMPTY_STATUS = '<click to set a status>' - - def __init__(self, host, status=''): - self.host = host - modifiedCb = lambda content: self.host.bridge.call('setStatus', None, self.host.status_panel.presence, content['text']) or True - HTMLTextEditor.__init__(self, {'text': status}, modifiedCb, options={'no_xhtml': True, 'listen_focus': True, 'listen_click': True}) - self.edit(False) - self.setStyleName('statusPanel') - - @property - def status(self): - return self._original_content['text'] - - def __cleanContent(self, content): - status = content['text'] - if status == self.EMPTY_STATUS or status in C.PRESENCE.values(): - content['text'] = '' - return content - - def getContent(self): - return self.__cleanContent(HTMLTextEditor.getContent(self)) - - def setContent(self, content): - content = self.__cleanContent(content) - BaseTextEditor.setContent(self, content) - - def setDisplayContent(self): - status = self._original_content['text'] - try: - presence = self.host.status_panel.presence - except AttributeError: # during initialization - presence = None - if not status: - if presence and presence in C.PRESENCE: - status = C.PRESENCE[presence] - else: - status = self.EMPTY_STATUS - self.display.setHTML(addURLToText(status)) - - -class PresenceStatusPanel(HorizontalPanel, ClickHandler): - - def __init__(self, host, presence="", status=""): - self.host = host - HorizontalPanel.__init__(self, Width='100%') - self.presence_button = Label(u"◉") - self.presence_button.setStyleName("presence-button") - self.status_panel = StatusPanel(host, status=status) - self.setPresence(presence) - entries = {} - for value in C.PRESENCE.keys(): - entries.update({C.PRESENCE[value]: {"value": value}}) - - def callback(sender, key): - self.setPresence(entries[key]["value"]) # order matters - self.host.send([("STATUS", None)], self.status_panel.status) - - self.presence_list = PopupMenuPanel(entries, callback=callback, style={"menu": "gwt-ListBox"}) - self.presence_list.registerClickSender(self.presence_button) - - panel = HorizontalPanel() - panel.add(self.presence_button) - panel.add(self.status_panel) - panel.setCellVerticalAlignment(self.presence_button, 'baseline') - panel.setCellVerticalAlignment(self.status_panel, 'baseline') - panel.setStyleName("marginAuto") - self.add(panel) - - self.status_panel.edit(False) - - ClickHandler.__init__(self) - self.addClickListener(self) - - @property - def presence(self): - return self._presence - - @property - def status(self): - return self.status_panel._original_content['text'] - - def setPresence(self, presence): - self._presence = presence - contact.setPresenceStyle(self.presence_button, self._presence) - - def setStatus(self, status): - self.status_panel.setContent({'text': status}) - self.status_panel.setDisplayContent() - - def onClick(self, sender): - # As status is the default target of uniBar, we don't want to select anything if click on it - self.host.setSelected(None) - - -class ChatPanel(base_widget.LiberviaWidget): - - def __init__(self, host, target, type_='one2one'): - """Panel used for conversation (one 2 one or group chat) - @param host: SatWebFrontend instance - @param target: entity (JID) with who we have a conversation (contact's jid for one 2 one chat, or MUC room) - @param type: one2one for simple conversation, group for MUC""" - base_widget.LiberviaWidget.__init__(self, host, title=target.bare, selectable=True) - self.vpanel = VerticalPanel() - self.vpanel.setSize('100%', '100%') - self.type = type_ - self.nick = None - if not target: - log.error("Empty target !") - return - self.target = target - self.__body = AbsolutePanel() - self.__body.setStyleName('chatPanel_body') - chat_area = HorizontalPanel() - chat_area.setStyleName('chatArea') - if type_ == 'group': - self.occupants_list = OccupantsList() - chat_area.add(self.occupants_list) - self.__body.add(chat_area) - self.content = AbsolutePanel() - self.content.setStyleName('chatContent') - self.content_scroll = base_widget.ScrollPanelWrapper(self.content) - chat_area.add(self.content_scroll) - chat_area.setCellWidth(self.content_scroll, '100%') - self.vpanel.add(self.__body) - self.vpanel.setCellHeight(self.__body, '100%') - self.addStyleName('chatPanel') - self.setWidget(self.vpanel) - self.state_machine = ChatStateMachine(self.host, str(self.target)) - self._state = None - - @classmethod - def registerClass(cls): - base_widget.LiberviaWidget.addDropKey("CONTACT", cls.createPanel) - - @classmethod - def createPanel(cls, host, item): - _contact = item if isinstance(item, JID) else JID(item) - host.contact_panel.setContactMessageWaiting(_contact.bare, False) - _new_panel = ChatPanel(host, _contact) # XXX: pyjamas doesn't seems to support creating with cls directly - _new_panel.historyPrint() - host.setSelected(_new_panel) - _new_panel.refresh() - return _new_panel - - def refresh(self): - """Refresh the display of this widget. If the unibox is disabled, - add a message box at the bottom of the panel""" - self.host.contact_panel.setContactMessageWaiting(self.target.bare, False) - self.content_scroll.scrollToBottom() - - enable_box = self.host.uni_box is None - if hasattr(self, 'message_box'): - self.message_box.setVisible(enable_box) - return - if enable_box: - self.message_box = MessageBox(self.host) - self.message_box.onSelectedChange(self) - self.vpanel.add(self.message_box) - - def matchEntity(self, entity): - """ - @param entity: target jid as a string or JID instance. - Could also be a couple with a type in the second element. - @return: True if self matches the given entity - """ - if isinstance(entity, tuple): - entity, type_ = entity if len(entity) > 1 else (entity[0], self.type) - else: - type_ = self.type - entity = entity if isinstance(entity, JID) else JID(entity) - try: - return self.target.bare == entity.bare and self.type == type_ - except AttributeError as e: - e.include_traceback() - return False - - def getWarningData(self): - if self.type not in ["one2one", "group"]: - raise Exception("Unmanaged type !") - if self.type == "one2one": - msg = "This message will be sent to your contact <span class='warningTarget'>%s</span>" % self.target - elif self.type == "group": - msg = "This message will be sent to all the participants of the multi-user room <span class='warningTarget'>%s</span>" % self.target - return ("ONE2ONE" if self.type == "one2one" else "GROUP", msg) - - def onTextEntered(self, text): - self.host.send([("groupchat" if self.type == 'group' else "chat", str(self.target))], text) - self.state_machine._onEvent("active") - - def onQuit(self): - base_widget.LiberviaWidget.onQuit(self) - if self.type == 'group': - self.host.bridge.call('mucLeave', None, self.target.bare) - - def setUserNick(self, nick): - """Set the nick of the user, usefull for e.g. change the color of the user""" - self.nick = nick - - def setPresents(self, nicks): - """Set the users presents in this room - @param occupants: list of nicks (string)""" - self.occupants_list.clear() - for nick in nicks: - self.occupants_list.addOccupant(nick) - - def userJoined(self, nick, data): - self.occupants_list.addOccupant(nick) - self.printInfo("=> %s has joined the room" % nick) - - def userLeft(self, nick, data): - self.occupants_list.removeOccupant(nick) - self.printInfo("<= %s has left the room" % nick) - - def changeUserNick(self, old_nick, new_nick): - assert(self.type == "group") - self.occupants_list.removeOccupant(old_nick) - self.occupants_list.addOccupant(new_nick) - self.printInfo(_("%(old_nick)s is now known as %(new_nick)s") % {'old_nick': old_nick, 'new_nick': new_nick}) - - def historyPrint(self, size=20): - """Print the initial history""" - def getHistoryCB(history): - # display day change - day_format = "%A, %d %b %Y" - previous_day = datetime.now().strftime(day_format) - for line in history: - timestamp, from_jid, to_jid, message, mess_type, extra = line - message_day = datetime.fromtimestamp(float(timestamp or time())).strftime(day_format) - if previous_day != message_day: - self.printInfo("* " + message_day) - previous_day = message_day - self.printMessage(from_jid, message, extra, timestamp) - self.host.bridge.call('getHistory', getHistoryCB, self.host.whoami.bare, self.target.bare, size, True) - - def printInfo(self, msg, type_='normal', link_cb=None): - """Print general info - @param msg: message to print - @param type_: one of: - "normal": general info like "toto has joined the room" - "link": general info that is clickable like "click here to join the main room" - "me": "/me" information like "/me clenches his fist" ==> "toto clenches his fist" - @param link_cb: method to call when the info is clicked, ignored if type_ is not 'link' - """ - _wid = HTML(msg) if type_ == 'link' else Label(msg) - if type_ == 'normal': - _wid.setStyleName('chatTextInfo') - elif type_ == 'link': - _wid.setStyleName('chatTextInfo-link') - if link_cb: - _wid.addClickListener(link_cb) - elif type_ == 'me': - _wid.setStyleName('chatTextMe') - else: - _wid.setStyleName('chatTextInfo') - self.content.add(_wid) - - def printMessage(self, from_jid, msg, extra, timestamp=None): - """Print message in chat window. Must be implemented by child class""" - _jid = JID(from_jid) - nick = _jid.node if self.type == 'one2one' else _jid.resource - mymess = _jid.resource == self.nick if self.type == "group" else _jid.bare == self.host.whoami.bare # mymess = True if message comes from local user - if msg.startswith('/me '): - self.printInfo('* %s %s' % (nick, msg[4:]), type_='me') - return - self.content.add(ChatText(timestamp, nick, mymess, msg, extra.get('xhtml'))) - self.content_scroll.scrollToBottom() - - def startGame(self, game_type, waiting, referee, players, *args): - """Configure the chat window to start a game""" - classes = {"Tarot": CardPanel, "RadioCol": RadioColPanel} - if game_type not in classes.keys(): - return # unknown game - attr = game_type.lower() - self.occupants_list.updateSpecials(players, SYMBOLS[attr]) - if waiting or not self.nick in players: - return # waiting for player or not playing - attr = "%s_panel" % attr - if hasattr(self, attr): - return - log.info("%s Game Started \o/" % game_type) - panel = classes[game_type](self, referee, self.nick, players, *args) - setattr(self, attr, panel) - self.vpanel.insert(panel, 0) - self.vpanel.setCellHeight(panel, panel.getHeight()) - - def getGame(self, game_type): - """Return class managing the game type""" - # TODO: check that the game is launched, and manage errors - if game_type == "Tarot": - return self.tarot_panel - elif game_type == "RadioCol": - return self.radiocol_panel - - def setState(self, state, nick=None): - """Set the chat state (XEP-0085) of the contact. Leave nick to None - to set the state for a one2one conversation, or give a nickname or - C.ALL_OCCUPANTS to set the state of a participant within a MUC. - @param state: the new chat state - @param nick: None for one2one, the MUC user nick or ALL_OCCUPANTS - """ - if nick: - assert(self.type == 'group') - occupants = self.occupants_list.occupants_list.keys() if nick == C.ALL_OCCUPANTS else [nick] - for occupant in occupants: - self.occupants_list.occupants_list[occupant].setState(state) - else: - assert(self.type == 'one2one') - self._state = state - self.refreshTitle() - self.state_machine.started = not not state # start to send "composing" state from now - - def refreshTitle(self): - """Refresh the title of this ChatPanel dialog""" - if self._state: - self.setTitle(self.target.bare + " (" + self._state + ")") - else: - self.setTitle(self.target.bare) - - -class WebPanel(base_widget.LiberviaWidget): - """ (mini)browser like widget """ - - def __init__(self, host, url=None): - """ - @param host: SatWebFrontend instance - """ - base_widget.LiberviaWidget.__init__(self, host) - self._vpanel = VerticalPanel() - self._vpanel.setSize('100%', '100%') - self._url = dialog.ExtTextBox(enter_cb=self.onUrlClick) - self._url.setText(url or "") - self._url.setWidth('100%') - hpanel = HorizontalPanel() - hpanel.add(self._url) - btn = Button("Go", self.onUrlClick) - hpanel.setCellWidth(self._url, "100%") - #self.setCellWidth(btn, "10%") - hpanel.add(self._url) - hpanel.add(btn) - self._vpanel.add(hpanel) - self._vpanel.setCellHeight(hpanel, '20px') - self._frame = Frame(url or "") - self._frame.setSize('100%', '100%') - DOM.setStyleAttribute(self._frame.getElement(), "position", "relative") - self._vpanel.add(self._frame) - self.setWidget(self._vpanel) - - def onUrlClick(self, sender): - self._frame.setUrl(self._url.getText()) - - -class MainPanel(AbsolutePanel): - - def __init__(self, host): - self.host = host - AbsolutePanel.__init__(self) - - # menu - self.menu = Menu(host) - - # unibox - self.unibox_panel = UniBoxPanel(host) - self.unibox_panel.setVisible(False) - - # contacts - self._contacts = HorizontalPanel() - self._contacts.addStyleName('globalLeftArea') - self.contacts_switch = Button(u'«', self._contactsSwitch) - self.contacts_switch.addStyleName('contactsSwitch') - self._contacts.add(self.contacts_switch) - self._contacts.add(self.host.contact_panel) - - # tabs - self.tab_panel = base_widget.MainTabPanel(host) - self.discuss_panel = base_widget.WidgetsPanel(self.host, locked=True) - self.tab_panel.add(self.discuss_panel, "Discussions") - self.tab_panel.selectTab(0) - - self.header = AbsolutePanel() - self.header.add(self.menu) - self.header.add(self.unibox_panel) - self.header.add(self.host.status_panel) - self.header.setStyleName('header') - self.add(self.header) - - self._hpanel = HorizontalPanel() - self._hpanel.add(self._contacts) - self._hpanel.add(self.tab_panel) - self.add(self._hpanel) - - self.setWidth("100%") - Window.addWindowResizeListener(self) - - def _contactsSwitch(self, btn=None): - """ (Un)hide contacts panel """ - if btn is None: - btn = self.contacts_switch - cpanel = self.host.contact_panel - cpanel.setVisible(not cpanel.getVisible()) - btn.setText(u"«" if cpanel.getVisible() else u"»") - self.host.resize() - - def _contactsMove(self, parent): - """Move the contacts container (containing the contact list and - the "hide/show" button) to another parent, but always as the - first child position (insert at index 0). - """ - if self._contacts.getParent(): - if self._contacts.getParent() == parent: - return - self._contacts.removeFromParent() - parent.insert(self._contacts, 0) - - def onWindowResized(self, width, height): - _elts = doc().getElementsByClassName('gwt-TabBar') - if not _elts.length: - tab_bar_h = 0 - else: - tab_bar_h = _elts.item(0).offsetHeight - ideal_height = Window.getClientHeight() - tab_bar_h - self.setHeight("%s%s" % (ideal_height, "px")) - - def refresh(self): - """Refresh the main panel""" - self.unibox_panel.refresh()
--- a/browser_side/plugin_xep_0085.py Fri May 16 11:51:10 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,82 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# SAT plugin for Chat State Notifications Protocol (xep-0085) -# Copyright (C) 2013, 2014 Adrien Cossa (souliane@mailoo.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/>. - -from pyjamas.Timer import Timer - - -# Copy of the map from sat/src/plugins/plugin_xep_0085 -TRANSITIONS = {"active": {"next_state": "inactive", "delay": 120}, - "inactive": {"next_state": "gone", "delay": 480}, - "gone": {"next_state": "", "delay": 0}, - "composing": {"next_state": "paused", "delay": 30}, - "paused": {"next_state": "inactive", "delay": 450} - } - - -class ChatStateMachine: - """This is an adapted version of the ChatStateMachine from sat/src/plugins/plugin_xep_0085 - which manage a timer on the web browser and keep it synchronized with the timer that runs - on the backend. This is only needed to avoid calling the bridge method chatStateComposing - too often ; accuracy is not needed so we can ignore the delay of the communication between - the web browser and the backend (the timer on the web browser always starts a bit before). - /!\ Keep this file up to date if you modify the one in the sat plugins directory. - """ - def __init__(self, host, target_s): - - self.host = host - self.target_s = target_s - self.started = False - self.state = None - self.timer = None - - def _onEvent(self, state): - """Pyjamas callback takes no extra argument so we need this trick""" - # Here we should check the value of the parameter "Send chat state notifications" - # but this costs two messages. It's even better to call chatStateComposing - # with a doubt, it will be checked by the back-end anyway before sending - # the actual notifications to the other client. - if state == "composing" and not self.started: - return - self.started = True - self.next_state = state - self.__onEvent(None) - - def __onEvent(self, timer): - # print "on event %s" % self.next_state - state = self.next_state - self.next_state = "" - if state != self.state and state == "composing": - self.host.bridge.call('chatStateComposing', None, self.target_s) - self.state = state - if not self.timer is None: - self.timer.cancel() - - if not state in TRANSITIONS: - return - if not "next_state" in TRANSITIONS[state]: - return - if not "delay" in TRANSITIONS[state]: - return - next_state = TRANSITIONS[state]["next_state"] - delay = TRANSITIONS[state]["delay"] - if next_state == "" or delay < 0: - return - self.next_state = next_state - # pyjamas timer in milliseconds - self.timer = Timer(delay * 1000, self.__onEvent)
--- a/browser_side/radiocol.py Fri May 16 11:51:10 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,321 +0,0 @@ -#!/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/>. - -import pyjd # this is dummy in pyjs -from sat.core.log import getLogger -log = getLogger(__name__) -from pyjamas.ui.VerticalPanel import VerticalPanel -from pyjamas.ui.HorizontalPanel import HorizontalPanel -from pyjamas.ui.FlexTable import FlexTable -from pyjamas.ui.FormPanel import FormPanel -from pyjamas.ui.Label import Label -from pyjamas.ui.Button import Button -from pyjamas.ui.ClickListener import ClickHandler -from pyjamas.ui.Hidden import Hidden -from pyjamas.ui.CaptionPanel import CaptionPanel -from pyjamas.media.Audio import Audio -from pyjamas import Window -from pyjamas.Timer import Timer - -from html_tools import html_sanitize -from file_tools import FilterFileUpload -from sat_frontends.tools.misc import DEFAULT_MUC -from sat.core.i18n import _ - - -class MetadataPanel(FlexTable): - - def __init__(self): - FlexTable.__init__(self) - title_lbl = Label("title:") - title_lbl.setStyleName('radiocol_metadata_lbl') - artist_lbl = Label("artist:") - artist_lbl.setStyleName('radiocol_metadata_lbl') - album_lbl = Label("album:") - album_lbl.setStyleName('radiocol_metadata_lbl') - self.title = Label("") - self.title.setStyleName('radiocol_metadata') - self.artist = Label("") - self.artist.setStyleName('radiocol_metadata') - self.album = Label("") - self.album.setStyleName('radiocol_metadata') - self.setWidget(0, 0, title_lbl) - self.setWidget(1, 0, artist_lbl) - self.setWidget(2, 0, album_lbl) - self.setWidget(0, 1, self.title) - self.setWidget(1, 1, self.artist) - self.setWidget(2, 1, self.album) - self.setStyleName("radiocol_metadata_pnl") - - def setTitle(self, title): - self.title.setText(title) - - def setArtist(self, artist): - self.artist.setText(artist) - - def setAlbum(self, album): - self.album.setText(album) - - -class ControlPanel(FormPanel): - """Panel used to show controls to add a song, or vote for the current one""" - - def __init__(self, parent): - FormPanel.__init__(self) - self.setEncoding(FormPanel.ENCODING_MULTIPART) - self.setMethod(FormPanel.METHOD_POST) - self.setAction("upload_radiocol") - self.timer_on = False - self._parent = parent - vPanel = VerticalPanel() - - types = [('audio/ogg', '*.ogg', 'Ogg Vorbis Audio'), - ('video/ogg', '*.ogv', 'Ogg Vorbis Video'), - ('application/ogg', '*.ogx', 'Ogg Vorbis Multiplex'), - ('audio/mpeg', '*.mp3', 'MPEG-Layer 3'), - ('audio/mp3', '*.mp3', 'MPEG-Layer 3'), - ] - self.file_upload = FilterFileUpload("song", 10, types) - vPanel.add(self.file_upload) - - hPanel = HorizontalPanel() - self.upload_btn = Button("Upload song", getattr(self, "onBtnClick")) - hPanel.add(self.upload_btn) - self.status = Label() - self.updateStatus() - hPanel.add(self.status) - #We need to know the filename and the referee - self.filename_field = Hidden('filename', '') - hPanel.add(self.filename_field) - referee_field = Hidden('referee', self._parent.referee) - hPanel.add(self.filename_field) - hPanel.add(referee_field) - vPanel.add(hPanel) - - self.add(vPanel) - self.addFormHandler(self) - - def updateStatus(self): - if self.timer_on: - return - # TODO: the status should be different if a song is being played or not - queue = self._parent.getQueueSize() - queue_data = self._parent.queue_data - if queue < queue_data[0]: - left = queue_data[0] - queue - self.status.setText("[we need %d more song%s]" % (left, "s" if left > 1 else "")) - elif queue < queue_data[1]: - left = queue_data[1] - queue - self.status.setText("[%d available spot%s]" % (left, "s" if left > 1 else "")) - elif queue >= queue_data[1]: - self.status.setText("[The queue is currently full]") - self.status.setStyleName('radiocol_status') - - def onBtnClick(self): - if self.file_upload.check(): - self.status.setText('[Submitting, please wait...]') - self.filename_field.setValue(self.file_upload.getFilename()) - if self.file_upload.getFilename().lower().endswith('.mp3'): - self._parent._parent.host.showWarning('STATUS', 'For a better support, it is recommended to submit Ogg Vorbis file instead of MP3. You can convert your files easily, ask for help if needed!', 5000) - self.submit() - self.file_upload.setFilename("") - - def onSubmit(self, event): - pass - - def blockUpload(self): - self.file_upload.setVisible(False) - self.upload_btn.setEnabled(False) - - def unblockUpload(self): - self.file_upload.setVisible(True) - self.upload_btn.setEnabled(True) - - def setTemporaryStatus(self, text, style): - self.status.setText(text) - self.status.setStyleName('radiocol_upload_status_%s' % style) - self.timer_on = True - - def cb(timer): - self.timer_on = False - self.updateStatus() - - Timer(5000, cb) - - def onSubmitComplete(self, event): - result = event.getResults() - if result == "OK": - # the song can still be rejected (not readable, full queue...) - self.setTemporaryStatus('[Your song has been submitted to the radio]', "ok") - elif result == "KO": - self.setTemporaryStatus('[Something went wrong during your song upload]', "ko") - self._parent.radiocolSongRejected(_("The uploaded file has been rejected, only Ogg Vorbis and MP3 songs are accepted.")) - # TODO: would be great to re-use the original Exception class and message - # but it is lost in the middle of the traceback and encapsulated within - # a DBusException instance --> extract the data from the traceback? - else: - Window.alert('Submit error: %s' % result) - self.status.setText('') - - -class Player(Audio): - - def __init__(self, player_id, metadata_panel): - Audio.__init__(self) - self._id = player_id - self.metadata = metadata_panel - self.timestamp = "" - self.title = "" - self.artist = "" - self.album = "" - self.filename = None - self.played = False # True when the song is playing/has played, becomes False on preload - self.setAutobuffer(True) - self.setAutoplay(False) - self.setVisible(False) - - - def preload(self, timestamp, filename, title, artist, album): - """preload the song but doesn't play it""" - self.timestamp = timestamp - self.filename = filename - self.title = title - self.artist = artist - self.album = album - self.played = False - self.setSrc("radiocol/%s" % html_sanitize(filename)) - log.debug("preloading %s in %s" % (title, self._id)) - - def play(self, play=True): - """Play or pause the song - @param play: set to True to play or to False to pause - """ - if play: - self.played = True - self.metadata.setTitle(self.title) - self.metadata.setArtist(self.artist) - self.metadata.setAlbum(self.album) - Audio.play(self) - else: - self.pause() - - -class RadioColPanel(HorizontalPanel, ClickHandler): - - def __init__(self, parent, referee, player_nick, players, queue_data): - """ - @param parent - @param referee - @param player_nick - @param players - @param queue_data: list of integers (queue to start, queue limit) - """ - # We need to set it here and not in the CSS :( - HorizontalPanel.__init__(self, Height="90px") - ClickHandler.__init__(self) - self._parent = parent - self.referee = referee - self.queue_data = queue_data - self.setStyleName("radiocolPanel") - - # Now we set up the layout - self.metadata_panel = MetadataPanel() - self.add(CaptionPanel("Now playing", self.metadata_panel)) - self.playlist_panel = VerticalPanel() - self.add(CaptionPanel("Songs queue", self.playlist_panel)) - self.control_panel = ControlPanel(self) - self.add(CaptionPanel("Controls", self.control_panel)) - - self.next_songs = [] - self.players = [Player("player_%d" % i, self.metadata_panel) for i in xrange(queue_data[1] + 1)] - self.current_player = None - for player in self.players: - self.add(player) - self.addClickListener(self) - - help_msg = """Accepted file formats: Ogg Vorbis (recommended), MP3.<br /> - Please do not submit files that are protected by copyright.<br /> - Click <a style="color: red;">here</a> if you need some support :)""" - link_cb = lambda: self._parent.host.bridge.call('joinMUC', None, DEFAULT_MUC, self._parent.nick) - self._parent.printInfo(help_msg, type_='link', link_cb=link_cb) - - def pushNextSong(self, title): - """Add a song to the left panel's next songs queue""" - next_song = Label(title) - next_song.setStyleName("radiocol_next_song") - self.next_songs.append(next_song) - self.playlist_panel.append(next_song) - self.control_panel.updateStatus() - - def popNextSong(self): - """Remove the first song of next songs list - should be called when the song is played""" - #FIXME: should check that the song we remove is the one we play - next_song = self.next_songs.pop(0) - self.playlist_panel.remove(next_song) - self.control_panel.updateStatus() - - def getQueueSize(self): - return len(self.playlist_panel.getChildren()) - - def radiocolCheckPreload(self, timestamp): - for player in self.players: - if player.timestamp == timestamp: - return False - return True - - def radiocolPreload(self, timestamp, filename, title, artist, album, sender): - if not self.radiocolCheckPreload(timestamp): - return # song already preloaded - preloaded = False - for player in self.players: - if not player.filename or \ - (player.played and player != self.current_player): - #if player has no file loaded, or it has already played its song - #we use it to preload the next one - player.preload(timestamp, filename, title, artist, album) - preloaded = True - break - if not preloaded: - log.warning("Can't preload song, we are getting too many songs to preload, we shouldn't have more than %d at once" % self.queue_data[1]) - else: - self.pushNextSong(title) - self._parent.printInfo(_('%(user)s uploaded %(artist)s - %(title)s') % {'user': sender, 'artist': artist, 'title': title}) - - def radiocolPlay(self, filename): - found = False - for player in self.players: - if not found and player.filename == filename: - player.play() - self.popNextSong() - self.current_player = player - found = True - else: - player.play(False) # in case the previous player was not sync - if not found: - log.error("Song not found in queue, can't play it. This should not happen") - - def radiocolNoUpload(self): - self.control_panel.blockUpload() - - def radiocolUploadOk(self): - self.control_panel.unblockUpload() - - def radiocolSongRejected(self, reason): - Window.alert("Song rejected: %s" % reason)
--- a/browser_side/register.py Fri May 16 11:51:10 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,249 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011, 2012, 2013, 2014 Jérôme Poisson <goffi@goffi.org> -# Copyright (C) 2011, 2012 Adrien Vigneron <adrienvigneron@mailoo.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/>. - -#This page manage subscription and new account creation -import pyjd # this is dummy in pyjs - -from constants import Const as C -from sat.core.i18n import _ -from pyjamas.ui.SimplePanel import SimplePanel -from pyjamas.ui.VerticalPanel import VerticalPanel -from pyjamas.ui.HorizontalPanel import HorizontalPanel -from pyjamas.ui.TabPanel import TabPanel -from pyjamas.ui.TabBar import TabBar -from pyjamas.ui.PasswordTextBox import PasswordTextBox -from pyjamas.ui.TextBox import TextBox -from pyjamas.ui.FormPanel import FormPanel -from pyjamas.ui.Button import Button -from pyjamas.ui.Label import Label -from pyjamas.ui.HTML import HTML -from pyjamas.ui.PopupPanel import PopupPanel -from pyjamas.ui.Image import Image -from pyjamas.ui.Hidden import Hidden -from pyjamas import Window -from pyjamas.ui.KeyboardListener import KEY_ENTER -import re -from pyjamas.Timer import Timer - - -class RegisterPanel(FormPanel): - - def __init__(self, callback): - """ - @param callback: method to call if login successful - """ - FormPanel.__init__(self) - self.setSize('600px', '350px') - self.callback = callback - self.setMethod(FormPanel.METHOD_POST) - main_panel = HorizontalPanel() - main_panel.setStyleName('registerPanel_main') - left_side = Image("media/libervia/register_left.png") - main_panel.add(left_side) - - ##TabPanel## - tab_bar = TabBar() - tab_bar.setStyleName('registerPanel_tabs') - self.right_side = TabPanel(tab_bar) - self.right_side.setStyleName('registerPanel_right_side') - main_panel.add(self.right_side) - main_panel.setCellWidth(self.right_side, '100%') - - ##Login tab## - login_tab = SimplePanel() - login_tab.setStyleName('registerPanel_content') - login_vpanel = VerticalPanel() - login_tab.setWidget(login_vpanel) - - self.login_warning_msg = Label('') - self.login_warning_msg.setVisible(False) - self.login_warning_msg.setStyleName('formWarning') - login_vpanel.add(self.login_warning_msg) - - login_label = Label('Login:') - self.login_box = TextBox() - self.login_box.setName("login") - self.login_box.addKeyboardListener(self) - login_pass_label = Label('Password:') - self.login_pass_box = PasswordTextBox() - self.login_pass_box.setName("login_password") - self.login_pass_box.addKeyboardListener(self) - - login_vpanel.add(login_label) - login_vpanel.add(self.login_box) - login_vpanel.add(login_pass_label) - login_vpanel.add(self.login_pass_box) - login_but = Button("Log in", getattr(self, "onLogin")) - login_but.setStyleName('button') - login_but.addStyleName('red') - login_vpanel.add(login_but) - - #The hidden submit_type field - self.submit_type = Hidden('submit_type') - login_vpanel.add(self.submit_type) - - ##Register tab## - register_tab = SimplePanel() - register_tab.setStyleName('registerPanel_content') - register_vpanel = VerticalPanel() - register_tab.setWidget(register_vpanel) - - self.register_warning_msg = HTML('') - self.register_warning_msg.setVisible(False) - self.register_warning_msg.setStyleName('formWarning') - register_vpanel.add(self.register_warning_msg) - - register_login_label = Label('Login:') - self.register_login_box = TextBox() - self.register_login_box.setName("register_login") - self.register_login_box.addKeyboardListener(self) - email_label = Label('E-mail:') - self.email_box = TextBox() - self.email_box.setName("email") - self.email_box.addKeyboardListener(self) - register_pass_label = Label('Password:') - self.register_pass_box = PasswordTextBox() - self.register_pass_box.setName("register_password") - self.register_pass_box.addKeyboardListener(self) - register_vpanel.add(register_login_label) - register_vpanel.add(self.register_login_box) - register_vpanel.add(email_label) - register_vpanel.add(self.email_box) - register_vpanel.add(register_pass_label) - register_vpanel.add(self.register_pass_box) - - register_but = Button("Register", getattr(self, "onRegister")) - register_but.setStyleName('button') - register_but.addStyleName('red') - register_vpanel.add(register_but) - - self.right_side.add(login_tab, 'Login') - self.right_side.add(register_tab, 'Register') - self.right_side.addTabListener(self) - self.right_side.selectTab(1) - login_tab.setWidth(None) - register_tab.setWidth(None) - - self.add(main_panel) - self.addFormHandler(self) - self.setAction('register_api/login') - - def onBeforeTabSelected(self, sender, tabIndex): - return True - - def onTabSelected(self, sender, tabIndex): - if tabIndex == 0: - self.login_box.setFocus(True) - elif tabIndex == 1: - self.register_login_box.setFocus(True) - - def onKeyPress(self, sender, keycode, modifiers): - if keycode == KEY_ENTER: - # Browsers offer an auto-completion feature to any - # text box, but the selected value is not set when - # the widget looses the focus. Using a timer with - # any delay value > 0 would do the trick. - if sender == self.login_box: - Timer(5, lambda timer: self.login_pass_box.setFocus(True)) - elif sender == self.login_pass_box: - self.onLogin(None) - elif sender == self.register_login_box: - Timer(5, lambda timer: self.email_box.setFocus(True)) - elif sender == self.email_box: - Timer(5, lambda timer: self.register_pass_box.setFocus(True)) - elif sender == self.register_pass_box: - self.onRegister(None) - - def onKeyUp(self, sender, keycode, modifiers): - pass - - def onKeyDown(self, sender, keycode, modifiers): - pass - - def onLogin(self, button): - if not re.match(r'^[a-z0-9_-]+$', self.login_box.getText(), re.IGNORECASE): - self.login_warning_msg.setText('Invalid login, valid characters are a-z A-Z 0-9 _ -') - self.login_warning_msg.setVisible(True) - else: - self.submit_type.setValue('login') - self.submit() - - def onRegister(self, button): - if not re.match(r'^[a-z0-9_-]+$', self.register_login_box.getText(), re.IGNORECASE): - self.register_warning_msg.setHTML(_('Invalid login, valid characters<br>are a-z A-Z 0-9 _ -')) - self.register_warning_msg.setVisible(True) - elif not re.match(r'^.+@.+\..+', self.email_box.getText(), re.IGNORECASE): - self.register_warning_msg.setHTML(_('Invalid email address')) - self.register_warning_msg.setVisible(True) - elif len(self.register_pass_box.getText()) < C.PASSWORD_MIN_LENGTH: - self.register_warning_msg.setHTML(_('Your password must contain<br>at least %d characters') % C.PASSWORD_MIN_LENGTH) - self.register_warning_msg.setVisible(True) - else: - self.register_warning_msg.setVisible(False) - self.submit_type.setValue('register') - self.submit() - - def onSubmit(self, event): - pass - - def onSubmitComplete(self, event): - result = event.getResults() - if result == "AUTH ERROR": - Window.alert('Your login and/or password is incorrect. Please try again') - elif result == "LOGGED": - self.callback() - elif result == "SESSION_ACTIVE": - Window.alert('Session already active, this should not happen, please contact the author to fix it') - elif result == "ALREADY EXISTS": - self.register_warning_msg.setHTML('This login already exists,<br>please choose another one') - self.register_warning_msg.setVisible(True) - elif result == "INTERNAL": - self.register_warning_msg.setHTML('SERVER ERROR: something went wrong during registration process, please contact the server administrator') - self.register_warning_msg.setVisible(True) - elif result == "REGISTRATION": - self.login_warning_msg.setVisible(False) - self.register_warning_msg.setVisible(False) - self.login_box.setText(self.register_login_box.getText()) - self.login_pass_box.setText('') - self.register_login_box.setText('') - self.register_pass_box.setText('') - self.email_box.setText('') - self.right_side.selectTab(0) - self.login_pass_box.setFocus(True) - Window.alert('An email has been sent to you with your login informations\nPlease remember that this is ONLY A TECHNICAL DEMO') - else: - Window.alert('Submit error: %s' % result) - - -class RegisterBox(PopupPanel): - - def __init__(self, callback, *args, **kwargs): - PopupPanel.__init__(self, *args, **kwargs) - self._form = RegisterPanel(callback) - self.setWidget(self._form) - - def onWindowResized(self, width, height): - super(RegisterBox, self).onWindowResized(width, height) - self.centerBox() - - def show(self): - super(RegisterBox, self).show() - self.centerBox() - self._form.login_box.setFocus(True)
--- a/browser_side/richtext.py Fri May 16 11:51:10 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,536 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2013, 2014 Adrien Cossa <souliane@mailoo.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/>. - -from pyjamas.ui.TextArea import TextArea -from pyjamas.ui.Button import Button -from pyjamas.ui.CheckBox import CheckBox -from pyjamas.ui.DialogBox import DialogBox -from pyjamas.ui.Label import Label -from pyjamas.ui.HTML import HTML -from pyjamas.ui.FlexTable import FlexTable -from pyjamas.ui.HorizontalPanel import HorizontalPanel -from pyjamas import Window -from pyjamas.ui.KeyboardListener import KeyboardHandler -from __pyjamas__ import doc - -from constants import Const as C -from dialog import ConfirmDialog, InfoDialog -from base_panels import TitlePanel, BaseTextEditor, HTMLTextEditor -from list_manager import ListManager -from html_tools import html_sanitize -from browser_side import panels - -from sat_frontends.tools import composition -from sat.core.i18n import _ - - -class RichTextEditor(BaseTextEditor, FlexTable): - """Panel for the rich text editor.""" - - def __init__(self, host, content=None, modifiedCb=None, afterEditCb=None, options=None, style=None): - """ - @param host: the SatWebFrontend instance - @param content: dict with at least a 'text' key - @param modifiedCb: method to be called when the text has been modified - @param afterEditCb: method to be called when the edition is done - @param options: list of UI options (see self.readOptions) - """ - self.host = host - self._debug = False # TODO: don't forget to set it False before commit - self.wysiwyg = False - self.__readOptions(options) - self.style = {'main': 'richTextEditor', - 'title': 'richTextTitle', - 'toolbar': 'richTextToolbar', - 'textarea': 'richTextArea'} - if isinstance(style, dict): - self.style.update(style) - self._prepareUI() - BaseTextEditor.__init__(self, content, None, modifiedCb, afterEditCb) - - def __readOptions(self, options): - """Set the internal flags according to the given options.""" - if options is None: - options = [] - self.read_only = 'read_only' in options - self.update_msg = 'update_msg' in options - self.no_title = 'no_title' in options or self.read_only - self.no_command = 'no_command' in options or self.read_only - - def _prepareUI(self, y_offset=0): - """Prepare the UI to host title panel, toolbar, text area... - @param y_offset: Y offset to start from (extra rows on top)""" - if not self.read_only: - self.title_offset = y_offset - self.toolbar_offset = self.title_offset + (0 if self.no_title else 1) - self.content_offset = self.toolbar_offset + (len(composition.RICH_SYNTAXES) if self._debug else 1) - self.command_offset = self.content_offset + 1 - else: - self.title_offset = self.toolbar_offset = self.content_offset = y_offset - self.command_offset = self.content_offset + 1 - FlexTable.__init__(self, self.command_offset + (0 if self.no_command else 1), 2) - self.addStyleName(self.style['main']) - - def addEditListener(self, listener): - """Add a method to be called whenever the text is edited. - @param listener: method taking two arguments: sender, keycode""" - BaseTextEditor.addEditListener(self, listener) - if hasattr(self, 'display'): - self.display.addEditListener(listener) - - def refresh(self, edit=None): - """Refresh the UI for edition/display mode - @param edit: set to True to display the edition mode""" - if edit is None: - edit = hasattr(self, 'textarea') and self.textarea.getVisible() - - for widget in ['title_panel', 'command']: - if hasattr(self, widget): - getattr(self, widget).setVisible(edit) - - if hasattr(self, 'toolbar'): - self.toolbar.setVisible(False) - if not hasattr(self, 'display'): - self.display = HTMLTextEditor(options={'enhance_display': False, 'listen_keyboard': False}) # for display mode - for listener in self.edit_listeners: - self.display.addEditListener(listener) - if not self.read_only and not hasattr(self, 'textarea'): - self.textarea = EditTextArea(self) # for edition mode - self.textarea.addStyleName(self.style['textarea']) - - self.getFlexCellFormatter().setColSpan(self.content_offset, 0, 2) - if edit and not self.wysiwyg: - self.textarea.setWidth('100%') # CSS width doesn't do it, don't know why - self.setWidget(self.content_offset, 0, self.textarea) - else: - self.setWidget(self.content_offset, 0, self.display) - if not edit: - return - - if not self.no_title and not hasattr(self, 'title_panel'): - self.title_panel = TitlePanel() - self.title_panel.addStyleName(self.style['title']) - self.getFlexCellFormatter().setColSpan(self.title_offset, 0, 2) - self.setWidget(self.title_offset, 0, self.title_panel) - - if not self.no_command and not hasattr(self, 'command'): - self.command = HorizontalPanel() - self.command.addStyleName("marginAuto") - self.command.add(Button("Cancel", lambda: self.edit(True, True))) - self.command.add(Button("Update" if self.update_msg else "Send message", lambda: self.edit(False))) - self.getFlexCellFormatter().setColSpan(self.command_offset, 0, 2) - self.setWidget(self.command_offset, 0, self.command) - - def setToolBar(self, syntax): - """This method is called asynchronously after the parameter - holding the rich text syntax is retrieved. It is called at - each call of self.edit(True) because the user may - have change his setting since the last time.""" - if syntax is None or syntax not in composition.RICH_SYNTAXES.keys(): - syntax = composition.RICH_SYNTAXES.keys()[0] - if hasattr(self, "toolbar") and self.toolbar.syntax == syntax: - self.toolbar.setVisible(True) - return - count = 0 - for syntax in composition.RICH_SYNTAXES.keys() if self._debug else [syntax]: - self.toolbar = HorizontalPanel() - self.toolbar.syntax = syntax - self.toolbar.addStyleName(self.style['toolbar']) - for key in composition.RICH_SYNTAXES[syntax].keys(): - self.addToolbarButton(syntax, key) - self.wysiwyg_button = CheckBox(_('WYSIWYG edition')) - wysiywgCb = lambda sender: self.setWysiwyg(sender.getChecked()) - self.wysiwyg_button.addClickListener(wysiywgCb) - self.toolbar.add(self.wysiwyg_button) - self.syntax_label = Label(_("Syntax: %s") % syntax) - self.syntax_label.addStyleName("richTextSyntaxLabel") - self.toolbar.add(self.syntax_label) - self.toolbar.setCellWidth(self.syntax_label, "100%") - self.getFlexCellFormatter().setColSpan(self.toolbar_offset + count, 0, 2) - self.setWidget(self.toolbar_offset + count, 0, self.toolbar) - count += 1 - - def setWysiwyg(self, wysiwyg, init=False): - """Toggle the edition mode between rich content syntax and wysiwyg. - @param wysiwyg: boolean value - @param init: set to True to re-init without switching the widgets.""" - def setWysiwyg(): - self.wysiwyg = wysiwyg - try: - self.wysiwyg_button.setChecked(wysiwyg) - except TypeError: - pass - try: - if wysiwyg: - self.syntax_label.addStyleName('transparent') - else: - self.syntax_label.removeStyleName('transparent') - except TypeError: - pass - if not wysiwyg: - self.display.removeStyleName('richTextWysiwyg') - - if init: - setWysiwyg() - return - - self.getFlexCellFormatter().setColSpan(self.content_offset, 0, 2) - if wysiwyg: - def syntaxConvertCb(text): - self.display.setContent({'text': text}) - self.textarea.removeFromParent() # XXX: force as it is not always done... - self.setWidget(self.content_offset, 0, self.display) - self.display.addStyleName('richTextWysiwyg') - self.display.edit(True) - content = self.getContent() - if content['text'] and content['syntax'] != C.SYNTAX_XHTML: - self.host.bridge.call('syntaxConvert', syntaxConvertCb, content['text'], content['syntax'], C.SYNTAX_XHTML) - else: - syntaxConvertCb(content['text']) - else: - syntaxConvertCb = lambda text: self.textarea.setText(text) - text = self.display.getContent()['text'] - if text and self.toolbar.syntax != C.SYNTAX_XHTML: - self.host.bridge.call('syntaxConvert', syntaxConvertCb, text) - else: - syntaxConvertCb(text) - self.setWidget(self.content_offset, 0, self.textarea) - self.textarea.setWidth('100%') # CSS width doesn't do it, don't know why - - setWysiwyg() # do it in the end because it affects self.getContent - - def addToolbarButton(self, syntax, key): - """Add a button with the defined parameters.""" - button = Button('<img src="%s" class="richTextIcon" />' % - composition.RICH_BUTTONS[key]["icon"]) - button.setTitle(composition.RICH_BUTTONS[key]["tip"]) - button.addStyleName('richTextToolButton') - self.toolbar.add(button) - - def buttonCb(): - """Generic callback for a toolbar button.""" - text = self.textarea.getText() - cursor_pos = self.textarea.getCursorPos() - selection_length = self.textarea.getSelectionLength() - data = composition.RICH_SYNTAXES[syntax][key] - if selection_length == 0: - middle_text = data[1] - else: - middle_text = text[cursor_pos:cursor_pos + selection_length] - self.textarea.setText(text[:cursor_pos] - + data[0] - + middle_text - + data[2] - + text[cursor_pos + selection_length:]) - self.textarea.setCursorPos(cursor_pos + len(data[0]) + len(middle_text)) - self.textarea.setFocus(True) - self.textarea.onKeyDown() - - def wysiwygCb(): - """Callback for a toolbar button while wysiwyg mode is enabled.""" - data = composition.COMMANDS[key] - - def execCommand(command, arg): - self.display.setFocus(True) - doc().execCommand(command, False, arg.strip() if arg else '') - # use Window.prompt instead of dialog.PromptDialog to not loose the focus - prompt = lambda command, text: execCommand(command, Window.prompt(text)) - if isinstance(data, tuple) or isinstance(data, list): - if data[1]: - prompt(data[0], data[1]) - else: - execCommand(data[0], data[2]) - else: - execCommand(data, False, '') - self.textarea.onKeyDown() - - button.addClickListener(lambda: wysiwygCb() if self.wysiwyg else buttonCb()) - - def getContent(self): - assert(hasattr(self, 'textarea')) - assert(hasattr(self, 'toolbar')) - if self.wysiwyg: - content = {'text': self.display.getContent()['text'], 'syntax': C.SYNTAX_XHTML} - else: - content = {'text': self.strproc(self.textarea.getText()), 'syntax': self.toolbar.syntax} - if hasattr(self, 'title_panel'): - content.update({'title': self.strproc(self.title_panel.getText())}) - return content - - def edit(self, edit=False, abort=False, sync=False): - """ - Remark: the editor must be visible before you call this method. - @param edit: set to True to edit the content or False to only display it - @param abort: set to True to cancel the edition and loose the changes. - If edit and abort are both True, self.abortEdition can be used to ask for a - confirmation. When edit is False and abort is True, abortion is actually done. - @param sync: set to True to cancel the edition after the content has been saved somewhere else - """ - if not (edit and abort): - self.refresh(edit) # not when we are asking for a confirmation - BaseTextEditor.edit(self, edit, abort, sync) # after the UI has been refreshed - if (edit and abort): - return # self.abortEdition is called by BaseTextEditor.edit - self.setWysiwyg(False, init=True) # after BaseTextEditor (it affects self.getContent) - if sync: - return - # the following must NOT be done at each UI refresh! - content = self._original_content - if edit: - def getParamCb(syntax): - # set the editable text in the current user-selected syntax - def syntaxConvertCb(text=None): - if text is not None: - # Important: this also update self._original_content - content.update({'text': text}) - content.update({'syntax': syntax}) - self.textarea.setText(content['text']) - if hasattr(self, 'title_panel') and 'title' in content: - self.title_panel.setText(content['title']) - self.title_panel.setStackVisible(0, content['title'] != '') - self.setToolBar(syntax) - if content['text'] and content['syntax'] != syntax: - self.host.bridge.call('syntaxConvert', syntaxConvertCb, content['text'], content['syntax']) - else: - syntaxConvertCb() - self.host.bridge.call('asyncGetParamA', getParamCb, composition.PARAM_NAME_SYNTAX, composition.PARAM_KEY_COMPOSITION) - else: - if not self.initialized: - # set the display text in XHTML only during init because a new MicroblogEntry instance is created after each modification - self.setDisplayContent() - self.display.edit(False) - - def setDisplayContent(self): - """Set the content of the HTMLTextEditor which is used for display/wysiwyg""" - content = self._original_content - text = content['text'] - if 'title' in content and content['title']: - text = '<h1>%s</h1>%s' % (html_sanitize(content['title']), content['text']) - self.display.setContent({'text': text}) - - def setFocus(self, focus): - self.textarea.setFocus(focus) - - def abortEdition(self, content): - """Ask for confirmation before closing the dialog.""" - def confirm_cb(answer): - if answer: - self.edit(False, True) - _dialog = ConfirmDialog(confirm_cb, text="Do you really want to %s?" % ("cancel your changes" if self.update_msg else "cancel this message")) - _dialog.cancel_button.setText(_("No")) - _dialog.show() - - -class RichMessageEditor(RichTextEditor): - """Use the rich text editor for sending messages with extended addressing. - Recipient panels are on top and data may be synchronized from/to the unibox.""" - - @classmethod - def getOrCreate(cls, host, parent=None, callback=None): - """Get or create the message editor associated to that host. - Add it to parent if parent is not None, otherwise display it - in a popup dialog. - @param host: the host - @param parent: parent panel (or None to display in a popup). - @return: the RichTextEditor instance if parent is not None, - otherwise a popup DialogBox containing the RichTextEditor. - """ - if not hasattr(host, 'richtext'): - modifiedCb = lambda content: True - - def afterEditCb(content): - if hasattr(host.richtext, 'popup'): - host.richtext.popup.hide() - else: - host.richtext.setVisible(False) - callback() - options = ['no_title'] - style = {'main': 'richMessageEditor', 'textarea': 'richMessageArea'} - host.richtext = RichMessageEditor(host, None, modifiedCb, afterEditCb, options, style) - - def add(widget, parent): - if widget.getParent() is not None: - if widget.getParent() != parent: - widget.removeFromParent() - parent.add(widget) - else: - parent.add(widget) - widget.setVisible(True) - widget.initialized = False # fake a new creation - widget.edit(True) - - if parent is None: - if not hasattr(host.richtext, 'popup'): - host.richtext.popup = DialogBox(autoHide=False, centered=True) - host.richtext.popup.setHTML("Compose your message") - host.richtext.popup.add(host.richtext) - add(host.richtext, host.richtext.popup) - host.richtext.popup.center() - else: - add(host.richtext, parent) - return host.richtext.popup if parent is None else host.richtext - - def _prepareUI(self, y_offset=0): - """Prepare the UI to host recipients panel, toolbar, text area... - @param y_offset: Y offset to start from (extra rows on top)""" - self.recipient_offset = y_offset - self.recipient_spacer_offset = self.recipient_offset + len(composition.RECIPIENT_TYPES) - RichTextEditor._prepareUI(self, self.recipient_spacer_offset + 1) - - def refresh(self, edit=None): - """Refresh the UI between edition/display mode - @param edit: set to True to display the edition mode""" - if edit is None: - edit = hasattr(self, 'textarea') and self.textarea.getVisible() - RichTextEditor.refresh(self, edit) - - for widget in ['recipient', 'recipient_spacer']: - if hasattr(self, widget): - getattr(self, widget).setVisible(edit) - - if not edit: - return - - if not hasattr(self, 'recipient'): - # recipient types sub-panels are automatically added by the manager - self.recipient = RecipientManager(self, self.recipient_offset) - self.recipient.createWidgets(title_format="%s: ") - self.recipient_spacer = HTML('') - self.recipient_spacer.setStyleName('recipientSpacer') - self.getFlexCellFormatter().setColSpan(self.recipient_spacer_offset, 0, 2) - self.setWidget(self.recipient_spacer_offset, 0, self.recipient_spacer) - - if not hasattr(self, 'sync_button'): - self.sync_button = Button("Back to quick box", lambda: self.edit(True, sync=True)) - self.command.insert(self.sync_button, 1) - - def syncToEditor(self): - """Synchronize from unibox.""" - def setContent(target, data): - if hasattr(self, 'recipient'): - self.recipient.setContacts({"To": [target]} if target else {}) - self.setContent({'text': data if data else '', 'syntax': ''}) - self.textarea.setText(data if data else '') - data, target = self.host.uni_box.getTargetAndData() if self.host.uni_box else (None, None) - setContent(target, data) - - def __syncToUniBox(self, recipients=None, emptyText=False): - """Synchronize to unibox if a maximum of one recipient is set. - @return True if the sync could be done, False otherwise""" - if not self.host.uni_box: - return - setText = lambda: self.host.uni_box.setText("" if emptyText else self.getContent()['text']) - if not hasattr(self, 'recipient'): - setText() - return True - if recipients is None: - recipients = self.recipient.getContacts() - target = "" - # we could eventually allow more in the future - allowed = 1 - for key in recipients: - count = len(recipients[key]) - if count == 0: - continue - allowed -= count - if allowed < 0: - return False - # TODO: change this if later more then one recipients are allowed - target = recipients[key][0] - setText() - if target == "": - return True - if target.startswith("@"): - _class = panels.MicroblogPanel - target = None if target == "@@" else target[1:] - else: - _class = panels.ChatPanel - self.host.getOrCreateLiberviaWidget(_class, target) - return True - - def syncFromEditor(self, content): - """Synchronize to unibox and close the dialog afterward. Display - a message and leave the dialog open if the sync was not possible.""" - if self.__syncToUniBox(): - self._afterEditCb(content) - return - InfoDialog("Too many recipients", - "A message with more than one direct recipient (To)," + - " or with any special recipient (Cc or Bcc), could not be" + - " stored in the quick box.\n\nPlease finish your composing" + - " in the rich text editor, and send your message directly" + - " from here.", Width="400px").center() - - def edit(self, edit=True, abort=False, sync=False): - if not edit and not abort and not sync: # force sending message even when the text has not been modified - if not self.__sendMessage(): # message has not been sent (missing information), do nothing - return - RichTextEditor.edit(self, edit, abort, sync) - - def __sendMessage(self): - """Send the message.""" - recipients = self.recipient.getContacts() - targets = [] - for addr in recipients: - for recipient in recipients[addr]: - if recipient.startswith("@"): - targets.append(("PUBLIC", None, addr) if recipient == "@@" else ("GROUP", recipient[1:], addr)) - else: - targets.append(("chat", recipient, addr)) - # check that we actually have a message target and data - content = self.getContent() - if content['text'] == "" or len(targets) == 0: - InfoDialog("Missing information", - "Some information are missing and the message hasn't been sent.", Width="400px").center() - return None - self.__syncToUniBox(recipients, emptyText=True) - extra = {'content_rich': content['text']} - if hasattr(self, 'title_panel'): - extra.update({'title': content['title']}) - self.host.send(targets, content['text'], extra=extra) - return True - - -class RecipientManager(ListManager): - """A manager for sub-panels to set the recipients for each recipient type.""" - - def __init__(self, parent, y_offset=0): - # TODO: be sure we also display empty groups and disconnected contacts + their groups - # store the full list of potential recipients (groups and contacts) - list_ = [] - list_.append("@@") - list_.extend("@%s" % group for group in parent.host.contact_panel.getGroups()) - list_.extend(contact for contact in parent.host.contact_panel.getContacts()) - ListManager.__init__(self, parent, composition.RECIPIENT_TYPES, list_, {'y': y_offset}) - - self.registerPopupMenuPanel(entries=composition.RECIPIENT_TYPES, - hide=lambda sender, key: self.__children[key]["panel"].isVisible(), - callback=self.setContactPanelVisible) - - -class EditTextArea(TextArea, KeyboardHandler): - def __init__(self, _parent): - TextArea.__init__(self) - self._parent = _parent - KeyboardHandler.__init__(self) - self.addKeyboardListener(self) - - def onKeyDown(self, sender=None, keycode=None, modifiers=None): - for listener in self._parent.edit_listeners: - listener(self, keycode)
--- a/browser_side/xmlui.py Fri May 16 11:51:10 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,418 +0,0 @@ -#!/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/>. - -from sat.core.log import getLogger -log = getLogger(__name__) -from pyjamas.ui.VerticalPanel import VerticalPanel -from pyjamas.ui.HorizontalPanel import HorizontalPanel -from pyjamas.ui.TabPanel import TabPanel -from pyjamas.ui.Grid import Grid -from pyjamas.ui.Label import Label -from pyjamas.ui.TextBox import TextBox -from pyjamas.ui.PasswordTextBox import PasswordTextBox -from pyjamas.ui.TextArea import TextArea -from pyjamas.ui.CheckBox import CheckBox -from pyjamas.ui.ListBox import ListBox -from pyjamas.ui.Button import Button -from pyjamas.ui.HTML import HTML -from nativedom import NativeDOM -from sat_frontends.tools import xmlui - - -class EmptyWidget(xmlui.EmptyWidget, Label): - - def __init__(self, parent): - Label.__init__(self, '') - - -class TextWidget(xmlui.TextWidget, Label): - - def __init__(self, parent, value): - Label.__init__(self, value) - - -class LabelWidget(xmlui.LabelWidget, TextWidget): - - def __init__(self, parent, value): - TextWidget.__init__(self, parent, value+": ") - - -class JidWidget(xmlui.JidWidget, TextWidget): - - def __init__(self, parent, value): - TextWidget.__init__(self, parent, value) - - -class DividerWidget(xmlui.DividerWidget, HTML): - - def __init__(self, parent, style='line'): - """Add a divider - - @param parent - @param style (string): one of: - - line: a simple line - - dot: a line of dots - - dash: a line of dashes - - plain: a full thick line - - blank: a blank line/space - """ - HTML.__init__(self, "<hr/>") - self.addStyleName(style) - - -class StringWidget(xmlui.StringWidget, TextBox): - - def __init__(self, parent, value): - TextBox.__init__(self) - self.setText(value) - - def _xmluiSetValue(self, value): - self.setText(value) - - def _xmluiGetValue(self): - return self.getText() - - def _xmluiOnChange(self, callback): - self.addChangeListener(callback) - - -class PasswordWidget(xmlui.PasswordWidget, PasswordTextBox): - - def __init__(self, parent, value): - PasswordTextBox.__init__(self) - self.setText(value) - - def _xmluiSetValue(self, value): - self.setText(value) - - def _xmluiGetValue(self): - return self.getText() - - def _xmluiOnChange(self, callback): - self.addChangeListener(callback) - - -class TextBoxWidget(xmlui.TextBoxWidget, TextArea): - - def __init__(self, parent, value): - TextArea.__init__(self) - self.setText(value) - - def _xmluiSetValue(self, value): - self.setText(value) - - def _xmluiGetValue(self): - return self.getText() - - def _xmluiOnChange(self, callback): - self.addChangeListener(callback) - - -class BoolWidget(xmlui.BoolWidget, CheckBox): - - def __init__(self, parent, state): - CheckBox.__init__(self) - self.setChecked(state) - - def _xmluiSetValue(self, value): - self.setChecked(value == "true") - - def _xmluiGetValue(self): - return "true" if self.isChecked() else "false" - - def _xmluiOnChange(self, callback): - self.addClickListener(callback) - - -class ButtonWidget(xmlui.ButtonWidget, Button): - - def __init__(self, parent, value, click_callback): - Button.__init__(self, value, click_callback) - - def _xmluiOnClick(self, callback): - self.addClickListener(callback) - - -class ListWidget(xmlui.ListWidget, ListBox): - - def __init__(self, parent, options, selected, flags): - ListBox.__init__(self) - multi_selection = 'single' not in flags - self.setMultipleSelect(multi_selection) - if multi_selection: - self.setVisibleItemCount(5) - for option in options: - self.addItem(option[1]) - self._xmlui_attr_map = {label: value for value, label in options} - self._xmluiSelectValues(selected) - - def _xmluiSelectValue(self, value): - """Select a value checking its item""" - try: - label = [label for label, _value in self._xmlui_attr_map.items() if _value == value][0] - except IndexError: - log.warning("Can't find value [%s] to select" % value) - return - self.selectItem(label) - - def _xmluiSelectValues(self, values): - """Select multiple values, ignore the items""" - self.setValueSelection(values) - - def _xmluiGetSelectedValues(self): - ret = [] - for label in self.getSelectedItemText(): - ret.append(self._xmlui_attr_map[label]) - return ret - - def _xmluiOnChange(self, callback): - self.addChangeListener(callback) - - def _xmluiAddValues(self, values, select=True): - selected = self._xmluiGetSelectedValues() - for value in values: - if value not in self._xmlui_attr_map.values(): - self.addItem(value) - self._xmlui_attr_map[value] = value - if value not in selected: - selected.append(value) - self._xmluiSelectValues(selected) - - -class LiberviaContainer(object): - - def _xmluiAppend(self, widget): - self.append(widget) - - -class AdvancedListContainer(xmlui.AdvancedListContainer, Grid): - - def __init__(self, parent, columns, selectable='no'): - Grid.__init__(self, 0, columns) - self.columns = columns - self.row = -1 - self.col = 0 - self._xmlui_rows_idx = [] - self._xmlui_selectable = selectable != 'no' - self._xmlui_selected_row = None - self.addTableListener(self) - if self._xmlui_selectable: - self.addStyleName('AdvancedListSelectable') - - def onCellClicked(self, grid, row, col): - if not self._xmlui_selectable: - return - self._xmlui_selected_row = row - try: - self._xmlui_select_cb(self) - except AttributeError: - log.warning("no select callback set") - - - def _xmluiAppend(self, widget): - self.setWidget(self.row, self.col, widget) - self.col += 1 - - def _xmluiAddRow(self, idx): - self.row += 1 - self.col = 0 - self._xmlui_rows_idx.insert(self.row, idx) - self.resizeRows(self.row+1) - - def _xmluiGetSelectedWidgets(self): - return [self.getWidget(self._xmlui_selected_row, col) for col in range(self.columns)] - - def _xmluiGetSelectedIndex(self): - try: - return self._xmlui_rows_idx[self._xmlui_selected_row] - except TypeError: - return None - - def _xmluiOnSelect(self, callback): - self._xmlui_select_cb = callback - - -class PairsContainer(xmlui.PairsContainer, Grid): - - def __init__(self, parent): - Grid.__init__(self, 0, 0) - self.row = 0 - self.col = 0 - - def _xmluiAppend(self, widget): - if self.col == 0: - self.resize(self.row+1, 2) - self.setWidget(self.row, self.col, widget) - self.col += 1 - if self.col == 2: - self.row +=1 - self.col = 0 - - - -class TabsContainer(LiberviaContainer, xmlui.TabsContainer, TabPanel): - - def __init__(self, parent): - TabPanel.__init__(self) - self.setStyleName('liberviaTabPanel') - - def _xmluiAddTab(self, label): - tab_panel = VerticalContainer(self) - self.add(tab_panel, label) - if len(self.getChildren()) == 1: - self.selectTab(0) - return tab_panel - - -class VerticalContainer(LiberviaContainer, xmlui.VerticalContainer, VerticalPanel): - __bases__ = (LiberviaContainer, xmlui.VerticalContainer, VerticalPanel) - - def __init__(self, parent): - VerticalPanel.__init__(self) - - -class WidgetFactory(object): - # XXX: __getattr__ doesn't work here for an unknown reason - - def createVerticalContainer(self, *args, **kwargs): - instance = VerticalContainer(*args, **kwargs) - instance._xmlui_main = self._xmlui_main - return instance - - def createPairsContainer(self, *args, **kwargs): - instance = PairsContainer(*args, **kwargs) - instance._xmlui_main = self._xmlui_main - return instance - - def createTabsContainer(self, *args, **kwargs): - instance = TabsContainer(*args, **kwargs) - instance._xmlui_main = self._xmlui_main - return instance - - def createAdvancedListContainer(self, *args, **kwargs): - instance = AdvancedListContainer(*args, **kwargs) - instance._xmlui_main = self._xmlui_main - return instance - - def createEmptyWidget(self, *args, **kwargs): - instance = EmptyWidget(*args, **kwargs) - instance._xmlui_main = self._xmlui_main - return instance - - def createTextWidget(self, *args, **kwargs): - instance = TextWidget(*args, **kwargs) - instance._xmlui_main = self._xmlui_main - return instance - - def createLabelWidget(self, *args, **kwargs): - instance = LabelWidget(*args, **kwargs) - instance._xmlui_main = self._xmlui_main - return instance - - def createJidWidget(self, *args, **kwargs): - instance = JidWidget(*args, **kwargs) - instance._xmlui_main = self._xmlui_main - return instance - - def createDividerWidget(self, *args, **kwargs): - instance = DividerWidget(*args, **kwargs) - instance._xmlui_main = self._xmlui_main - return instance - - def createStringWidget(self, *args, **kwargs): - instance = StringWidget(*args, **kwargs) - instance._xmlui_main = self._xmlui_main - return instance - - def createPasswordWidget(self, *args, **kwargs): - instance = PasswordWidget(*args, **kwargs) - instance._xmlui_main = self._xmlui_main - return instance - - def createTextBoxWidget(self, *args, **kwargs): - instance = TextBoxWidget(*args, **kwargs) - instance._xmlui_main = self._xmlui_main - return instance - - def createBoolWidget(self, *args, **kwargs): - instance = BoolWidget(*args, **kwargs) - instance._xmlui_main = self._xmlui_main - return instance - - def createButtonWidget(self, *args, **kwargs): - instance = ButtonWidget(*args, **kwargs) - instance._xmlui_main = self._xmlui_main - return instance - - def createListWidget(self, *args, **kwargs): - instance = ListWidget(*args, **kwargs) - instance._xmlui_main = self._xmlui_main - return instance - - - # def __getattr__(self, attr): - # if attr.startswith("create"): - # cls = globals()[attr[6:]] - # cls._xmlui_main = self._xmlui_main - # return cls - - -class XMLUI(xmlui.XMLUI, VerticalPanel): - widget_factory = WidgetFactory() - - def __init__(self, host, xml_data, title = None, flags = None): - self.widget_factory._xmlui_main = self - self.dom = NativeDOM() - dom_parse = lambda xml_data: self.dom.parseString(xml_data) - VerticalPanel.__init__(self) - self.setSize('100%', '100%') - xmlui.XMLUI.__init__(self, host, xml_data, title, flags, dom_parse) - - def setCloseCb(self, close_cb): - self.close_cb = close_cb - - def _xmluiClose(self): - if self.close_cb: - self.close_cb() - else: - log.warning("no close method defined") - - def _xmluiLaunchAction(self, action_id, data): - self.host.launchAction(action_id, data) - - def _xmluiSetParam(self, name, value, category): - self.host.bridge.call('setParam', None, name, value, category) - - def constructUI(self, xml_data): - super(XMLUI, self).constructUI(xml_data) - self.add(self.main_cont) - self.setCellHeight(self.main_cont, '100%') - if self.type == 'form': - hpanel = HorizontalPanel() - hpanel.setStyleName('marginAuto') - hpanel.add(Button('Submit',self.onFormSubmitted)) - if not 'NO_CANCEL' in self.flags: - hpanel.add(Button('Cancel',self.onFormCancelled)) - self.add(hpanel) - elif self.type == 'param': - assert(isinstance(self.children[0][0],TabPanel)) - hpanel = HorizontalPanel() - hpanel.add(Button('Save', self.onSaveParams)) - hpanel.add(Button('Cancel', lambda ignore: self._xmluiClose())) - self.add(hpanel)
--- a/constants.py Fri May 16 11:51:10 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,50 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Primitivus: a SAT frontend -# Copyright (C) 2009, 2010, 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/>. - -from sat.core.i18n import D_ -from sat_frontends import constants - - -class Const(constants.Const): - - APP_NAME = 'Libervia' - SERVICE_PROFILE = 'libervia' # the SàT profile that is used for exporting the service - - TIMEOUT = 300 # Session's time out, after that the user will be disconnected - LIBERVIA_DIR = "output/" - MEDIA_DIR = "media/" - AVATARS_DIR = "avatars/" - CARDS_DIR = "games/cards/tarot" - - ERRNUM_BRIDGE_ERRBACK = 0 # FIXME - ERRNUM_LIBERVIA = 0 # FIXME - - # Security limit for Libervia (get/set params) - SECURITY_LIMIT = 5 - - # Security limit for Libervia server_side - SERVER_SECURITY_LIMIT = constants.Const.NO_SECURITY_LIMIT - - # Frontend parameters - ENABLE_UNIBOX_KEY = D_("Composition") - ENABLE_UNIBOX_PARAM = D_("Enable unibox") - - # MISC - PASSWORD_MIN_LENGTH = 6 # for new account creation - LOG_OPT_SECTION = APP_NAME.lower()
--- a/libervia.py Fri May 16 11:51:10 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,903 +0,0 @@ -#!/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/>. - -import pyjd # this is dummy in pyjs - -### logging configuration ### -from browser_side import logging -logging.configure() -from sat.core.log import getLogger -log = getLogger(__name__) -### - -from pyjamas.ui.RootPanel import RootPanel -from pyjamas.ui.HTML import HTML -from pyjamas.ui.KeyboardListener import KEY_ESCAPE -from pyjamas.Timer import Timer -from pyjamas import Window, DOM -from pyjamas.JSONService import JSONProxy - -from browser_side.register import RegisterBox -from browser_side.contact import ContactPanel -from browser_side.base_widget import WidgetsPanel -from browser_side.panels import MicroblogItem -from browser_side import panels, dialog -from browser_side.jid import JID -from browser_side.xmlui import XMLUI -from browser_side.html_tools import html_sanitize -from browser_side.notification import Notification - -from sat_frontends.tools.misc import InputHistory -from sat_frontends.tools.strings import getURLParams -from sat.core.i18n import _ -from constants import Const as C - - -MAX_MBLOG_CACHE = 500 # Max microblog entries kept in memories - -# Set to true to not create a new LiberviaWidget when a similar one -# already exist (i.e. a chat panel with the same target). Instead -# the existing widget will be eventually removed from its parent -# and added to new WidgetsPanel, or replaced to the expected -# position if the previous and the new parent are the same. -REUSE_EXISTING_LIBERVIA_WIDGETS = True - - -class LiberviaJsonProxy(JSONProxy): - def __init__(self, *args, **kwargs): - JSONProxy.__init__(self, *args, **kwargs) - self.handler = self - self.cb = {} - self.eb = {} - - def call(self, method, cb, *args): - _id = self.callMethod(method, args) - if cb: - if isinstance(cb, tuple): - if len(cb) != 2: - log.error("tuple syntax for bridge.call is (callback, errback), aborting") - return - if cb[0] is not None: - self.cb[_id] = cb[0] - self.eb[_id] = cb[1] - else: - self.cb[_id] = cb - - def onRemoteResponse(self, response, request_info): - if request_info.id in self.cb: - _cb = self.cb[request_info.id] - # if isinstance(_cb, tuple): - # #we have arguments attached to the callback - # #we send them after the answer - # callback, args = _cb - # callback(response, *args) - # else: - # #No additional argument, we call directly the callback - _cb(response) - del self.cb[request_info.id] - if request_info.id in self.eb: - del self.eb[request_info.id] - - def onRemoteError(self, code, errobj, request_info): - """def dump(obj): - print "\n\nDUMPING %s\n\n" % obj - for i in dir(obj): - print "%s: %s" % (i, getattr(obj,i))""" - if request_info.id in self.eb: - _eb = self.eb[request_info.id] - _eb((code, errobj)) - del self.cb[request_info.id] - del self.eb[request_info.id] - else: - if code != 0: - log.error("Internal server error") - """for o in code, error, request_info: - dump(o)""" - else: - if isinstance(errobj['message'], dict): - log.error("Error %s: %s" % (errobj['message']['faultCode'], errobj['message']['faultString'])) - else: - log.error("%s" % errobj['message']) - - -class RegisterCall(LiberviaJsonProxy): - def __init__(self): - LiberviaJsonProxy.__init__(self, "/register_api", - ["isRegistered", "isConnected", "connect", "registerParams", "getMenus"]) - - -class BridgeCall(LiberviaJsonProxy): - def __init__(self): - LiberviaJsonProxy.__init__(self, "/json_api", - ["getContacts", "addContact", "sendMessage", "sendMblog", "sendMblogComment", - "getLastMblogs", "getMassiveLastMblogs", "getMblogComments", "getProfileJid", - "getHistory", "getPresenceStatuses", "joinMUC", "mucLeave", "getRoomsJoined", - "inviteMUC", "launchTarotGame", "getTarotCardsPaths", "tarotGameReady", - "tarotGamePlayCards", "launchRadioCollective", "getMblogs", "getMblogsWithComments", - "getWaitingSub", "subscription", "delContact", "updateContact", "getCard", - "getEntityData", "getParamsUI", "asyncGetParamA", "setParam", "launchAction", - "disconnect", "chatStateComposing", "getNewAccountDomain", "confirmationAnswer", - "syntaxConvert", "getAccountDialogUI", - ]) - - -class BridgeSignals(LiberviaJsonProxy): - RETRY_BASE_DELAY = 1000 - - def __init__(self, host): - self.host = host - self.retry_delay = self.RETRY_BASE_DELAY - LiberviaJsonProxy.__init__(self, "/json_signal_api", - ["getSignals"]) - - def onRemoteResponse(self, response, request_info): - self.retry_delay = self.RETRY_BASE_DELAY - LiberviaJsonProxy.onRemoteResponse(self, response, request_info) - - def onRemoteError(self, code, errobj, request_info): - if errobj['message'] == 'Empty Response': - Window.getLocation().reload() # XXX: reset page in case of session ended. - # FIXME: Should be done more properly without hard reload - LiberviaJsonProxy.onRemoteError(self, code, errobj, request_info) - #we now try to reconnect - if isinstance(errobj['message'], dict) and errobj['message']['faultCode'] == 0: - Window.alert('You are not allowed to connect to server') - else: - def _timerCb(timer): - self.host.bridge_signals.call('getSignals', self.host._getSignalsCB) - Timer(notify=_timerCb).schedule(self.retry_delay) - self.retry_delay *= 2 - - -class SatWebFrontend(InputHistory): - def onModuleLoad(self): - log.info("============ onModuleLoad ==============") - panels.ChatPanel.registerClass() - panels.MicroblogPanel.registerClass() - self.whoami = None - self._selected_listeners = set() - self.bridge = BridgeCall() - self.bridge_signals = BridgeSignals(self) - self.uni_box = None - self.status_panel = HTML('<br />') - self.contact_panel = ContactPanel(self) - self.panel = panels.MainPanel(self) - self.discuss_panel = self.panel.discuss_panel - self.tab_panel = self.panel.tab_panel - self.tab_panel.addTabListener(self) - self.libervia_widgets = set() # keep track of all actives LiberviaWidgets - self.room_list = [] # list of rooms - self.mblog_cache = [] # used to keep our own blog entries in memory, to show them in new mblog panel - self.avatars_cache = {} # keep track of jid's avatar hash (key=jid, value=file) - self._register_box = None - RootPanel().add(self.panel) - self.notification = Notification() - DOM.addEventPreview(self) - self._register = RegisterCall() - self._register.call('getMenus', self.panel.menu.createMenus) - self._register.call('registerParams', None) - self._register.call('isRegistered', self._isRegisteredCB) - self.initialised = False - self.init_cache = [] # used to cache events until initialisation is done - # define here the parameters that have an incidende to UI refresh - self.params_ui = {"unibox": {"name": C.ENABLE_UNIBOX_PARAM, - "category": C.ENABLE_UNIBOX_KEY, - "cast": lambda value: value == 'true', - "value": None - } - } - - def addSelectedListener(self, callback): - self._selected_listeners.add(callback) - - def getSelected(self): - wid = self.tab_panel.getCurrentPanel() - if not isinstance(wid, WidgetsPanel): - log.error("Tab widget is not a WidgetsPanel, can't get selected widget") - return None - return wid.selected - - def setSelected(self, widget): - """Define the selected widget""" - widgets_panel = self.tab_panel.getCurrentPanel() - if not isinstance(widgets_panel, WidgetsPanel): - return - - selected = widgets_panel.selected - - if selected == widget: - return - - if selected: - selected.removeStyleName('selected_widget') - - widgets_panel.selected = widget - - if widget: - widgets_panel.selected.addStyleName('selected_widget') - - for callback in self._selected_listeners: - callback(widget) - - def resize(self): - """Resize elements""" - Window.onResize() - - def onBeforeTabSelected(self, sender, tab_index): - return True - - def onTabSelected(self, sender, tab_index): - selected = self.getSelected() - for callback in self._selected_listeners: - callback(selected) - - def onEventPreview(self, event): - if event.type in ["keydown", "keypress", "keyup"] and event.keyCode == KEY_ESCAPE: - #needed to prevent request cancellation in Firefox - event.preventDefault() - return True - - def getAvatar(self, jid_str): - """Return avatar of a jid if in cache, else ask for it""" - def dataReceived(result): - if 'avatar' in result: - self._entityDataUpdatedCb(jid_str, 'avatar', result['avatar']) - else: - self.bridge.call("getCard", None, jid_str) - - def avatarError(error_data): - # The jid is maybe not in our roster, we ask for the VCard - self.bridge.call("getCard", None, jid_str) - - if jid_str not in self.avatars_cache: - self.bridge.call('getEntityData', (dataReceived, avatarError), jid_str, ['avatar']) - self.avatars_cache[jid_str] = "/media/icons/tango/emotes/64/face-plain.png" - return self.avatars_cache[jid_str] - - def registerWidget(self, wid): - log.debug("Registering %s" % wid.getDebugName()) - self.libervia_widgets.add(wid) - - def unregisterWidget(self, wid): - try: - self.libervia_widgets.remove(wid) - except KeyError: - log.warning('trying to remove a non registered Widget: %s' % wid.getDebugName()) - - def refresh(self): - """Refresh the general display.""" - self.panel.refresh() - if self.params_ui['unibox']['value']: - self.uni_box = self.panel.unibox_panel.unibox - else: - self.uni_box = None - for lib_wid in self.libervia_widgets: - lib_wid.refresh() - self.resize() - - def addTab(self, label, wid, select=True): - """Create a new tab and eventually add a widget in - @param label: label of the tab - @param wid: LiberviaWidget to add - @param select: True to select the added tab - """ - widgets_panel = WidgetsPanel(self) - self.tab_panel.add(widgets_panel, label) - widgets_panel.addWidget(wid) - if select: - self.tab_panel.selectTab(self.tab_panel.getWidgetCount() - 1) - return widgets_panel - - def addWidget(self, wid, tab_index=None): - """ Add a widget at the bottom of the current or specified tab - @param wid: LiberviaWidget to add - @param tab_index: index of the tab to add the widget to""" - if tab_index is None or tab_index < 0 or tab_index >= self.tab_panel.getWidgetCount(): - panel = self.tab_panel.getCurrentPanel() - else: - panel = self.tab_panel.tabBar.getTabWidget(tab_index) - panel.addWidget(wid) - - def displayNotification(self, title, body): - self.notification.notify(title, body) - - def _isRegisteredCB(self, result): - registered, warning = result - if not registered: - self._register_box = RegisterBox(self.logged) - self._register_box.centerBox() - self._register_box.show() - if warning: - dialog.InfoDialog(_('Security warning'), warning).show() - self._tryAutoConnect(skip_validation=not not warning) - else: - self._register.call('isConnected', self._isConnectedCB) - - def _isConnectedCB(self, connected): - if not connected: - self._register.call('connect', lambda x: self.logged()) - else: - self.logged() - - def logged(self): - if self._register_box: - self._register_box.hide() - del self._register_box # don't work if self._register_box is None - - # display the real presence status panel - self.panel.header.remove(self.status_panel) - self.status_panel = panels.PresenceStatusPanel(self) - self.panel.header.add(self.status_panel) - - #it's time to fill the page - self.bridge.call('getContacts', self._getContactsCB) - self.bridge.call('getParamsUI', self._getParamsUICB) - self.bridge_signals.call('getSignals', self._getSignalsCB) - #We want to know our own jid - self.bridge.call('getProfileJid', self._getProfileJidCB) - - def domain_cb(value): - self._defaultDomain = value - log.info("new account domain: %s" % value) - - def domain_eb(value): - self._defaultDomain = "libervia.org" - - self.bridge.call("getNewAccountDomain", (domain_cb, domain_eb)) - self.discuss_panel.addWidget(panels.MicroblogPanel(self, [])) - - # get ui params and refresh the display - count = 0 # used to do something similar to DeferredList - - def params_ui_cb(param, value=None): - count += 1 - refresh = count == len(self.params_ui) - self._paramUpdate(param['name'], value, param['category'], refresh) - for param in self.params_ui: - self.bridge.call('asyncGetParamA', lambda value: params_ui_cb(self.params_ui[param], value), - self.params_ui[param]['name'], self.params_ui[param]['category']) - - def _tryAutoConnect(self, skip_validation=False): - """This method retrieve the eventual URL parameters to auto-connect the user. - @param skip_validation: if True, set the form values but do not validate it - """ - params = getURLParams(Window.getLocation().getSearch()) - if "login" in params: - self._register_box._form.login_box.setText(params["login"]) - self._register_box._form.login_pass_box.setFocus(True) - if "passwd" in params: - # try to connect - self._register_box._form.login_pass_box.setText(params["passwd"]) - if not skip_validation: - self._register_box._form.onLogin(None) - return True - else: - # this would eventually set the browser saved password - Timer(5, lambda: self._register_box._form.login_pass_box.setFocus(True)) - - def _actionCb(self, data): - if not data: - # action was a one shot, nothing to do - pass - elif "xmlui" in data: - ui = XMLUI(self, xml_data = data['xmlui']) - options = ['NO_CLOSE'] if ui.type == 'form' else [] - _dialog = dialog.GenericDialog(ui.title, ui, options=options) - ui.setCloseCb(_dialog.close) - _dialog.show() - else: - dialog.InfoDialog("Error", - "Unmanaged action result", Width="400px").center() - - def _actionEb(self, err_data): - err_code, err_obj = err_data - dialog.InfoDialog("Error", - str(err_obj), Width="400px").center() - - def launchAction(self, callback_id, data): - """ Launch a dynamic action - @param callback_id: id of the action to launch - @param data: data needed only for certain actions - - """ - if data is None: - data = {} - self.bridge.call('launchAction', (self._actionCb, self._actionEb), callback_id, data) - - def _getContactsCB(self, contacts_data): - for contact in contacts_data: - jid, attributes, groups = contact - self._newContactCb(jid, attributes, groups) - - def _getSignalsCB(self, signal_data): - self.bridge_signals.call('getSignals', self._getSignalsCB) - log.debug("Got signal ==> name: %s, params: %s" % (signal_data[0], signal_data[1])) - name, args = signal_data - if name == 'personalEvent': - self._personalEventCb(*args) - elif name == 'newMessage': - self._newMessageCb(*args) - elif name == 'presenceUpdate': - self._presenceUpdateCb(*args) - elif name == 'paramUpdate': - self._paramUpdate(*args) - elif name == 'roomJoined': - self._roomJoinedCb(*args) - elif name == 'roomLeft': - self._roomLeftCb(*args) - elif name == 'roomUserJoined': - self._roomUserJoinedCb(*args) - elif name == 'roomUserLeft': - self._roomUserLeftCb(*args) - elif name == 'roomUserChangedNick': - self._roomUserChangedNickCb(*args) - elif name == 'askConfirmation': - self._askConfirmation(*args) - elif name == 'newAlert': - self._newAlert(*args) - elif name == 'tarotGamePlayers': - self._tarotGameStartedCb(True, *args) - elif name == 'tarotGameStarted': - self._tarotGameStartedCb(False, *args) - elif name == 'tarotGameNew' or \ - name == 'tarotGameChooseContrat' or \ - name == 'tarotGameShowCards' or \ - name == 'tarotGameInvalidCards' or \ - name == 'tarotGameCardsPlayed' or \ - name == 'tarotGameYourTurn' or \ - name == 'tarotGameScore': - self._tarotGameGenericCb(name, args[0], args[1:]) - elif name == 'radiocolPlayers': - self._radioColStartedCb(True, *args) - elif name == 'radiocolStarted': - self._radioColStartedCb(False, *args) - elif name == 'radiocolPreload': - self._radioColGenericCb(name, args[0], args[1:]) - elif name == 'radiocolPlay': - self._radioColGenericCb(name, args[0], args[1:]) - elif name == 'radiocolNoUpload': - self._radioColGenericCb(name, args[0], args[1:]) - elif name == 'radiocolUploadOk': - self._radioColGenericCb(name, args[0], args[1:]) - elif name == 'radiocolSongRejected': - self._radioColGenericCb(name, args[0], args[1:]) - elif name == 'subscribe': - self._subscribeCb(*args) - elif name == 'contactDeleted': - self._contactDeletedCb(*args) - elif name == 'newContact': - self._newContactCb(*args) - elif name == 'entityDataUpdated': - self._entityDataUpdatedCb(*args) - elif name == 'chatStateReceived': - self._chatStateReceivedCb(*args) - - def _getParamsUICB(self, xmlui): - """Hide the parameters item if there's nothing to display""" - if not xmlui: - self.panel.menu.removeItemParams() - - def _ownBlogsFills(self, mblogs): - #put our own microblogs in cache, then fill all panels with them - for publisher in mblogs: - for mblog in mblogs[publisher]: - if not mblog.has_key('content'): - log.warning("No content found in microblog [%s]" % mblog) - continue - if mblog.has_key('groups'): - _groups = set(mblog['groups'].split() if mblog['groups'] else []) - else: - _groups = None - mblog_entry = MicroblogItem(mblog) - 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)] - for lib_wid in self.libervia_widgets: - if isinstance(lib_wid, panels.MicroblogPanel): - self.FillMicroblogPanel(lib_wid) - self.initialised = True # initialisation phase is finished here - for event_data in self.init_cache: # so we have to send all the cached events - self._personalEventCb(*event_data) - del self.init_cache - - def _getProfileJidCB(self, jid): - self.whoami = JID(jid) - #we can now ask our status - self.bridge.call('getPresenceStatuses', self._getPresenceStatusesCb) - #the rooms where we are - self.bridge.call('getRoomsJoined', self._getRoomsJoinedCb) - #and if there is any subscription request waiting for us - self.bridge.call('getWaitingSub', self._getWaitingSubCb) - #we fill the panels already here - for lib_wid in self.libervia_widgets: - if isinstance(lib_wid, panels.MicroblogPanel): - if lib_wid.accept_all(): - self.bridge.call('getMassiveLastMblogs', lib_wid.massiveInsert, 'ALL', [], 10) - else: - self.bridge.call('getMassiveLastMblogs', lib_wid.massiveInsert, 'GROUP', lib_wid.accepted_groups, 10) - - #we ask for our own microblogs: - self.bridge.call('getMassiveLastMblogs', self._ownBlogsFills, 'JID', [self.whoami.bare], 10) - - ## Signals callbacks ## - - def _personalEventCb(self, sender, event_type, data): - 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: - log.warning("No content found in microblog data") - return - if 'groups' in data: - _groups = set(data['groups'].split() if data['groups'] else []) - else: - _groups = None - mblog_entry = MicroblogItem(data) - - for lib_wid in self.libervia_widgets: - if isinstance(lib_wid, panels.MicroblogPanel): - self.addBlogEntry(lib_wid, sender, _groups, mblog_entry) - - if sender == self.whoami.bare: - 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']) - log.debug("%s %s %s" % (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 - @param mblog_panel: MicroblogPanel instance - @param sender: jid of the entry sender - @param _groups: groups which can receive this entry - @param mblog_entry: MicroblogItem instance""" - if mblog_entry.type == "comment" or mblog_panel.isJidAccepted(sender) or (_groups == None and self.whoami and sender == self.whoami.bare) \ - or (_groups and _groups.intersection(mblog_panel.accepted_groups)): - mblog_panel.addEntry(mblog_entry) - - def FillMicroblogPanel(self, mblog_panel): - """Fill a microblog panel with entries in cache - @param mblog_panel: MicroblogPanel instance - """ - #XXX: only our own entries are cached - for cache_entry in self.mblog_cache: - _groups, mblog_entry = cache_entry - self.addBlogEntry(mblog_panel, self.whoami.bare, *cache_entry) - - def getEntityMBlog(self, entity): - log.info("geting mblog for entity [%s]" % (entity,)) - for lib_wid in self.libervia_widgets: - if isinstance(lib_wid, panels.MicroblogPanel): - if lib_wid.isJidAccepted(entity): - self.bridge.call('getMassiveLastMblogs', lib_wid.massiveInsert, 'JID', [entity], 10) - - def getLiberviaWidget(self, class_, entity, ignoreOtherTabs=True): - """Get the corresponding panel if it exists. - @param class_: class of the panel (ChatPanel, MicroblogPanel...) - @param entity: polymorphic parameter, see class_.matchEntity. - @param ignoreOtherTabs: if True, the widgets that are not - contained by the currently selected tab will be ignored - @return: the existing widget that has been found or None.""" - selected_tab = self.tab_panel.getCurrentPanel() - for lib_wid in self.libervia_widgets: - parent = lib_wid.getWidgetsPanel(verbose=False) - if parent is None or (ignoreOtherTabs and parent != selected_tab): - # do not return a widget that is not in the currently selected tab - continue - if isinstance(lib_wid, class_): - try: - if lib_wid.matchEntity(entity): - log.debug("existing widget found: %s" % lib_wid.getDebugName()) - return lib_wid - except AttributeError as e: - e.stack_list() - return None - return None - - def getOrCreateLiberviaWidget(self, class_, entity, select=True, new_tab=None): - """Get the matching LiberviaWidget if it exists, or create a new one. - @param class_: class of the panel (ChatPanel, MicroblogPanel...) - @param entity: polymorphic parameter, see class_.matchEntity. - @param select: if True, select the widget that has been found or created - @param new_tab: if not None, a widget which is created is created in - a new tab. In that case new_tab is a unicode to label that new tab. - If new_tab is not None and a widget is found, no tab is created. - @return: the newly created wigdet if REUSE_EXISTING_LIBERVIA_WIDGETS - is set to False or if the widget has not been found, the existing - widget that has been found otherwise.""" - lib_wid = None - tab = None - if REUSE_EXISTING_LIBERVIA_WIDGETS: - lib_wid = self.getLiberviaWidget(class_, entity, new_tab is None) - if lib_wid is None: # create a new widget - lib_wid = class_.createPanel(self, entity[0] if isinstance(entity, tuple) else entity) - if new_tab is None: - self.addWidget(lib_wid) - else: - tab = self.addTab(new_tab, lib_wid, False) - else: # reuse existing widget - tab = lib_wid.getWidgetsPanel(verbose=False) - if new_tab is None: - if tab is not None: - tab.removeWidget(lib_wid) - self.addWidget(lib_wid) - if select: - if new_tab is not None: - self.tab_panel.selectTab(tab) - # must be done after the widget is added, - # for example to scroll to the bottom - self.setSelected(lib_wid) - lib_wid.refresh() - return lib_wid - - def _newMessageCb(self, from_jid, msg, msg_type, to_jid, extra): - _from = JID(from_jid) - _to = JID(to_jid) - other = _to if _from.bare == self.whoami.bare else _from - lib_wid = self.getLiberviaWidget(panels.ChatPanel, other, ignoreOtherTabs=False) - self.displayNotification(_from, msg) - if lib_wid is not None: - lib_wid.printMessage(_from, msg, extra) - else: - # The message has not been shown, we must indicate it - self.contact_panel.setContactMessageWaiting(other.bare, True) - - def _presenceUpdateCb(self, entity, show, priority, statuses): - entity_jid = JID(entity) - if self.whoami and self.whoami == entity_jid: # XXX: QnD way to get our presence/status - self.status_panel.setPresence(show) - if statuses: - self.status_panel.setStatus(statuses.values()[0]) - else: - self.contact_panel.setConnected(entity_jid.bare, entity_jid.resource, show, priority, statuses) - - def _roomJoinedCb(self, room_jid, room_nicks, user_nick): - _target = JID(room_jid) - if _target not in self.room_list: - self.room_list.append(_target) - chat_panel = panels.ChatPanel(self, _target, type_='group') - chat_panel.setUserNick(user_nick) - if _target.node.startswith('sat_tarot_'): #XXX: it's not really beautiful, but it works :) - self.addTab("Tarot", chat_panel) - elif _target.node.startswith('sat_radiocol_'): - self.addTab("Radio collective", chat_panel) - else: - self.addTab(_target.node, chat_panel) - chat_panel.setPresents(room_nicks) - chat_panel.historyPrint() - chat_panel.refresh() - - def _roomLeftCb(self, room_jid, room_nicks, user_nick): - # FIXME: room_list contains JID instances so why MUST we do - # 'remove(room_jid)' and not 'remove(JID(room_jid))' ????!! - # This looks like a pyjamas bug --> check/report - try: - self.room_list.remove(room_jid) - except KeyError: - pass - - def _roomUserJoinedCb(self, room_jid_s, user_nick, user_data): - for lib_wid in self.libervia_widgets: - if isinstance(lib_wid, panels.ChatPanel) and lib_wid.type == 'group' and lib_wid.target.bare == room_jid_s: - lib_wid.userJoined(user_nick, user_data) - - def _roomUserLeftCb(self, room_jid_s, user_nick, user_data): - for lib_wid in self.libervia_widgets: - if isinstance(lib_wid, panels.ChatPanel) and lib_wid.type == 'group' and lib_wid.target.bare == room_jid_s: - lib_wid.userLeft(user_nick, user_data) - - def _roomUserChangedNickCb(self, room_jid_s, old_nick, new_nick): - """Called when an user joined a MUC room""" - for lib_wid in self.libervia_widgets: - if isinstance(lib_wid, panels.ChatPanel) and lib_wid.type == 'group' and lib_wid.target.bare == room_jid_s: - lib_wid.changeUserNick(old_nick, new_nick) - - def _tarotGameStartedCb(self, waiting, room_jid_s, referee, players): - for lib_wid in self.libervia_widgets: - if isinstance(lib_wid, panels.ChatPanel) and lib_wid.type == 'group' and lib_wid.target.bare == room_jid_s: - lib_wid.startGame("Tarot", waiting, referee, players) - - def _tarotGameGenericCb(self, event_name, room_jid_s, args): - for lib_wid in self.libervia_widgets: - if isinstance(lib_wid, panels.ChatPanel) and lib_wid.type == 'group' and lib_wid.target.bare == room_jid_s: - getattr(lib_wid.getGame("Tarot"), event_name)(*args) - - def _radioColStartedCb(self, waiting, room_jid_s, referee, players, queue_data): - for lib_wid in self.libervia_widgets: - if isinstance(lib_wid, panels.ChatPanel) and lib_wid.type == 'group' and lib_wid.target.bare == room_jid_s: - lib_wid.startGame("RadioCol", waiting, referee, players, queue_data) - - def _radioColGenericCb(self, event_name, room_jid_s, args): - for lib_wid in self.libervia_widgets: - if isinstance(lib_wid, panels.ChatPanel) and lib_wid.type == 'group' and lib_wid.target.bare == room_jid_s: - getattr(lib_wid.getGame("RadioCol"), event_name)(*args) - - def _getPresenceStatusesCb(self, presence_data): - for entity in presence_data: - for resource in presence_data[entity]: - args = presence_data[entity][resource] - self._presenceUpdateCb("%s/%s" % (entity, resource), *args) - - def _getRoomsJoinedCb(self, room_data): - for room in room_data: - self._roomJoinedCb(*room) - - def _getWaitingSubCb(self, waiting_sub): - for sub in waiting_sub: - self._subscribeCb(waiting_sub[sub], sub) - - def _subscribeCb(self, sub_type, entity): - if sub_type == 'subscribed': - dialog.InfoDialog('Subscription confirmation', 'The contact <b>%s</b> has added you to his/her contact list' % html_sanitize(entity)).show() - self.getEntityMBlog(entity) - - elif sub_type == 'unsubscribed': - dialog.InfoDialog('Subscription refusal', 'The contact <b>%s</b> has refused to add you in his/her contact list' % html_sanitize(entity)).show() - #TODO: remove microblogs from panels - - elif sub_type == 'subscribe': - #The user want to subscribe to our presence - _dialog = None - msg = HTML('The contact <b>%s</b> want to add you in his/her contact list, do you accept ?' % html_sanitize(entity)) - - def ok_cb(ignore): - self.bridge.call('subscription', None, "subscribed", entity, '', _dialog.getSelectedGroups()) - def cancel_cb(ignore): - self.bridge.call('subscription', None, "unsubscribed", entity, '', '') - - _dialog = dialog.GroupSelector([msg], self.contact_panel.getGroups(), [], "Add", ok_cb, cancel_cb) - _dialog.setHTML('<b>Add contact request</b>') - _dialog.show() - - def _contactDeletedCb(self, entity): - self.contact_panel.removeContact(entity) - - def _newContactCb(self, contact, attributes, groups): - self.contact_panel.updateContact(contact, attributes, groups) - - def _entityDataUpdatedCb(self, entity_jid_s, key, value): - if key == "avatar": - avatar = '/avatars/%s' % value - - self.avatars_cache[entity_jid_s] = avatar - - for lib_wid in self.libervia_widgets: - if isinstance(lib_wid, panels.MicroblogPanel): - if lib_wid.isJidAccepted(entity_jid_s) or (self.whoami and entity_jid_s == self.whoami.bare): - lib_wid.updateValue('avatar', entity_jid_s, avatar) - - def _chatStateReceivedCb(self, from_jid_s, state): - """Callback when a new chat state is received. - @param from_jid_s: JID of the contact who sent his state, or '@ALL@' - @param state: new state (string) - """ - if from_jid_s == '@ALL@': - target = '@ALL@' - nick = C.ALL_OCCUPANTS - else: - from_jid = JID(from_jid_s) - target = from_jid.bare - nick = from_jid.resource - - for lib_wid in self.libervia_widgets: - if isinstance(lib_wid, panels.ChatPanel): - if target == '@ALL' or target == lib_wid.target.bare: - if lib_wid.type == 'one2one': - lib_wid.setState(state) - elif lib_wid.type == 'group': - lib_wid.setState(state, nick=nick) - - def _askConfirmation(self, confirmation_id, confirmation_type, data): - answer_data = {} - - def confirm_cb(result): - self.bridge.call('confirmationAnswer', None, confirmation_id, result, answer_data) - - if confirmation_type == "YES/NO": - dialog.ConfirmDialog(confirm_cb, text=data["message"], title=data["title"]).show() - - def _newAlert(self, message, title, alert_type): - dialog.InfoDialog(title, message).show() - - def _paramUpdate(self, name, value, category, refresh=True): - """This is called when the paramUpdate signal is received, but also - during initialization when the UI parameters values are retrieved. - @param refresh: set to True to refresh the general UI - """ - for param in self.params_ui: - if name == self.params_ui[param]['name']: - self.params_ui[param]['value'] = self.params_ui[param]['cast'](value) - if refresh: - self.refresh() - break - - def sendError(self, errorData): - dialog.InfoDialog("Error while sending message", - "Your message can't be sent", Width="400px").center() - log.error("sendError: %s" % str(errorData)) - - def send(self, targets, text, extra={}): - """Send a message to any target type. - @param targets: list of tuples (type, entities, addr) with: - - type in ("PUBLIC", "GROUP", "COMMENT", "STATUS" , "groupchat" , "chat") - - entities could be a JID, a list groups, a node hash... depending the target - - addr in ("To", "Cc", "Bcc") - ignore case - @param text: the message content - @param extra: options - """ - # FIXME: too many magic strings, we should use constants instead - addresses = [] - for target in targets: - type_, entities, addr = target[0], target[1], 'to' if len(target) < 3 else target[2].lower() - if type_ in ("PUBLIC", "GROUP"): - self.bridge.call("sendMblog", None, type_, entities if type_ == "GROUP" else None, text, extra) - elif type_ == "COMMENT": - self.bridge.call("sendMblogComment", None, entities, text, extra) - elif type_ == "STATUS": - self.bridge.call('setStatus', None, self.status_panel.presence, text) - elif type_ in ("groupchat", "chat"): - addresses.append((addr, entities)) - else: - log.error("Unknown target type") - if addresses: - if len(addresses) == 1 and addresses[0][0] == 'to': - self.bridge.call('sendMessage', (None, self.sendError), addresses[0][1], text, '', type_, extra) - else: - extra.update({'address': '\n'.join([('%s:%s' % entry) for entry in addresses])}) - self.bridge.call('sendMessage', (None, self.sendError), self.whoami.domain, text, '', type_, extra) - - def showWarning(self, type_=None, msg=None): - """Display a popup information message, e.g. to notify the recipient of a message being composed. - If type_ is None, a popup being currently displayed will be hidden. - @type_: a type determining the CSS style to be applied (see WarningPopup.showWarning) - @msg: message to be displayed - """ - if not hasattr(self, "warning_popup"): - self.warning_popup = panels.WarningPopup() - self.warning_popup.showWarning(type_, msg) - - -if __name__ == '__main__': - pyjd.setup("http://localhost:8080/libervia.html") - app = SatWebFrontend() - app.onModuleLoad() - pyjd.run()
--- a/libervia_server/__init__.py Fri May 16 11:51:10 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1142 +0,0 @@ -#!/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/>. - -from twisted.application import service -from twisted.internet import glib2reactor -glib2reactor.install() -from twisted.internet import reactor, defer -from twisted.web import server -from twisted.web.static import File -from twisted.web.resource import Resource, NoResource -from twisted.web.util import Redirect, redirectTo -from twisted.python.components import registerAdapter -from twisted.python.failure import Failure -from twisted.words.protocols.jabber.jid import JID -from txjsonrpc.web import jsonrpc -from txjsonrpc import jsonrpclib - -from sat.core.log import getLogger -log = getLogger(__name__) -import re -import glob -import os.path -import sys -import tempfile -import shutil -import uuid -from zope.interface import Interface, Attribute, implements -from xml.dom import minidom -from httplib import HTTPS_PORT - -from constants import Const as C -from libervia_server.blog import MicroBlog -from sat_frontends.bridge.DBus import DBusBridgeFrontend, BridgeExceptionNoService -from sat.core.i18n import _, D_ -from sat.tools.xml_tools import paramsXML2XMLUI -try: - import OpenSSL - from twisted.internet import ssl - ssl_available = True -except: - ssl_available = False - - -class ISATSession(Interface): - profile = Attribute("Sat profile") - jid = Attribute("JID associated with the profile") - -class SATSession(object): - implements(ISATSession) - def __init__(self, session): - self.profile = None - self.jid = None - -class LiberviaSession(server.Session): - sessionTimeout = C.TIMEOUT - - def __init__(self, *args, **kwargs): - self.__lock = False - server.Session.__init__(self, *args, **kwargs) - - def lock(self): - """Prevent session from expiring""" - self.__lock = True - self._expireCall.reset(sys.maxint) - - def unlock(self): - """Allow session to expire again, and touch it""" - self.__lock = False - self.touch() - - def touch(self): - if not self.__lock: - server.Session.touch(self) - -class ProtectedFile(File): - """A File class which doens't show directory listing""" - - def directoryListing(self): - return NoResource() - -class SATActionIDHandler(object): - """Manage SàT action action_id lifecycle""" - ID_LIFETIME = 30 #after this time (in seconds), action_id will be suppressed and action result will be ignored - - def __init__(self): - self.waiting_ids = {} - - def waitForId(self, callback, action_id, profile, *args, **kwargs): - """Wait for an action result - @param callback: method to call when action gave a result back - @param action_id: action_id to wait for - @param profile: %(doc_profile)s - @param *args: additional argument to pass to callback - @param **kwargs: idem""" - action_tuple = (action_id, profile) - self.waiting_ids[action_tuple] = (callback, args, kwargs) - reactor.callLater(self.ID_LIFETIME, self.purgeID, action_tuple) - - def purgeID(self, action_tuple): - """Called when an action_id has not be handled in time""" - if action_tuple in self.waiting_ids: - log.warning ("action of action_id %s [%s] has not been managed, action_id is now ignored" % action_tuple) - del self.waiting_ids[action_tuple] - - def actionResultCb(self, answer_type, action_id, data, profile): - """Manage the actionResult signal""" - action_tuple = (action_id, profile) - if action_tuple in self.waiting_ids: - callback, args, kwargs = self.waiting_ids[action_tuple] - del self.waiting_ids[action_tuple] - callback(answer_type, action_id, data, *args, **kwargs) - -class JSONRPCMethodManager(jsonrpc.JSONRPC): - - def __init__(self, sat_host): - jsonrpc.JSONRPC.__init__(self) - self.sat_host=sat_host - - def asyncBridgeCall(self, method_name, *args, **kwargs): - """Call an asynchrone bridge method and return a deferred - @param method_name: name of the method as a unicode - @return: a deferred which trigger the result - - """ - d = defer.Deferred() - - def _callback(*args): - if not args: - d.callback(None) - else: - if len(args) != 1: - Exception("Multiple return arguments not supported") - d.callback(args[0]) - - def _errback(result): - d.errback(Failure(jsonrpclib.Fault(C.ERRNUM_BRIDGE_ERRBACK, unicode(result)))) - - kwargs["callback"] = _callback - kwargs["errback"] = _errback - getattr(self.sat_host.bridge, method_name)(*args, **kwargs) - return d - - -class MethodHandler(JSONRPCMethodManager): - - def __init__(self, sat_host): - JSONRPCMethodManager.__init__(self, sat_host) - self.authorized_params = None - - def render(self, request): - self.session = request.getSession() - profile = ISATSession(self.session).profile - if not profile: - #user is not identified, we return a jsonrpc fault - parsed = jsonrpclib.loads(request.content.read()) - fault = jsonrpclib.Fault(C.ERRNUM_LIBERVIA, "Not allowed") #FIXME: define some standard error codes for libervia - return jsonrpc.JSONRPC._cbRender(self, fault, request, parsed.get('id'), parsed.get('jsonrpc')) - return jsonrpc.JSONRPC.render(self, request) - - def jsonrpc_getProfileJid(self): - """Return the jid of the profile""" - sat_session = ISATSession(self.session) - profile = sat_session.profile - sat_session.jid = JID(self.sat_host.bridge.getParamA("JabberID", "Connection", profile_key=profile)) - return sat_session.jid.full() - - def jsonrpc_disconnect(self): - """Disconnect the profile""" - sat_session = ISATSession(self.session) - profile = sat_session.profile - self.sat_host.bridge.disconnect(profile) - - def jsonrpc_getContacts(self): - """Return all passed args.""" - profile = ISATSession(self.session).profile - return self.sat_host.bridge.getContacts(profile) - - def jsonrpc_addContact(self, entity, name, groups): - """Subscribe to contact presence, and add it to the given groups""" - profile = ISATSession(self.session).profile - self.sat_host.bridge.addContact(entity, profile) - self.sat_host.bridge.updateContact(entity, name, groups, profile) - - def jsonrpc_delContact(self, entity): - """Remove contact from contacts list""" - profile = ISATSession(self.session).profile - self.sat_host.bridge.delContact(entity, profile) - - def jsonrpc_updateContact(self, entity, name, groups): - """Update contact's roster item""" - profile = ISATSession(self.session).profile - self.sat_host.bridge.updateContact(entity, name, groups, profile) - - def jsonrpc_subscription(self, sub_type, entity, name, groups): - """Confirm (or infirm) subscription, - and setup user roster in case of subscription""" - profile = ISATSession(self.session).profile - self.sat_host.bridge.subscription(sub_type, entity, profile) - if sub_type == 'subscribed': - self.sat_host.bridge.updateContact(entity, name, groups, profile) - - def jsonrpc_getWaitingSub(self): - """Return list of room already joined by user""" - profile = ISATSession(self.session).profile - return self.sat_host.bridge.getWaitingSub(profile) - - def jsonrpc_setStatus(self, presence, status): - """Change the presence and/or status - @param presence: value from ("", "chat", "away", "dnd", "xa") - @param status: any string to describe your status - """ - profile = ISATSession(self.session).profile - self.sat_host.bridge.setPresence('', presence, {'': status}, profile) - - - def jsonrpc_sendMessage(self, to_jid, msg, subject, type_, options={}): - """send message""" - profile = ISATSession(self.session).profile - return self.asyncBridgeCall("sendMessage", to_jid, msg, subject, type_, options, profile) - - def jsonrpc_sendMblog(self, type_, dest, text, extra={}): - """ Send microblog message - @param type_: one of "PUBLIC", "GROUP" - @param dest: destinees (list of groups, ignored for "PUBLIC") - @param text: microblog's text - """ - profile = ISATSession(self.session).profile - extra['allow_comments'] = 'True' - - if not type_: # auto-detect - type_ = "PUBLIC" if dest == [] else "GROUP" - - if type_ in ("PUBLIC", "GROUP") and text: - if type_ == "PUBLIC": - #This text if for the public microblog - print "sending public blog" - return self.sat_host.bridge.sendGroupBlog("PUBLIC", [], text, extra, profile) - else: - print "sending group blog" - dest = dest if isinstance(dest, list) else [dest] - return self.sat_host.bridge.sendGroupBlog("GROUP", dest, text, extra, profile) - 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 - @param text: comment - """ - profile = ISATSession(self.session).profile - if node and text: - return self.sat_host.bridge.sendGroupBlogComment(node, text, extra, profile) - else: - raise Exception("Invalid data") - - def jsonrpc_getMblogs(self, publisher_jid, item_ids): - """Get specified microblogs posted by a contact - @param publisher_jid: jid of the publisher - @param item_ids: list of microblogs items IDs - @return list of microblog data (dict)""" - profile = ISATSession(self.session).profile - d = self.asyncBridgeCall("getGroupBlogs", publisher_jid, item_ids, profile) - return d - - def jsonrpc_getMblogsWithComments(self, publisher_jid, item_ids): - """Get specified microblogs posted by a contact and their comments - @param publisher_jid: jid of the publisher - @param item_ids: list of microblogs items IDs - @return list of couple (microblog data, list of microblog data)""" - profile = ISATSession(self.session).profile - d = self.asyncBridgeCall("getGroupBlogsWithComments", publisher_jid, item_ids, profile) - return d - - def jsonrpc_getLastMblogs(self, publisher_jid, max_item): - """Get last microblogs posted by a contact - @param publisher_jid: jid of the publisher - @param max_item: number of items to ask - @return list of microblog data (dict)""" - profile = ISATSession(self.session).profile - d = self.asyncBridgeCall("getLastGroupBlogs", publisher_jid, max_item, profile) - return d - - def jsonrpc_getMassiveLastMblogs(self, publishers_type, publishers_list, max_item): - """Get lasts microblogs posted by several contacts at once - @param publishers_type: one of "ALL", "GROUP", "JID" - @param publishers_list: list of publishers type (empty list of all, list of groups or list of jids) - @param max_item: number of items to ask - @return: dictionary key=publisher's jid, value=list of microblog data (dict)""" - profile = ISATSession(self.session).profile - d = self.asyncBridgeCall("getMassiveLastGroupBlogs", publishers_type, publishers_list, max_item, profile) - self.sat_host.bridge.massiveSubscribeGroupBlogs(publishers_type, publishers_list, profile) - return d - - def jsonrpc_getMblogComments(self, service, node): - """Get all comments of given node - @param service: jid of the service hosting the node - @param node: comments node - """ - profile = ISATSession(self.session).profile - d = self.asyncBridgeCall("getGroupBlogComments", service, node, profile) - return d - - - def jsonrpc_getPresenceStatuses(self): - """Get Presence information for connected contacts""" - profile = ISATSession(self.session).profile - return self.sat_host.bridge.getPresenceStatuses(profile) - - def jsonrpc_getHistory(self, from_jid, to_jid, size, between): - """Return history for the from_jid/to_jid couple""" - sat_session = ISATSession(self.session) - profile = sat_session.profile - sat_jid = sat_session.jid - if not sat_jid: - log.error("No jid saved for this profile") - return {} - if JID(from_jid).userhost() != sat_jid.userhost() and JID(to_jid).userhost() != sat_jid.userhost(): - log.error("Trying to get history from a different jid, maybe a hack attempt ?") - return {} - d = self.asyncBridgeCall("getHistory", from_jid, to_jid, size, between, profile) - def show(result_dbus): - result = [] - for line in result_dbus: - #XXX: we have to do this stupid thing because Python D-Bus use its own types instead of standard types - # and txJsonRPC doesn't accept D-Bus types, resulting in a empty query - timestamp, from_jid, to_jid, message, mess_type, extra = line - result.append((float(timestamp), unicode(from_jid), unicode(to_jid), unicode(message), unicode(mess_type), dict(extra))) - return result - d.addCallback(show) - return d - - def jsonrpc_joinMUC(self, room_jid, nick): - """Join a Multi-User Chat room - @room_jid: leave empty string to generate a unique name - """ - profile = ISATSession(self.session).profile - try: - if room_jid != "": - room_jid = JID(room_jid).userhost() - except: - log.warning('Invalid room jid') - return - d = self.asyncBridgeCall("joinMUC", room_jid, nick, {}, profile) - return d - - def jsonrpc_inviteMUC(self, contact_jid, room_jid): - """Invite a user to a Multi-User Chat room""" - profile = ISATSession(self.session).profile - try: - room_jid = JID(room_jid).userhost() - except: - log.warning('Invalid room jid') - return - room_id = room_jid.split("@")[0] - service = room_jid.split("@")[1] - self.sat_host.bridge.inviteMUC(contact_jid, service, room_id, {}, profile) - - def jsonrpc_mucLeave(self, room_jid): - """Quit a Multi-User Chat room""" - profile = ISATSession(self.session).profile - try: - room_jid = JID(room_jid) - except: - log.warning('Invalid room jid') - return - self.sat_host.bridge.mucLeave(room_jid.userhost(), profile) - - def jsonrpc_getRoomsJoined(self): - """Return list of room already joined by user""" - profile = ISATSession(self.session).profile - return self.sat_host.bridge.getRoomsJoined(profile) - - def jsonrpc_launchTarotGame(self, other_players, room_jid=""): - """Create a room, invite the other players and start a Tarot game - @param room_jid: leave empty string to generate a unique room name - """ - profile = ISATSession(self.session).profile - try: - if room_jid != "": - room_jid = JID(room_jid).userhost() - except: - log.warning('Invalid room jid') - return - self.sat_host.bridge.tarotGameLaunch(other_players, room_jid, profile) - - def jsonrpc_getTarotCardsPaths(self): - """Give the path of all the tarot cards""" - _join = os.path.join - _media_dir = _join(self.sat_host.media_dir,'') - return map(lambda x: _join(C.MEDIA_DIR, x[len(_media_dir):]), glob.glob(_join(_media_dir, C.CARDS_DIR, '*_*.png'))); - - def jsonrpc_tarotGameReady(self, player, referee): - """Tell to the server that we are ready to start the game""" - profile = ISATSession(self.session).profile - self.sat_host.bridge.tarotGameReady(player, referee, profile) - - def jsonrpc_tarotGamePlayCards(self, player_nick, referee, cards): - """Tell to the server the cards we want to put on the table""" - profile = ISATSession(self.session).profile - self.sat_host.bridge.tarotGamePlayCards(player_nick, referee, cards, profile) - - def jsonrpc_launchRadioCollective(self, invited, room_jid=""): - """Create a room, invite people, and start a radio collective - @param room_jid: leave empty string to generate a unique room name - """ - profile = ISATSession(self.session).profile - try: - if room_jid != "": - room_jid = JID(room_jid).userhost() - except: - log.warning('Invalid room jid') - return - self.sat_host.bridge.radiocolLaunch(invited, room_jid, profile) - - def jsonrpc_getEntityData(self, jid, keys): - """Get cached data for an entit - @param jid: jid of contact from who we want data - @param keys: name of data we want (list) - @return: requested data""" - profile = ISATSession(self.session).profile - return self.sat_host.bridge.getEntityData(jid, keys, profile) - - def jsonrpc_getCard(self, jid): - """Get VCard for entiry - @param jid: jid of contact from who we want data - @return: id to retrieve the profile""" - profile = ISATSession(self.session).profile - return self.sat_host.bridge.getCard(jid, profile) - - def jsonrpc_getAccountDialogUI(self): - """Get the dialog for managing user account - @return: XML string of the XMLUI""" - profile = ISATSession(self.session).profile - return self.sat_host.bridge.getAccountDialogUI(profile) - - def jsonrpc_getParamsUI(self): - """Return the parameters XML for profile""" - profile = ISATSession(self.session).profile - d = self.asyncBridgeCall("getParams", C.SECURITY_LIMIT, C.APP_NAME, profile) - - def setAuthorizedParams(params_xml): - if self.authorized_params is None: - self.authorized_params = {} - for cat in minidom.parseString(params_xml.encode('utf-8')).getElementsByTagName("category"): - params = cat.getElementsByTagName("param") - params_list = [param.getAttribute("name") for param in params] - self.authorized_params[cat.getAttribute("name")] = params_list - if self.authorized_params: - return params_xml - else: - return None - - d.addCallback(setAuthorizedParams) - - d.addCallback(lambda params_xml: paramsXML2XMLUI(params_xml) if params_xml else "") - - return d - - def jsonrpc_asyncGetParamA(self, param, category, attribute="value"): - """Return the parameter value for profile""" - profile = ISATSession(self.session).profile - d = self.asyncBridgeCall("asyncGetParamA", param, category, attribute, C.SECURITY_LIMIT, profile_key=profile) - return d - - def jsonrpc_setParam(self, name, value, category): - profile = ISATSession(self.session).profile - if category in self.authorized_params and name in self.authorized_params[category]: - return self.sat_host.bridge.setParam(name, value, category, C.SECURITY_LIMIT, profile) - else: - log.warning("Trying to set parameter '%s' in category '%s' without authorization!!!" - % (name, category)) - - def jsonrpc_launchAction(self, callback_id, data): - #FIXME: any action can be launched, this can be a huge security issue if callback_id can be guessed - # a security system with authorised callback_id must be implemented, similar to the one for authorised params - profile = ISATSession(self.session).profile - d = self.asyncBridgeCall("launchAction", callback_id, data, profile) - return d - - def jsonrpc_chatStateComposing(self, to_jid_s): - """Call the method to process a "composing" state. - @param to_jid_s: contact the user is composing to - """ - profile = ISATSession(self.session).profile - self.sat_host.bridge.chatStateComposing(to_jid_s, profile) - - def jsonrpc_getNewAccountDomain(self): - """@return: the domain for new account creation""" - d = self.asyncBridgeCall("getNewAccountDomain") - return d - - def jsonrpc_confirmationAnswer(self, confirmation_id, result, answer_data): - """Send the user's answer to any previous 'askConfirmation' signal""" - profile = ISATSession(self.session).profile - self.sat_host.bridge.confirmationAnswer(confirmation_id, result, answer_data, profile) - - def jsonrpc_syntaxConvert(self, text, syntax_from=C.SYNTAX_XHTML, syntax_to=C.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 - It provide an api for the browser, check password and setup the web server""" - - def __init__(self, sat_host): - JSONRPCMethodManager.__init__(self, sat_host) - self.profiles_waiting={} - self.request=None - - def getWaitingRequest(self, profile): - """Tell if a profile is trying to log in""" - if self.profiles_waiting.has_key(profile): - return self.profiles_waiting[profile] - else: - return None - - def render(self, request): - """ - Render method with some hacks: - - if login is requested, try to login with form data - - except login, every method is jsonrpc - - user doesn't need to be authentified for explicitely listed methods, but must be for all others - """ - if request.postpath == ['login']: - return self.loginOrRegister(request) - _session = request.getSession() - parsed = jsonrpclib.loads(request.content.read()) - method = parsed.get("method") - if method not in ['isRegistered', 'registerParams', 'getMenus']: - #if we don't call these methods, we need to be identified - profile = ISATSession(_session).profile - if not profile: - #user is not identified, we return a jsonrpc fault - fault = jsonrpclib.Fault(C.ERRNUM_LIBERVIA, "Not allowed") #FIXME: define some standard error codes for libervia - return jsonrpc.JSONRPC._cbRender(self, fault, request, parsed.get('id'), parsed.get('jsonrpc')) - self.request = request - return jsonrpc.JSONRPC.render(self, request) - - def loginOrRegister(self, request): - """This method is called with the POST information from the registering form. - - @param request: request of the register form - @return: a constant indicating the state: - - BAD REQUEST: something is wrong in the request (bad arguments) - - a return value from self._loginAccount or self._registerNewAccount - """ - try: - submit_type = request.args['submit_type'][0] - except KeyError: - return "BAD REQUEST" - - if submit_type == 'register': - return self._registerNewAccount(request) - elif submit_type == 'login': - return self._loginAccount(request) - return Exception('Unknown submit type') - - def _loginAccount(self, request): - """Try to authenticate the user with the request information. - @param request: request of the register form - @return: a constant indicating the state: - - BAD REQUEST: something is wrong in the request (bad arguments) - - AUTH ERROR: either the profile (login) or the password is wrong - - ALREADY WAITING: a request has already been submitted for this profile - - server.NOT_DONE_YET: the profile is being processed, the return - value will be given by self._logged or self._logginError - """ - try: - login_ = request.args['login'][0] - password_ = request.args['login_password'][0] - except KeyError: - return "BAD REQUEST" - - if login_.startswith('@'): - raise Exception('No profile_key allowed') - - profile_check = self.sat_host.bridge.getProfileName(login_) - if not profile_check or profile_check != login_ or not password_: - # profiles with empty passwords are restricted to local frontends - return "AUTH ERROR" - - if login_ in self.profiles_waiting: - return "ALREADY WAITING" - - def auth_eb(ignore=None): - self.__cleanWaiting(login_) - log.info("Profile %s doesn't exist or the submitted password is wrong" % login_) - request.write("AUTH ERROR") - request.finish() - - self.profiles_waiting[login_] = request - d = self.asyncBridgeCall("asyncConnect", login_, password_) - d.addCallbacks(lambda connected: self._logged(login_, request) if connected else None, auth_eb) - - return server.NOT_DONE_YET - - def _registerNewAccount(self, request): - """Create a new account, or return error - @param request: request of the register form - @return: a constant indicating the state: - - BAD REQUEST: something is wrong in the request (bad arguments) - - REGISTRATION: new account has been successfully registered - - ALREADY EXISTS: the given profile already exists - - INTERNAL or 'Unknown error (...)' - - server.NOT_DONE_YET: the profile is being processed, the return - value will be given later (one of those previously described) - """ - try: - profile = login = request.args['register_login'][0] - password = request.args['register_password'][0] - email = request.args['email'][0] - except KeyError: - return "BAD REQUEST" - if not re.match(r'^[a-z0-9_-]+$', login, re.IGNORECASE) or \ - not re.match(r'^.+@.+\..+', email, re.IGNORECASE) or \ - len(password) < C.PASSWORD_MIN_LENGTH: - return "BAD REQUEST" - - def registered(result): - request.write('REGISTRATION') - request.finish() - - def registeringError(failure): - reason = failure.value.faultString - if reason == "ConflictError": - request.write('ALREADY EXISTS') - elif reason == "InternalError": - request.write('INTERNAL') - else: - log.error('Unknown registering error: %s' % (reason,)) - request.write('Unknown error (%s)' % reason) - request.finish() - - d = self.asyncBridgeCall("registerSatAccount", email, password, profile) - d.addCallback(registered) - d.addErrback(registeringError) - return server.NOT_DONE_YET - - def __cleanWaiting(self, login): - """Remove login from waiting queue""" - try: - del self.profiles_waiting[login] - except KeyError: - pass - - def _logged(self, profile, request): - """Set everything when a user just logged in - - @param profile - @param request - @return: a constant indicating the state: - - LOGGED - - SESSION_ACTIVE - """ - self.__cleanWaiting(profile) - _session = request.getSession() - sat_session = ISATSession(_session) - if sat_session.profile: - log.error(('/!\\ Session has already a profile, this should NEVER happen!')) - request.write('SESSION_ACTIVE') - request.finish() - return - sat_session.profile = profile - self.sat_host.prof_connected.add(profile) - - def onExpire(): - log.info("Session expired (profile=%s)" % (profile,)) - try: - #We purge the queue - del self.sat_host.signal_handler.queue[profile] - except KeyError: - pass - #and now we disconnect the profile - self.sat_host.bridge.disconnect(profile) - - _session.notifyOnExpire(onExpire) - - request.write('LOGGED') - request.finish() - - def _logginError(self, login, request, error_type): - """Something went wrong during logging in - @return: error - """ - self.__cleanWaiting(login) - return error_type - - def jsonrpc_isConnected(self): - _session = self.request.getSession() - profile = ISATSession(_session).profile - return self.sat_host.bridge.isConnected(profile) - - def jsonrpc_connect(self): - _session = self.request.getSession() - profile = ISATSession(_session).profile - if self.profiles_waiting.has_key(profile): - raise jsonrpclib.Fault(1,'Already waiting') #FIXME: define some standard error codes for libervia - self.profiles_waiting[profile] = self.request - self.sat_host.bridge.connect(profile) - return server.NOT_DONE_YET - - def jsonrpc_isRegistered(self): - """ - @return: a couple (registered, message) with: - - registered: True if the user is already registered, False otherwise - - message: a security warning message if registered is False *and* the connection is unsecure, None otherwise - """ - _session = self.request.getSession() - profile = ISATSession(_session).profile - if bool(profile): - return (True, None) - return (False, self.__getSecurityWarning()) - - def jsonrpc_registerParams(self): - """Register the frontend specific parameters""" - params = """ - <params> - <individual> - <category name="%(category_name)s" label="%(category_label)s"> - <param name="%(param_name)s" label="%(param_label)s" value="false" type="bool" security="0"/> - </category> - </individual> - </params> - """ % { - 'category_name': C.ENABLE_UNIBOX_KEY, - 'category_label': _(C.ENABLE_UNIBOX_KEY), - 'param_name': C.ENABLE_UNIBOX_PARAM, - 'param_label': _(C.ENABLE_UNIBOX_PARAM) - } - - self.sat_host.bridge.paramsRegisterApp(params, C.SECURITY_LIMIT, C.APP_NAME) - - def jsonrpc_getMenus(self): - """Return the parameters XML for profile""" - # XXX: we put this method in Register because we get menus before being logged - return self.sat_host.bridge.getMenus('', C.SECURITY_LIMIT) - - def __getSecurityWarning(self): - """@return: a security warning message, or None if the connection is secure""" - if self.request.URLPath().scheme == 'https' or not self.sat_host.security_warning: - return None - text = D_("You are about to connect to an unsecured service.") - if self.sat_host.connection_type == 'both': - new_port = (':%s' % self.sat_host.port_https_ext) if self.sat_host.port_https_ext != HTTPS_PORT else '' - url = "https://%s" % self.request.URLPath().netloc.replace(':%s' % self.sat_host.port, new_port) - text += D_('<br />Secure version of this website: <a href="%(url)s">%(url)s</a>') % {'url': url} - return text - - -class SignalHandler(jsonrpc.JSONRPC): - - def __init__(self, sat_host): - Resource.__init__(self) - self.register=None - self.sat_host=sat_host - self.signalDeferred = {} - self.queue = {} - - def plugRegister(self, register): - self.register = register - - def jsonrpc_getSignals(self): - """Keep the connection alive until a signal is received, then send it - @return: (signal, *signal_args)""" - _session = self.request.getSession() - profile = ISATSession(_session).profile - if profile in self.queue: #if we have signals to send in queue - if self.queue[profile]: - return self.queue[profile].pop(0) - else: - #the queue is empty, we delete the profile from queue - del self.queue[profile] - _session.lock() #we don't want the session to expire as long as this connection is active - def unlock(signal, profile): - _session.unlock() - try: - source_defer = self.signalDeferred[profile] - if source_defer.called and source_defer.result[0] == "disconnected": - log.info(u"[%s] disconnected" % (profile,)) - _session.expire() - except IndexError: - log.error("Deferred result should be a tuple with fonction name first") - - self.signalDeferred[profile] = defer.Deferred() - self.request.notifyFinish().addBoth(unlock, profile) - return self.signalDeferred[profile] - - def getGenericCb(self, function_name): - """Return a generic function which send all params to signalDeferred.callback - function must have profile as last argument""" - def genericCb(*args): - profile = args[-1] - if not profile in self.sat_host.prof_connected: - return - if profile in self.signalDeferred: - self.signalDeferred[profile].callback((function_name,args[:-1])) - del self.signalDeferred[profile] - else: - if not self.queue.has_key(profile): - self.queue[profile] = [] - self.queue[profile].append((function_name, args[:-1])) - return genericCb - - def connected(self, profile): - assert(self.register) # register must be plugged - request = self.register.getWaitingRequest(profile) - if request: - self.register._logged(profile, request) - - def disconnected(self, profile): - if not profile in self.sat_host.prof_connected: - log.error("'disconnected' signal received for a not connected profile") - return - self.sat_host.prof_connected.remove(profile) - if profile in self.signalDeferred: - self.signalDeferred[profile].callback(("disconnected",)) - del self.signalDeferred[profile] - else: - if not self.queue.has_key(profile): - self.queue[profile] = [] - self.queue[profile].append(("disconnected",)) - - - def connectionError(self, error_type, profile): - assert(self.register) #register must be plugged - request = self.register.getWaitingRequest(profile) - if request: #The user is trying to log in - if error_type == "AUTH_ERROR": - _error_t = "AUTH ERROR" - else: - _error_t = "UNKNOWN" - self.register._logginError(profile, request, _error_t) - - def render(self, request): - """ - Render method wich reject access if user is not identified - """ - _session = request.getSession() - parsed = jsonrpclib.loads(request.content.read()) - profile = ISATSession(_session).profile - if not profile: - #user is not identified, we return a jsonrpc fault - fault = jsonrpclib.Fault(C.ERRNUM_LIBERVIA, "Not allowed") #FIXME: define some standard error codes for libervia - return jsonrpc.JSONRPC._cbRender(self, fault, request, parsed.get('id'), parsed.get('jsonrpc')) - self.request = request - return jsonrpc.JSONRPC.render(self, request) - <