comparison 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
comparison
equal deleted inserted replaced
1264:60dfa2f5d61f 1265:e3a9ea76de35
16 16
17 # You should have received a copy of the GNU Affero General Public License 17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. 18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 19
20 from sat.core.i18n import _ 20 from sat.core.i18n import _
21 from sat.core import log as logging
22 log = logging.getLogger(__name__)
21 from sat_frontends.primitivus.constants import Const as C 23 from sat_frontends.primitivus.constants import Const as C
24 from sat_frontends.primitivus.keys import action_key_map as a_key
25 from urwid_satext import sat_widgets
22 import urwid 26 import urwid
23 from urwid_satext.sat_widgets import AdvancedEdit, Password, List, InputDialog, ConfirmDialog, Alert 27
24 from sat_frontends.primitivus.keys import action_key_map as a_key 28 class ProfileRecord(object):
29
30 def __init__(self, profile=None, login=None, password=None):
31 self._profile = profile
32 self._login = login
33 self._password = password
34
35 @property
36 def profile(self):
37 return self._profile
38
39 @profile.setter
40 def profile(self, value):
41 self._profile = value
42 # if we change the profile,
43 # we must have no login/password until backend give them
44 self._login = self._password = None
45
46 @property
47 def login(self):
48 return self._login
49
50 @login.setter
51 def login(self, value):
52 self._login = value
53
54 @property
55 def password(self):
56 return self._password
57
58 @password.setter
59 def password(self, value):
60 self._password = value
25 61
26 62
27 class ProfileManager(urwid.WidgetWrap): 63 class ProfileManager(urwid.WidgetWrap):
28 64 """Class with manage profiles creation/deletion/connection"""
29 def __init__(self, host): 65
66 def __init__(self, host, autoconnect=None):
67 """Create the manager
68
69 @param host: %(doc_host)s
70 @param autoconnect(iterable): list of profiles to connect automatically
71 """
30 self.host = host 72 self.host = host
31 #profiles list 73 self._autoconnect = bool(autoconnect)
74 self.current = ProfileRecord()
32 profiles = self.host.bridge.getProfilesList() 75 profiles = self.host.bridge.getProfilesList()
33 profiles.sort() 76 profiles.sort()
34 77
35 #login & password box must be created before list because of onProfileChange 78 #login & password box must be created before list because of onProfileChange
36 self.login_wid = AdvancedEdit(_('Login:'), align='center') 79 self.login_wid = sat_widgets.AdvancedEdit(_('Login:'), align='center')
37 self.pass_wid = Password(_('Password:'), align='center') 80 self.pass_wid = sat_widgets.Password(_('Password:'), align='center')
38 81
39 self.selected_profile = None # allow to reselect the previous selection until the profile is authenticated 82 style = ['no_first_select']
40 style = ['single'] 83 self.list_profile = sat_widgets.List(profiles, style=style, align='center', on_change=self.onProfileChange)
41 if self.host.options.profile:
42 style.append('no_first_select')
43 self.list_profile = List(profiles, style=style, align='center', on_change=self.onProfileChange)
44 84
45 #new & delete buttons 85 #new & delete buttons
46 buttons = [urwid.Button(_("New"), self.onNewProfile), 86 buttons = [urwid.Button(_("New"), self.onNewProfile),
47 urwid.Button(_("Delete"), self.onDeleteProfile)] 87 urwid.Button(_("Delete"), self.onDeleteProfile)]
48 buttons_flow = urwid.GridFlow(buttons, max([len(button.get_label()) for button in buttons])+4, 1, 1, 'center') 88 buttons_flow = urwid.GridFlow(buttons, max([len(button.get_label()) for button in buttons])+4, 1, 1, 'center')
49 89
50 #second part: login information: 90 #second part: login information:
51 divider = urwid.Divider('-') 91 divider = urwid.Divider('-')
52 92
53 #connect button 93 #connect button
54 connect_button = urwid.Button(_("Connect"), self.onConnectProfile) 94 connect_button = sat_widgets.CustomButton(_("Connect"), self.onConnectProfiles, align='center')
55 95
56 #we now build the widget 96 #we now build the widget
57 list_walker = urwid.SimpleFocusListWalker([buttons_flow,self.list_profile,divider,self.login_wid, self.pass_wid, connect_button]) 97 list_walker = urwid.SimpleFocusListWalker([buttons_flow,self.list_profile, divider, self.login_wid, self.pass_wid, connect_button])
58 frame_body = urwid.ListBox(list_walker) 98 frame_body = urwid.ListBox(list_walker)
59 frame = urwid.Frame(frame_body,urwid.AttrMap(urwid.Text(_("Profile Manager"),align='center'),'title')) 99 frame = urwid.Frame(frame_body,urwid.AttrMap(urwid.Text(_("Profile Manager"),align='center'),'title'))
60 self.main_widget = urwid.LineBox(frame) 100 self.main_widget = urwid.LineBox(frame)
61 urwid.WidgetWrap.__init__(self, self.main_widget) 101 urwid.WidgetWrap.__init__(self, self.main_widget)
102 if self._autoconnect:
103 self.autoconnect(autoconnect)
104
105 def autoconnect(self, profile_keys):
106 """Automatically connect profiles
107
108 @param profile_keys(iterable): list of profile keys to connect
109 """
110 if not profile_keys:
111 log.warning("No profile given to autoconnect")
112 return
113 self._autoconnect = True
114 self._autoconnect_profiles=[]
115 self._do_autoconnect(profile_keys)
116
62 117
63 def keypress(self, size, key): 118 def keypress(self, size, key):
64 if key == a_key['APP_QUIT']: 119 if key == a_key['APP_QUIT']:
65 self.host.onExit() 120 self.host.onExit()
66 raise urwid.ExitMainLoop() 121 raise urwid.ExitMainLoop()
78 list_box.set_focus(current_focus, 'above' if focus_diff == 1 else 'below') 133 list_box.set_focus(current_focus, 'above' if focus_diff == 1 else 'below')
79 list_box._invalidate() 134 list_box._invalidate()
80 return 135 return
81 return super(ProfileManager, self).keypress(size, key) 136 return super(ProfileManager, self).keypress(size, key)
82 137
83 def __refillProfiles(self): 138 def _do_autoconnect(self, profile_keys):
139 """Connect automatically given profiles
140
141 @param profile_kes(iterable): profiles to connect
142 """
143 assert self._autoconnect
144
145 def authenticate_cb(callback_id, data, profile):
146
147 if C.bool(data['validated']):
148 self._autoconnect_profiles.append(profile)
149 if len(self._autoconnect_profiles) == len(profile_keys):
150 # all the profiles have been validated
151 self.host.plug_profiles(self._autoconnect_profiles)
152 else:
153 # a profile is not validated, we go to manual mode
154 self._autoconnect=False
155
156 for profile_key in profile_keys:
157 profile = self.host.bridge.getProfileName(profile_key)
158 if not profile:
159 self._autoconnect = False # manual mode
160 msg = _("Trying to plug an unknown profile key ({})".format(profile_key))
161 log.warning(msg)
162 popup = sat_widgets.Alert(_("Profile plugging in error"), msg, ok_cb=self.host.removePopUp)
163 self.host.showPopUp(popup)
164 break
165 self.host.launchAction(C.AUTHENTICATE_PROFILE_ID, callback=authenticate_cb, profile=profile)
166
167 def refillProfiles(self):
84 """Update the list of profiles""" 168 """Update the list of profiles"""
85 profiles = self.host.bridge.getProfilesList() 169 profiles = self.host.bridge.getProfilesList()
86 profiles.sort() 170 profiles.sort()
87 self.list_profile.changeValues(profiles) 171 self.list_profile.changeValues(profiles)
172 self.host.redraw()
88 173
89 def cancelDialog(self, button): 174 def cancelDialog(self, button):
90 self.host.removePopUp() 175 self.host.removePopUp()
91 176
92 def newProfile(self, button, edit): 177 def newProfile(self, button, edit):
93 """Create the profile""" 178 """Create the profile"""
94 name = edit.get_edit_text() 179 name = edit.get_edit_text()
95 self.host.bridge.asyncCreateProfile(name, callback=lambda: self._newProfileCreated(name), errback=self._profileCreationFailure) 180 self.host.bridge.asyncCreateProfile(name, callback=lambda: self.newProfileCreated(name), errback=self.profileCreationFailure)
96 181
97 def _newProfileCreated(self, name): 182 def newProfileCreated(self, profile):
98 self.__refillProfiles() 183 self.host.removePopUp()
99 #We select the profile created in the list 184 self.refillProfiles()
100 self.list_profile.selectValue(name) 185 self.list_profile.selectValue(profile)
101 self.host.removePopUp() 186 self.current.profile=profile
187 self.getConnectionParams(profile)
102 self.host.redraw() 188 self.host.redraw()
103 189
104 def _profileCreationFailure(self, reason): 190 def profileCreationFailure(self, reason):
105 self.host.removePopUp() 191 self.host.removePopUp()
106 if reason == "ConflictError": 192 if reason == "ConflictError":
107 message = _("A profile with this name already exists") 193 message = _("A profile with this name already exists")
108 elif reason == "CancelError": 194 elif reason == "CancelError":
109 message = _("Profile creation cancelled by backend") 195 message = _("Profile creation cancelled by backend")
196 elif reason == "ValueError":
197 message = _("You profile name is not valid") # TODO: print a more informative message (empty name, name starting with '@')
110 else: 198 else:
111 message = _("Unknown reason (%s)") % reason 199 message = _("Can't create profile ({})").format(reason)
112 popup = Alert(_("Can't create profile"), message, ok_cb=self.host.removePopUp) 200 popup = sat_widgets.Alert(_("Can't create profile"), message, ok_cb=self.host.removePopUp)
113 self.host.showPopUp(popup) 201 self.host.showPopUp(popup)
114 202
115 def deleteProfile(self, button): 203 def deleteProfile(self, button):
116 profile_name = self.list_profile.getSelectedValue() 204 if self.current.profile:
117 if profile_name: 205 self.host.bridge.asyncDeleteProfile(self.current.profile, callback=self.refillProfiles)
118 self.host.bridge.asyncDeleteProfile(profile_name, callback=self.__refillProfiles) 206 self.resetFields()
119 self.host.removePopUp() 207 self.host.removePopUp()
120 208
121 def onNewProfile(self, e): 209 def onNewProfile(self, e):
122 pop_up_widget = InputDialog(_("New profile"), _("Please enter a new profile name"), cancel_cb=self.cancelDialog, ok_cb=self.newProfile) 210 pop_up_widget = sat_widgets.InputDialog(_("New profile"), _("Please enter a new profile name"), cancel_cb=self.cancelDialog, ok_cb=self.newProfile)
123 self.host.showPopUp(pop_up_widget) 211 self.host.showPopUp(pop_up_widget)
124 212
125 def onDeleteProfile(self, e): 213 def onDeleteProfile(self, e):
126 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) 214 if self.current.profile:
127 self.host.showPopUp(pop_up_widget) 215 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)
128 216 self.host.showPopUp(pop_up_widget)
129 def getXMPPParams(self, profile): 217
130 """This is called from PrimitivusApp.launchAction when the profile has been authenticated. 218 def resetFields(self):
219 """Set profile to None, and reset fields"""
220 self.current.profile=None
221 self.login_wid.set_edit_text("")
222 self.pass_wid.set_edit_text("")
223 self.list_profile.unselectAll(invisible=True)
224
225 def getConnectionParams(self, profile):
226 """Get login and password and display them
131 227
132 @param profile: %(doc_profile)s 228 @param profile: %(doc_profile)s
133 """ 229 """
134 def setJID(jabberID): 230 def setJID(jabberID):
135 self.login_wid.set_edit_text(jabberID) 231 self.login_wid.set_edit_text(jabberID)
136 self.host.redraw() 232 self.current.login = jabberID
233 self.host.redraw() # FIXME: redraw should be avoided
137 234
138 def setPassword(password): 235 def setPassword(password):
139 self.pass_wid.set_edit_text(password) 236 self.pass_wid.set_edit_text(password)
237 self.current.password = password
140 self.host.redraw() 238 self.host.redraw()
141 239
142 self.list_profile.selectValue(profile, move_focus=False)
143 self.selected_profile = profile
144 self.host.bridge.asyncGetParamA("JabberID", "Connection", profile_key=profile, callback=setJID, errback=self.getParamError) 240 self.host.bridge.asyncGetParamA("JabberID", "Connection", profile_key=profile, callback=setJID, errback=self.getParamError)
145 self.host.bridge.asyncGetParamA("Password", "Connection", profile_key=profile, callback=setPassword, errback=self.getParamError) 241 self.host.bridge.asyncGetParamA("Password", "Connection", profile_key=profile, callback=setPassword, errback=self.getParamError)
146 242
243 def updateConnectionParams(self):
244 """Check if connection parameters have changed, and update them if so"""
245 if self.current.profile:
246 login = self.login_wid.get_edit_text()
247 password = self.pass_wid.get_edit_text()
248 if login != self.current.login and self.current.login is not None:
249 self.current.login = login
250 self.host.bridge.setParam("JabberID", login, "Connection", profile_key=self.current.profile)
251 log.info("login updated for profile [{}]".format(self.current.profile))
252 if password != self.current.password and self.current.password is not None:
253 self.current.password = password
254 self.host.bridge.setParam("Password", password, "Connection", profile_key=self.current.profile)
255 log.info("password updated for profile [{}]".format(self.current.profile))
256
147 def onProfileChange(self, list_wid): 257 def onProfileChange(self, list_wid):
148 """This is called when a profile is selected in the profile list. 258 """This is called when a profile is selected in the profile list.
149 259
150 @param list_wid: the List widget who sent the event 260 @param list_wid: the List widget who sent the event
151 """ 261 """
152 profile_name = list_wid.getSelectedValue() 262 self.updateConnectionParams()
153 if not profile_name or profile_name == self.selected_profile: 263 focused = list_wid.focus
154 return # avoid infinite loop 264 selected = focused.getState()
155 if self.selected_profile: 265 if not selected: # profile was just unselected
156 list_wid.selectValue(self.selected_profile, move_focus=False) 266 return
157 else: 267 focused.setState(False, invisible=True) # we don't want the widget to be selected until we are sure we can access it
158 list_wid.unselectAll(invisible=True) 268 def authenticate_cb(callback_id, data, profile):
159 self.host.redraw() 269 if C.bool(data['validated']):
160 self.host.profile = profile_name # FIXME: EXTREMELY DIRTY, needed for sat_frontends.tools.xmlui.XMLUI._xmluiLaunchAction 270 self.current.profile = profile
161 self.host.launchAction(C.AUTHENTICATE_PROFILE_ID, {'caller': 'profile_manager'}, profile_key=profile_name) 271 focused.setState(True, invisible=True)
162 272 self.getConnectionParams(profile)
163 def onConnectProfile(self, button): 273 self.host.redraw()
164 profile_name = self.list_profile.getSelectedValue() 274 self.host.launchAction(C.AUTHENTICATE_PROFILE_ID, callback=authenticate_cb, profile=focused.text)
165 assert(profile_name == self.selected_profile) # if not, there's a bug somewhere... 275
166 if not profile_name: 276 def onConnectProfiles(self, button):
167 pop_up_widget = Alert(_('No profile selected'), _('You need to create and select a profile before connecting'), ok_cb=self.cancelDialog) 277 """Connect the profiles and start the main widget
278
279 @param button: the connect button
280 """
281 if self._autoconnect:
282 pop_up_widget = sat_widgets.Alert(_('Internal error'), _('You can connect manually and automatically at the same time'), ok_cb=self.cancelDialog)
168 self.host.showPopUp(pop_up_widget) 283 self.host.showPopUp(pop_up_widget)
169 elif profile_name[0] == '@': 284 return
170 pop_up_widget = Alert(_('Bad profile name'), _("A profile name can't start with a @"), ok_cb=self.cancelDialog) 285 self.updateConnectionParams()
286 profiles = self.list_profile.getSelectedValues()
287 if not profiles:
288 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)
171 self.host.showPopUp(pop_up_widget) 289 self.host.showPopUp(pop_up_widget)
172 else: 290 else:
173 profile = self.host.bridge.getProfileName(profile_name) 291 # All profiles in the list are already validated, so we can plug them directly
174 assert(profile) 292 self.host.plug_profiles(profiles)
175 #TODO: move this to quick_app 293
176 self.host.bridge.asyncGetParamA("JabberID", "Connection", profile_key=profile, 294 def getParamError(self, dummy):
177 callback=lambda old_jid: self.__old_jidReceived(old_jid, profile), errback=self.getParamError) 295 popup = sat_widgets.Alert("Error", _("Can't get profile parameter"), ok_cb=self.host.removePopUp)
178
179 def __old_jidReceived(self, old_jid, profile):
180 self.host.bridge.asyncGetParamA("Password", "Connection", profile_key=profile,
181 callback=lambda old_pass: self.__old_passReceived(old_jid, old_pass, profile), errback=self.getParamError)
182
183 def __old_passReceived(self, old_jid, old_pass, profile):
184 """Check if we have new jid/pass, save them if it is the case, and plug profile"""
185 new_jid = self.login_wid.get_edit_text()
186 new_pass = self.pass_wid.get_edit_text()
187
188 if old_jid != new_jid:
189 self.host.bridge.setParam("JabberID", new_jid, "Connection", profile_key=profile)
190 if old_pass != new_pass:
191 self.host.bridge.setParam("Password", new_pass, "Connection", profile_key=profile)
192 self.host.plug_profile(profile)
193
194 def getParamError(self, ignore):
195 popup = Alert("Error", _("Can't get profile parameter"), ok_cb=self.host.removePopUp)
196 self.host.showPopUp(popup) 296 self.host.showPopUp(popup)