diff frontends/src/primitivus/profile_manager.py @ 1265:e3a9ea76de35 frontends_multi_profiles

quick_frontend, primitivus: multi-profiles refactoring part 1 (big commit, sorry :p): This refactoring allow primitivus to manage correctly several profiles at once, with various other improvments: - profile_manager can now plug several profiles at once, requesting password when needed. No more profile plug specific method is used anymore in backend, instead a "validated" key is used in actions - Primitivus widget are now based on a common "PrimitivusWidget" classe which mainly manage the decoration so far - all widgets are treated in the same way (contactList, Chat, Progress, etc), no more chat_wins specific behaviour - widgets are created in a dedicated manager, with facilities to react on new widget creation or other events - quick_frontend introduce a new QuickWidget class, which aims to be as generic and flexible as possible. It can manage several targets (jids or something else), and several profiles - each widget class return a Hash according to its target. For example if given a target jid and a profile, a widget class return a hash like (target.bare, profile), the same widget will be used for all resources of the same jid - better management of CHAT_GROUP mode for Chat widgets - some code moved from Primitivus to QuickFrontend, the final goal is to have most non backend code in QuickFrontend, and just graphic code in subclasses - no more (un)escapePrivate/PRIVATE_PREFIX - contactList improved a lot: entities not in roster and special entities (private MUC conversations) are better managed - resources can be displayed in Primitivus, and their status messages - profiles are managed in QuickFrontend with dedicated managers This is work in progress, other frontends are broken. Urwid SàText need to be updated. Most of features of Primitivus should work as before (or in a better way ;))
author Goffi <goffi@goffi.org>
date Wed, 10 Dec 2014 19:00:09 +0100
parents 49d39b619e5d
children 7cf32aeeebdb
line wrap: on
line diff
--- a/frontends/src/primitivus/profile_manager.py	Wed Dec 10 18:37:14 2014 +0100
+++ b/frontends/src/primitivus/profile_manager.py	Wed Dec 10 19:00:09 2014 +0100
@@ -18,29 +18,69 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 from sat.core.i18n import _
+from sat.core import log as logging
+log = logging.getLogger(__name__)
 from sat_frontends.primitivus.constants import Const as C
+from sat_frontends.primitivus.keys import action_key_map as a_key
+from urwid_satext import sat_widgets
 import urwid
-from urwid_satext.sat_widgets import AdvancedEdit, Password, List, InputDialog, ConfirmDialog, Alert
-from sat_frontends.primitivus.keys import action_key_map as a_key
+
+class ProfileRecord(object):
+
+    def __init__(self, profile=None, login=None, password=None):
+        self._profile = profile
+        self._login = login
+        self._password = password
+
+    @property
+    def profile(self):
+        return self._profile
+
+    @profile.setter
+    def profile(self, value):
+        self._profile = value
+        # if we change the profile,
+        # we must have no login/password until backend give them
+        self._login = self._password = None
+
+    @property
+    def login(self):
+        return self._login
+
+    @login.setter
+    def login(self, value):
+        self._login = value
+
+    @property
+    def password(self):
+        return self._password
+
+    @password.setter
+    def password(self, value):
+        self._password = value
 
 
 class ProfileManager(urwid.WidgetWrap):
+    """Class with manage profiles creation/deletion/connection"""
 
-    def __init__(self, host):
+    def __init__(self, host, autoconnect=None):
+        """Create the manager
+
+        @param host: %(doc_host)s
+        @param autoconnect(iterable): list of profiles to connect automatically
+        """
         self.host = host
-        #profiles list
+        self._autoconnect = bool(autoconnect)
+        self.current = ProfileRecord()
         profiles = self.host.bridge.getProfilesList()
         profiles.sort()
 
         #login & password box must be created before list because of onProfileChange
-        self.login_wid = AdvancedEdit(_('Login:'), align='center')
-        self.pass_wid = Password(_('Password:'), align='center')
+        self.login_wid = sat_widgets.AdvancedEdit(_('Login:'), align='center')
+        self.pass_wid = sat_widgets.Password(_('Password:'), align='center')
 
-        self.selected_profile = None  # allow to reselect the previous selection until the profile is authenticated
-        style = ['single']
-        if self.host.options.profile:
-            style.append('no_first_select')
-        self.list_profile = List(profiles, style=style, align='center', on_change=self.onProfileChange)
+        style = ['no_first_select']
+        self.list_profile = sat_widgets.List(profiles, style=style, align='center', on_change=self.onProfileChange)
 
         #new & delete buttons
         buttons = [urwid.Button(_("New"), self.onNewProfile),
@@ -51,14 +91,29 @@
         divider = urwid.Divider('-')
 
         #connect button
-        connect_button = urwid.Button(_("Connect"), self.onConnectProfile)
+        connect_button = sat_widgets.CustomButton(_("Connect"), self.onConnectProfiles, align='center')
 
         #we now build the widget
-        list_walker = urwid.SimpleFocusListWalker([buttons_flow,self.list_profile,divider,self.login_wid, self.pass_wid, connect_button])
+        list_walker = urwid.SimpleFocusListWalker([buttons_flow,self.list_profile, divider, self.login_wid, self.pass_wid, connect_button])
         frame_body = urwid.ListBox(list_walker)
         frame = urwid.Frame(frame_body,urwid.AttrMap(urwid.Text(_("Profile Manager"),align='center'),'title'))
         self.main_widget = urwid.LineBox(frame)
         urwid.WidgetWrap.__init__(self, self.main_widget)
+        if self._autoconnect:
+            self.autoconnect(autoconnect)
+
+    def autoconnect(self, profile_keys):
+        """Automatically connect profiles
+
+        @param profile_keys(iterable): list of profile keys to connect
+        """
+        if not profile_keys:
+            log.warning("No profile given to autoconnect")
+            return
+        self._autoconnect = True
+        self._autoconnect_profiles=[]
+        self._do_autoconnect(profile_keys)
+
 
     def keypress(self, size, key):
         if key == a_key['APP_QUIT']:
@@ -80,11 +135,41 @@
                     return
         return super(ProfileManager, self).keypress(size, key)
 
-    def __refillProfiles(self):
+    def _do_autoconnect(self, profile_keys):
+        """Connect automatically given profiles
+
+        @param profile_kes(iterable): profiles to connect
+        """
+        assert self._autoconnect
+
+        def authenticate_cb(callback_id, data, profile):
+
+            if C.bool(data['validated']):
+                self._autoconnect_profiles.append(profile)
+                if len(self._autoconnect_profiles) == len(profile_keys):
+                    # all the profiles have been validated
+                    self.host.plug_profiles(self._autoconnect_profiles)
+            else:
+                # a profile is not validated, we go to manual mode
+                self._autoconnect=False
+
+        for profile_key in profile_keys:
+            profile = self.host.bridge.getProfileName(profile_key)
+            if not profile:
+                self._autoconnect = False # manual mode
+                msg = _("Trying to plug an unknown profile key ({})".format(profile_key))
+                log.warning(msg)
+                popup = sat_widgets.Alert(_("Profile plugging in error"), msg, ok_cb=self.host.removePopUp)
+                self.host.showPopUp(popup)
+                break
+            self.host.launchAction(C.AUTHENTICATE_PROFILE_ID, callback=authenticate_cb, profile=profile)
+
+    def refillProfiles(self):
         """Update the list of profiles"""
         profiles = self.host.bridge.getProfilesList()
         profiles.sort()
         self.list_profile.changeValues(profiles)
+        self.host.redraw()
 
     def cancelDialog(self, button):
         self.host.removePopUp()
@@ -92,105 +177,120 @@
     def newProfile(self, button, edit):
         """Create the profile"""
         name = edit.get_edit_text()
-        self.host.bridge.asyncCreateProfile(name, callback=lambda: self._newProfileCreated(name), errback=self._profileCreationFailure)
+        self.host.bridge.asyncCreateProfile(name, callback=lambda: self.newProfileCreated(name), errback=self.profileCreationFailure)
 
-    def _newProfileCreated(self, name):
-        self.__refillProfiles()
-        #We select the profile created in the list
-        self.list_profile.selectValue(name)
+    def newProfileCreated(self, profile):
         self.host.removePopUp()
+        self.refillProfiles()
+        self.list_profile.selectValue(profile)
+        self.current.profile=profile
+        self.getConnectionParams(profile)
         self.host.redraw()
 
-    def _profileCreationFailure(self, reason):
+    def profileCreationFailure(self, reason):
         self.host.removePopUp()
         if reason == "ConflictError":
             message = _("A profile with this name already exists")
         elif reason == "CancelError":
             message = _("Profile creation cancelled by backend")
+        elif reason == "ValueError":
+            message = _("You profile name is not valid") # TODO: print a more informative message (empty name, name starting with '@')
         else:
-            message = _("Unknown reason (%s)") % reason
-        popup = Alert(_("Can't create profile"), message, ok_cb=self.host.removePopUp)
+            message = _("Can't create profile ({})").format(reason)
+        popup = sat_widgets.Alert(_("Can't create profile"), message, ok_cb=self.host.removePopUp)
         self.host.showPopUp(popup)
 
     def deleteProfile(self, button):
-        profile_name = self.list_profile.getSelectedValue()
-        if profile_name:
-            self.host.bridge.asyncDeleteProfile(profile_name, callback=self.__refillProfiles)
+        if self.current.profile:
+            self.host.bridge.asyncDeleteProfile(self.current.profile, callback=self.refillProfiles)
+            self.resetFields()
         self.host.removePopUp()
 
     def onNewProfile(self, e):
-        pop_up_widget = InputDialog(_("New profile"), _("Please enter a new profile name"), cancel_cb=self.cancelDialog, ok_cb=self.newProfile)
+        pop_up_widget = sat_widgets.InputDialog(_("New profile"), _("Please enter a new profile name"), cancel_cb=self.cancelDialog, ok_cb=self.newProfile)
         self.host.showPopUp(pop_up_widget)
 
     def onDeleteProfile(self, e):
-        pop_up_widget = ConfirmDialog(_("Are you sure you want to delete the profile %s ?") % self.list_profile.getSelectedValue(), no_cb=self.cancelDialog, yes_cb=self.deleteProfile)
-        self.host.showPopUp(pop_up_widget)
+        if self.current.profile:
+            pop_up_widget = sat_widgets.ConfirmDialog(_("Are you sure you want to delete the profile {} ?").format(self.current.profile), no_cb=self.cancelDialog, yes_cb=self.deleteProfile)
+            self.host.showPopUp(pop_up_widget)
 
-    def getXMPPParams(self, profile):
-        """This is called from PrimitivusApp.launchAction when the profile has been authenticated.
+    def resetFields(self):
+        """Set profile to None, and reset fields"""
+        self.current.profile=None
+        self.login_wid.set_edit_text("")
+        self.pass_wid.set_edit_text("")
+        self.list_profile.unselectAll(invisible=True)
+
+    def getConnectionParams(self, profile):
+        """Get login and password and display them
 
         @param profile: %(doc_profile)s
         """
         def setJID(jabberID):
             self.login_wid.set_edit_text(jabberID)
-            self.host.redraw()
+            self.current.login = jabberID
+            self.host.redraw() # FIXME: redraw should be avoided
 
         def setPassword(password):
             self.pass_wid.set_edit_text(password)
+            self.current.password = password
             self.host.redraw()
 
-        self.list_profile.selectValue(profile, move_focus=False)
-        self.selected_profile = profile
         self.host.bridge.asyncGetParamA("JabberID", "Connection", profile_key=profile, callback=setJID, errback=self.getParamError)
         self.host.bridge.asyncGetParamA("Password", "Connection", profile_key=profile, callback=setPassword, errback=self.getParamError)
 
+    def updateConnectionParams(self):
+        """Check if connection parameters have changed, and update them if so"""
+        if self.current.profile:
+            login = self.login_wid.get_edit_text()
+            password = self.pass_wid.get_edit_text()
+            if login != self.current.login and self.current.login is not None:
+                self.current.login = login
+                self.host.bridge.setParam("JabberID", login, "Connection", profile_key=self.current.profile)
+                log.info("login updated for profile [{}]".format(self.current.profile))
+            if password != self.current.password and self.current.password is not None:
+                self.current.password = password
+                self.host.bridge.setParam("Password", password, "Connection", profile_key=self.current.profile)
+                log.info("password updated for profile [{}]".format(self.current.profile))
+
     def onProfileChange(self, list_wid):
         """This is called when a profile is selected in the profile list.
 
         @param list_wid: the List widget who sent the event
         """
-        profile_name = list_wid.getSelectedValue()
-        if not profile_name or profile_name == self.selected_profile:
-            return  # avoid infinite loop
-        if self.selected_profile:
-            list_wid.selectValue(self.selected_profile, move_focus=False)
-        else:
-            list_wid.unselectAll(invisible=True)
-        self.host.redraw()
-        self.host.profile = profile_name  # FIXME: EXTREMELY DIRTY, needed for sat_frontends.tools.xmlui.XMLUI._xmluiLaunchAction
-        self.host.launchAction(C.AUTHENTICATE_PROFILE_ID, {'caller': 'profile_manager'}, profile_key=profile_name)
+        self.updateConnectionParams()
+        focused = list_wid.focus
+        selected = focused.getState()
+        if not selected: # profile was just unselected
+            return
+        focused.setState(False, invisible=True) # we don't want the widget to be selected until we are sure we can access it
+        def authenticate_cb(callback_id, data, profile):
+            if C.bool(data['validated']):
+                self.current.profile = profile
+                focused.setState(True, invisible=True)
+                self.getConnectionParams(profile)
+                self.host.redraw()
+        self.host.launchAction(C.AUTHENTICATE_PROFILE_ID, callback=authenticate_cb, profile=focused.text)
 
-    def onConnectProfile(self, button):
-        profile_name = self.list_profile.getSelectedValue()
-        assert(profile_name == self.selected_profile)  # if not, there's a bug somewhere...
-        if not profile_name:
-            pop_up_widget = Alert(_('No profile selected'), _('You need to create and select a profile before connecting'), ok_cb=self.cancelDialog)
+    def onConnectProfiles(self, button):
+        """Connect the profiles and start the main widget
+
+        @param button: the connect button
+        """
+        if self._autoconnect:
+            pop_up_widget = sat_widgets.Alert(_('Internal error'), _('You can connect manually and automatically at the same time'), ok_cb=self.cancelDialog)
             self.host.showPopUp(pop_up_widget)
-        elif profile_name[0] == '@':
-            pop_up_widget = Alert(_('Bad profile name'), _("A profile name can't start with a @"), ok_cb=self.cancelDialog)
+            return
+        self.updateConnectionParams()
+        profiles = self.list_profile.getSelectedValues()
+        if not profiles:
+            pop_up_widget = sat_widgets.Alert(_('No profile selected'), _('You need to create and select at least one profile before connecting'), ok_cb=self.cancelDialog)
             self.host.showPopUp(pop_up_widget)
         else:
-            profile = self.host.bridge.getProfileName(profile_name)
-            assert(profile)
-            #TODO: move this to quick_app
-            self.host.bridge.asyncGetParamA("JabberID", "Connection", profile_key=profile,
-                                            callback=lambda old_jid: self.__old_jidReceived(old_jid, profile), errback=self.getParamError)
-
-    def __old_jidReceived(self, old_jid, profile):
-        self.host.bridge.asyncGetParamA("Password", "Connection", profile_key=profile,
-                                        callback=lambda old_pass: self.__old_passReceived(old_jid, old_pass, profile), errback=self.getParamError)
+            # All profiles in the list are already validated, so we can plug them directly
+            self.host.plug_profiles(profiles)
 
-    def __old_passReceived(self, old_jid, old_pass, profile):
-        """Check if we have new jid/pass, save them if it is the case, and plug profile"""
-        new_jid = self.login_wid.get_edit_text()
-        new_pass = self.pass_wid.get_edit_text()
-
-        if old_jid != new_jid:
-            self.host.bridge.setParam("JabberID", new_jid, "Connection", profile_key=profile)
-        if old_pass != new_pass:
-            self.host.bridge.setParam("Password", new_pass, "Connection", profile_key=profile)
-        self.host.plug_profile(profile)
-
-    def getParamError(self, ignore):
-        popup = Alert("Error", _("Can't get profile parameter"), ok_cb=self.host.removePopUp)
+    def getParamError(self, dummy):
+        popup = sat_widgets.Alert("Error", _("Can't get profile parameter"), ok_cb=self.host.removePopUp)
         self.host.showPopUp(popup)