# HG changeset patch # User souliane # Date 1396534244 -7200 # Node ID 3a96920c07b7c22528278ef351c43d476020f3d9 # Parent 224cafc67324cd69b151af9c06e5da62277e1405 core, frontends: unify the roster management UIs in sat/stdui/ui_contact_list.py diff -r 224cafc67324 -r 3a96920c07b7 frontends/src/primitivus/primitivus --- a/frontends/src/primitivus/primitivus Mon Apr 07 16:24:29 2014 +0200 +++ b/frontends/src/primitivus/primitivus Thu Apr 03 16:10:44 2014 +0200 @@ -290,9 +290,8 @@ menu.addMenu(general, _("Parameters"), self.onParam) menu.addMenu(general, _("About"), self.onAboutRequest) menu.addMenu(general, _("Exit"), self.onExitRequest, 'ctrl x') - contact = _("Contact") - menu.addMenu(contact, _("Add contact"), self.onAddContactRequest) - menu.addMenu(contact, _("Remove contact"), self.onRemoveContactRequest) + contact = _("Contacts") + menu.addMenu(contact) communication = _("Communication") menu.addMenu(communication, _("Join room"), self.onJoinRoomRequest, 'meta j') #additionals menus @@ -550,21 +549,6 @@ error (message) self.showPopUp(sat_widgets.Alert(_("Error"), message, ok_cb=self.removePopUp)) - def onAddContact(self, button, edit): - self.removePopUp() - jid=JID(edit.get_edit_text()) - if jid.is_valid(): - self.bridge.addContact(jid.bare, profile_key=self.profile) - else: - message = _("'%s' is an invalid JID !") % jid - error (message) - self.showPopUp(sat_widgets.Alert(_("Error"), message, ok_cb=self.removePopUp)) - - def onRemoveContact(self, button): - self.removePopUp() - info(_("Unsubscribing %s presence"),self.contact_list.getContact()) - self.bridge.delContact(self.contact_list.getContact(), profile_key=self.profile) - #MENU EVENTS# def onConnectRequest(self, menu): self.bridge.connect(self.profile) @@ -589,18 +573,6 @@ pop_up_widget = sat_widgets.InputDialog(_("Entering a MUC room"), _("Please enter MUC's JID"), default_txt = 'room@muc_service.server.tld', cancel_cb=self.removePopUp, ok_cb=self.onJoinRoom) self.showPopUp(pop_up_widget) - def onAddContactRequest(self, menu): - pop_up_widget = sat_widgets.InputDialog(_("Adding a contact"), _("Please enter new contact JID"), default_txt = 'name@server.tld', cancel_cb=self.removePopUp, ok_cb=self.onAddContact) - self.showPopUp(pop_up_widget) - - def onRemoveContactRequest(self, menu): - contact = self.contact_list.getContact() - if not contact: - self.showPopUp(sat_widgets.Alert(_("Error"), _("You have not selected any contact to delete !"), ok_cb=self.removePopUp)) - else: - pop_up_widget = sat_widgets.ConfirmDialog(_("Are you sure you want to delete the contact [%s] ?" % contact), yes_cb=self.onRemoveContact, no_cb=self.removePopUp) - self.showPopUp(pop_up_widget) - def onAboutRequest(self, menu): self.showPopUp(sat_widgets.Alert(_("About"), Const.APP_NAME + " v" + self.bridge.getVersion(), ok_cb=self.removePopUp)) diff -r 224cafc67324 -r 3a96920c07b7 frontends/src/wix/main_window.py --- a/frontends/src/wix/main_window.py Mon Apr 07 16:24:29 2014 +0200 +++ b/frontends/src/wix/main_window.py Thu Apr 03 16:10:44 2014 +0200 @@ -37,11 +37,9 @@ idEXIT,\ idABOUT,\ idPARAM,\ -idADD_CONTACT,\ -idREMOVE_CONTACT,\ idSHOW_PROFILE,\ idJOIN_ROOM,\ -= range(9) + = range(7) class ChatList(QuickChatList): """This class manage the list of chat windows""" @@ -124,10 +122,6 @@ connectMenu.Append(idABOUT, _("A&bout"), _(" About %s") % Const.APP_NAME) connectMenu.Append(idEXIT,_("E&xit"),_(" Terminate the program")) contactMenu = wx.Menu() - contactMenu.Append(idADD_CONTACT, _("&Add contact"),_(" Add a contact to your list")) - contactMenu.Append(idREMOVE_CONTACT, _("&Remove contact"),_(" Remove the selected contact from your list")) - contactMenu.AppendSeparator() - contactMenu.Append(idSHOW_PROFILE, _("&Show profile"), _(" Show contact's profile")) communicationMenu = wx.Menu() communicationMenu.Append(idJOIN_ROOM, _("&Join Room"),_(" Join a Multi-User Chat room")) self.menuBar = wx.MenuBar() @@ -163,6 +157,9 @@ wx.EVT_MENU(self, item_id, event_answer) + # menu items that should be displayed after the automatically added ones + contactMenu.AppendSeparator() + contactMenu.Append(idSHOW_PROFILE, _("&Show profile"), _(" Show contact's profile")) #events wx.EVT_MENU(self, idCONNECT, self.onConnectRequest) @@ -170,8 +167,6 @@ wx.EVT_MENU(self, idPARAM, self.onParam) wx.EVT_MENU(self, idABOUT, self.onAbout) wx.EVT_MENU(self, idEXIT, self.onExit) - wx.EVT_MENU(self, idADD_CONTACT, self.onAddContact) - wx.EVT_MENU(self, idREMOVE_CONTACT, self.onRemoveContact) wx.EVT_MENU(self, idSHOW_PROFILE, self.onShowProfile) wx.EVT_MENU(self, idJOIN_ROOM, self.onJoinRoom) @@ -434,45 +429,6 @@ def onExit(self, e): self.Close() - def onAddContact(self, e): - debug(_("Add contact request")) - dlg = wx.TextEntryDialog( - self, _('Please enter new contact JID'), - _('Adding a contact'), _('name@server.tld')) - - if dlg.ShowModal() == wx.ID_OK: - jid=JID(dlg.GetValue()) - if jid.is_valid(): - self.bridge.addContact(jid.bare, profile_key=self.profile) - else: - error (_("'%s' is an invalid JID !"), jid) - #TODO: notice the user - - dlg.Destroy() - - def onRemoveContact(self, e): - debug(_("Remove contact request")) - target = self.contact_list.getSelection() - if not target: - dlg = wx.MessageDialog(self, _("You haven't selected any contact !"), - _('Error'), - wx.OK | wx.ICON_ERROR - ) - dlg.ShowModal() - dlg.Destroy() - return - - dlg = wx.MessageDialog(self, _("Are you sure you want to delete %s from your roster list ?") % target.bare, - _('Contact suppression'), - wx.YES_NO | wx.ICON_QUESTION - ) - - if dlg.ShowModal() == wx.ID_YES: - info(_("Unsubscribing %s presence"), target.bare) - self.bridge.delContact(target.bare, profile_key=self.profile) - - dlg.Destroy() - def onShowProfile(self, e): debug(_("Show contact's profile request")) target = self.contact_list.getSelection() diff -r 224cafc67324 -r 3a96920c07b7 setup.py --- a/setup.py Mon Apr 07 16:24:29 2014 +0200 +++ b/setup.py Thu Apr 03 16:10:44 2014 +0200 @@ -169,7 +169,7 @@ package_dir={'sat': 'src', 'sat_frontends': 'frontends/src'}, packages=['sat', 'sat.tools', 'sat.bridge', 'sat.plugins', 'sat.test', 'sat.core', 'sat.memory', 'sat_frontends', 'sat_frontends.bridge', 'sat_frontends.quick_frontend', 'sat_frontends.jp', - 'sat_frontends.primitivus', 'sat_frontends.wix', 'sat_frontends.tools'], + 'sat_frontends.primitivus', 'sat_frontends.wix', 'sat_frontends.tools', 'sat.stdui'], package_data={'sat': ['sat.tac', 'sat.sh'], 'sat_frontends': ['wix/COPYING']}, data_files=[(os.path.join(sys.prefix, 'share/locale/fr/LC_MESSAGES'), ['i18n/fr/LC_MESSAGES/sat.mo']), diff -r 224cafc67324 -r 3a96920c07b7 src/core/sat_main.py --- a/src/core/sat_main.py Mon Apr 07 16:24:29 2014 +0200 +++ b/src/core/sat_main.py Thu Apr 03 16:10:44 2014 +0200 @@ -40,9 +40,15 @@ from sat.core.constants import Const as C from sat.memory.memory import Memory from sat.tools.misc import TriggerManager +from sat.stdui import ui_contact_list from glob import glob from uuid import uuid4 +try: + from collections import OrderedDict # only available from python 2.7 +except ImportError: + from ordereddict import OrderedDict + ### logging configuration FIXME: put this elsewhere ### logging.basicConfig(level=logging.DEBUG, format='%(message)s') @@ -84,7 +90,7 @@ def __init__(self): self._cb_map = {} # map from callback_id to callbacks - self._menus = {} # dynamic menus. key: callback_id, value: menu data (dictionnary) + self._menus = OrderedDict() # dynamic menus. key: callback_id, value: menu data (dictionnary) self.__private_data = {} # used for internal callbacks (key = id) FIXME: to be removed self.profiles = {} self.plugins = {} @@ -144,6 +150,7 @@ """Method called after memory initialization is done""" info(_("Memory initialised")) self._import_plugins() + ui_contact_list.ContactList(self) def _import_plugins(self): """Import all plugins found in plugins directory""" diff -r 224cafc67324 -r 3a96920c07b7 src/plugins/plugin_misc_account.py --- a/src/plugins/plugin_misc_account.py Mon Apr 07 16:24:29 2014 +0200 +++ b/src/plugins/plugin_misc_account.py Thu Apr 03 16:10:44 2014 +0200 @@ -120,7 +120,7 @@ info(_(u"Plugin Account initialization")) self.host = host host.bridge.addMethod("registerSatAccount", ".plugin", in_sign='sss', out_sign='', method=self._registerAccount, async=True) - host.bridge.addMethod("getNewAccountDomain", ".plugin", in_sign='', out_sign='s', method=self._getNewAccountDomain, async=False) + host.bridge.addMethod("getNewAccountDomain", ".plugin", in_sign='', out_sign='s', method=self.getNewAccountDomain, async=False) host.bridge.addMethod("getAccountDialogUI", ".plugin", in_sign='s', out_sign='s', method=self._getAccountDialogUI, async=False) self._prosody_path = self.getConfig('prosody_path') if self._prosody_path is None: @@ -235,7 +235,7 @@ d_admin.addCallbacks(email_ok, email_ko) return defer.DeferredList([d_user, d_admin]) - def _getNewAccountDomain(self): + def getNewAccountDomain(self): """@return: the domain that will be set to new account""" return self.getConfig('new_account_domain') @@ -342,7 +342,7 @@ form_ui = xml_tools.XMLUI("form", title=D_("Delete your account?"), submit_id=self.__delete_account_id) form_ui.addText(D_("If you confirm this dialog, you will be disconnected and then your XMPP account AND your SàT profile will both be DELETED.")) target = D_('contact list, messages history, blog posts and comments' if 'GROUPBLOG' in self.host.plugins else D_('contact list and messages history')) - form_ui.addText(D_("All your data stored on %(server)s, including your %(target)s will be erased.") % {'server': self._getNewAccountDomain(), 'target': target}) + form_ui.addText(D_("All your data stored on %(server)s, including your %(target)s will be erased.") % {'server': self.getNewAccountDomain(), 'target': target}) form_ui.addText(D_("There is no other confirmation dialog, this is the very last one! Are you sure?")) return {'xmlui': form_ui.toXml()} diff -r 224cafc67324 -r 3a96920c07b7 src/stdui/ui_contact_list.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/stdui/ui_contact_list.py Thu Apr 03 16:10:44 2014 +0200 @@ -0,0 +1,260 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# SAT standard user interface for managing contacts +# 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 . + +from sat.core.i18n import _, D_ +from sat.core.constants import Const as C +from sat.tools import xml_tools +from twisted.words.protocols.jabber import jid +from xml.dom.minidom import Element +import re + + +class ContactList(object): + """Add, update and remove contacts.""" + + def __init__(self, host): + self.host = host + self.__add_id = host.registerCallback(self._addContact, with_data=True) + self.__update_id = host.registerCallback(self._updateContact, with_data=True) + self.__confirm_delete_id = host.registerCallback(self._getConfirmRemoveXMLUI, with_data=True) + + host.importMenu((D_("Contacts"), D_("Add contact")), self._getAddDialogXMLUI, security_limit=2, help_string=D_("Add contact")) + host.importMenu((D_("Contacts"), D_("Update contact")), self._getUpdateDialogXMLUI, security_limit=2, help_string=D_("Update contact")) + host.importMenu((D_("Contacts"), D_("Remove contact")), self._getRemoveDialogXMLUI, security_limit=2, help_string=D_("Remove contact")) + + if 'MISC-ACCOUNT' in self.host.plugins: + self.default_host = self.host.plugins['MISC-ACCOUNT'].getNewAccountDomain() + else: + self.default_host = 'example.net' + + def getContacts(self, profile): + """Return a sorted list of the contacts for that profile + + @param profile: %(doc_profile)s + @return: list[string] + """ + client = self.host.getClient(profile) + ret = [contact.userhost() for contact in client.roster.getBareJids()] + ret.sort() + return ret + + def getGroups(self, new_groups=None, profile=C.PROF_KEY_NONE): + """Return a sorted list of the groups for that profile + + @param new_group (list): add these groups to the existing ones + @param profile: %(doc_profile)s + @return: list[string] + """ + client = self.host.getClient(profile) + ret = client.roster.getGroups() + ret.sort() + ret.extend([group for group in new_groups if group not in ret]) + return ret + + def getGroupsOfContact(self, user_jid_s, profile): + """Return all the groups of the given contact + + @param user_jid_s (string) + @param profile: %(doc_profile)s + @return: list[string] + """ + client = self.host.getClient(profile) + return client.roster.getItem(jid.JID(user_jid_s)).groups + + def getGroupsOfAllContacts(self, profile): + """Return a mapping between the contacts and their groups + + @param profile: %(doc_profile)s + @return: dict (key: string, value: list[string]): + - key: the JID userhost + - value: list of groups + """ + client = self.host.getClient(profile) + return {item.jid.userhost(): item.groups for item in client.roster.getItems()} + + def _data2elts(self, data): + """Convert a contacts data dict to minidom Elements + + @param data (dict) + @return list[Element] + """ + elts = [] + for key in data: + key_elt = Element('jid') + key_elt.setAttribute('name', key) + for value in data[key]: + value_elt = Element('group') + value_elt.setAttribute('name', value) + key_elt.childNodes.append(value_elt) + elts.append(key_elt) + return elts + + def getDialogXMLUI(self, options, data, profile): + """Generic method to return the XMLUI dialog for adding or updating a contact + + @param options (dict): parameters for the dialog, with the keys: + - 'id': the menu callback id + - 'title': deferred localized string + - 'contact_text': deferred localized string + @param data (dict) + @param profile: %(doc_profile)s + @return dict + """ + form_ui = xml_tools.XMLUI("form", title=options['title'], submit_id=options['id']) + if 'message' in data: + form_ui.addText(data['message']) + form_ui.addDivider('dash') + + form_ui.addText(options['contact_text']) + if options['id'] == self.__add_id: + contact = data.get(xml_tools.formEscape('contact_jid'), '@%s' % self.default_host) + form_ui.addString('contact_jid', value=contact) + elif options['id'] == self.__update_id: + contacts = self.getContacts(profile) + list_ = form_ui.addList('contact_jid', options=contacts, selected=contacts[0]) + elts = self._data2elts(self.getGroupsOfAllContacts(profile)) + list_.setInternalCallback('groups_of_contact', fields=['contact_jid', 'groups_list'], data_elts=elts) + + form_ui.addDivider('blank') + + form_ui.addText(_("Select in which groups your contact is:")) + selected_groups = [] + if 'selected_groups' in data: + selected_groups = data['selected_groups'] + elif options['id'] == self.__update_id: + try: + selected_groups = self.getGroupsOfContact(contacts[0], profile) + except IndexError: + pass + groups = self.getGroups(selected_groups, profile) + form_ui.addList('groups_list', options=groups, selected=selected_groups, style=['multi']) + + adv_list = form_ui.changeContainer("advanced_list", columns=3, selectable='no') + form_ui.addLabel(D_("Add group")) + form_ui.addString("add_group") + button = form_ui.addButton('', value=D_('Add')) + button.setInternalCallback('move', fields=['add_group', 'groups_list']) + adv_list.end() + + form_ui.addDivider('blank') + return {'xmlui': form_ui.toXml()} + + def _getAddDialogXMLUI(self, data, profile): + """Get the dialog for adding contact + + @param data (dict) + @param profile: %(doc_profile)s + @return dict + """ + options = {'id': self.__add_id, + 'title': D_('Add contact'), + 'contact_text': D_("New contact identifier (JID):"), + } + return self.getDialogXMLUI(options, {}, profile) + + def _getUpdateDialogXMLUI(self, data, profile): + """Get the dialog for updating contact + + @param data (dict) + @param profile: %(doc_profile)s + @return dict + """ + if not self.getContacts(profile): + _dialog = xml_tools.XMLUI('popup', title=D_('Nothing to update')) + _dialog.addText(_('Your contact list is empty.')) + return {'xmlui': _dialog.toXml()} + + options = {'id': self.__update_id, + 'title': D_('Update contact'), + 'contact_text': D_("Which contact do you want to update?"), + } + return self.getDialogXMLUI(options, {}, profile) + + def _getRemoveDialogXMLUI(self, data, profile): + """Get the dialog for removing contact + + @param data (dict) + @param profile: %(doc_profile)s + @return dict + """ + if not self.getContacts(profile): + _dialog = xml_tools.XMLUI('popup', title=D_('Nothing to delete')) + _dialog.addText(_('Your contact list is empty.')) + return {'xmlui': _dialog.toXml()} + + form_ui = xml_tools.XMLUI("form", title=D_('Who do you want to remove from your contacts?'), submit_id=self.__confirm_delete_id) + form_ui.addList('contact_jid', options=self.getContacts(profile)) + return {'xmlui': form_ui.toXml()} + + def _getConfirmRemoveXMLUI(self, data, profile): + """Get the confirmation dialog for removing contact + + @param data (dict) + @param profile: %(doc_profile)s + @return dict + """ + contact = data[xml_tools.formEscape('contact_jid')] + cb = lambda data, profile: self._deleteContact(jid.JID(contact), profile) + delete_id = self.host.registerCallback(cb, with_data=True, one_shot=True) + form_ui = xml_tools.XMLUI("form", title=D_("Delete contact"), submit_id=delete_id) + form_ui.addText(D_("Are you sure you want to remove %s from your contact list?") % contact) + return {'xmlui': form_ui.toXml()} + + def _addContact(self, data, profile): + """Add the selected contact + + @param data (dict) + @param profile: %(doc_profile)s + @return dict + """ + contact_jid_s = data[xml_tools.formEscape('contact_jid')] + if not re.match(r'^.+@.+\..+', contact_jid_s, re.IGNORECASE): + # TODO: replace '\t' by a constant (see tools.xmlui.XMLUI.onFormSubmitted) + data['selected_groups'] = data[xml_tools.formEscape('groups_list')].split('\t') + options = {'id': self.__add_id, + 'title': D_('Add contact'), + 'contact_text': D_('Please enter a valid JID (like "contact@%s"):') % self.default_host, + } + return self.getDialogXMLUI(options, data, profile) + contact_jid = jid.JID(contact_jid_s) + self.host.addContact(contact_jid, profile_key=profile) + return self._updateContact(data, profile) # after adding, updating + + def _updateContact(self, data, profile): + """Update the selected contact + + @param data (dict) + @param profile: %(doc_profile)s + @return dict + """ + contact_jid = jid.JID(data[xml_tools.formEscape('contact_jid')]) + # TODO: replace '\t' by a constant (see tools.xmlui.XMLUI.onFormSubmitted) + groups = data[xml_tools.formEscape('groups_list')].split('\t') + self.host.updateContact(contact_jid, name='', groups=groups, profile_key=profile) + return {} + + def _deleteContact(self, contact_jid, profile): + """Delete the selected contact + + @param contact_jid (JID) + @param profile: %(doc_profile)s + @return dict + """ + self.host.delContact(contact_jid, profile_key=profile) + return {}