accounts.py 32.4 KB
Newer Older
1 2 3
# Copyright (C) 2010 AG Projects. See LICENSE for details.
#

4
__all__ = ['AccountModel', 'ActiveAccountModel', 'AccountSelector', 'AddAccountDialog', 'ServerToolsAccountModel', 'ServerToolsWindow']
5 6 7

import os
import re
8
import sys
9 10 11 12 13
import urllib
import urllib2
from collections import defaultdict

from PyQt4 import uic
14
from PyQt4.QtCore import Qt, QAbstractListModel, QModelIndex, QUrl
15
from PyQt4.QtGui  import QAction, QButtonGroup, QComboBox, QIcon, QMenu, QMovie, QPalette, QPixmap, QSortFilterProxyModel, QStyledItemDelegate
16 17
from PyQt4.QtNetwork import QNetworkAccessManager
from PyQt4.QtWebKit  import QWebView
18

19
import cjson
20
from application.notification import IObserver, NotificationCenter
21
from application.python import Null
22
from gnutls.errors import GNUTLSError
23 24
from zope.interface import implements

25 26
from sipsimple.account import Account, AccountManager, BonjourAccount
from sipsimple.configuration import DuplicateIDError
27
from sipsimple.configuration.settings import SIPSimpleSettings
28
from sipsimple.threading import run_in_thread
29
from sipsimple.util import user_info
30 31

from blink.resources import Resources
32
from blink.widgets.labels import Status
33
from blink.util import QSingleton, call_in_gui_thread, run_in_gui_thread
34 35 36


class AccountInfo(object):
37
    def __init__(self, account, icon=None):
38 39 40 41
        self.account = account
        self.icon = icon
        self.registration_state = None

42 43 44 45
    @property
    def name(self):
        return u'Bonjour' if self.account is BonjourAccount() else unicode(self.account.id)

46 47 48 49 50
    def __eq__(self, other):
        if isinstance(other, basestring):
            return self.name == other
        elif isinstance(other, (Account, BonjourAccount)):
            return self.account == other
51 52
        elif isinstance(other, AccountInfo):
            return self.account == other.account
53 54 55 56 57 58 59 60 61 62 63 64 65 66
        return False

    def __ne__(self, other):
        return not self.__eq__(other)


class AccountModel(QAbstractListModel):
    implements(IObserver)

    def __init__(self, parent=None):
        super(AccountModel, self).__init__(parent)
        self.accounts = []

        notification_center = NotificationCenter()
67
        notification_center.add_observer(self, name='CFGSettingsObjectDidChange')
68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106
        notification_center.add_observer(self, name='SIPAccountWillRegister')
        notification_center.add_observer(self, name='SIPAccountRegistrationDidSucceed')
        notification_center.add_observer(self, name='SIPAccountRegistrationDidFail')
        notification_center.add_observer(self, name='SIPAccountRegistrationDidEnd')
        notification_center.add_observer(self, name='BonjourAccountWillRegister')
        notification_center.add_observer(self, name='BonjourAccountRegistrationDidSucceed')
        notification_center.add_observer(self, name='BonjourAccountRegistrationDidFail')
        notification_center.add_observer(self, name='BonjourAccountRegistrationDidEnd')
        notification_center.add_observer(self, sender=AccountManager())

    def rowCount(self, parent=QModelIndex()):
        return len(self.accounts)

    def data(self, index, role=Qt.DisplayRole):
        if not index.isValid():
            return None
        account_info = self.accounts[index.row()]
        if role == Qt.DisplayRole:
            return account_info.name
        elif role == Qt.DecorationRole:
            return account_info.icon
        elif role == Qt.UserRole:
            return account_info
        return None

    @run_in_gui_thread
    def handle_notification(self, notification):
        handler = getattr(self, '_NH_%s' % notification.name, Null)
        handler(notification)

    def _NH_SIPAccountManagerDidAddAccount(self, notification):
        account = notification.data.account
        icon = None
        if account is BonjourAccount():
            pixmap = QPixmap()
            if pixmap.load(Resources.get('icons/bonjour.png')):
                pixmap = pixmap.scaled(16, 16, Qt.KeepAspectRatio, Qt.SmoothTransformation)
                icon = QIcon(pixmap)
        self.beginInsertRows(QModelIndex(), len(self.accounts), len(self.accounts))
107
        self.accounts.append(AccountInfo(account, icon))
108 109
        self.endInsertRows()

110 111 112 113 114
    def _NH_CFGSettingsObjectDidChange(self, notification):
        if isinstance(notification.sender, (Account, BonjourAccount)):
            position = self.accounts.index(notification.sender)
            self.dataChanged.emit(self.index(position), self.index(position))

115 116 117 118 119 120 121
    def _NH_SIPAccountManagerDidRemoveAccount(self, notification):
        position = self.accounts.index(notification.data.account)
        self.beginRemoveRows(QModelIndex(), position, position)
        del self.accounts[position]
        self.endRemoveRows()

    def _NH_SIPAccountWillRegister(self, notification):
122 123 124 125
        try:
            position = self.accounts.index(notification.sender)
        except ValueError:
            return
126 127 128 129
        self.accounts[position].registration_state = 'started'
        self.dataChanged.emit(self.index(position), self.index(position))

    def _NH_SIPAccountRegistrationDidSucceed(self, notification):
130 131 132 133
        try:
            position = self.accounts.index(notification.sender)
        except ValueError:
            return
134 135 136 137
        self.accounts[position].registration_state = 'succeeded'
        self.dataChanged.emit(self.index(position), self.index(position))

    def _NH_SIPAccountRegistrationDidFail(self, notification):
138 139 140 141
        try:
            position = self.accounts.index(notification.sender)
        except ValueError:
            return
142 143 144 145
        self.accounts[position].registration_state = 'failed'
        self.dataChanged.emit(self.index(position), self.index(position))

    def _NH_SIPAccountRegistrationDidEnd(self, notification):
146 147 148 149
        try:
            position = self.accounts.index(notification.sender)
        except ValueError:
            return
150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173
        self.accounts[position].registration_state = 'ended'
        self.dataChanged.emit(self.index(position), self.index(position))

    _NH_BonjourAccountWillRegister = _NH_SIPAccountWillRegister
    _NH_BonjourAccountRegistrationDidSucceed = _NH_SIPAccountRegistrationDidSucceed
    _NH_BonjourAccountRegistrationDidFail = _NH_SIPAccountRegistrationDidFail
    _NH_BonjourAccountRegistrationDidEnd = _NH_SIPAccountRegistrationDidEnd


class ActiveAccountModel(QSortFilterProxyModel):
    def __init__(self, model, parent=None):
        super(ActiveAccountModel, self).__init__(parent)
        self.setSourceModel(model)
        self.setDynamicSortFilter(True)

    def filterAcceptsRow(self, source_row, source_parent):
        source_model = self.sourceModel()
        source_index = source_model.index(source_row, 0, source_parent)
        account_info = source_model.data(source_index, Qt.UserRole)
        return account_info.account.enabled


class AccountDelegate(QStyledItemDelegate):
    def paint(self, painter, option, index):
174
        account_info = index.data(Qt.UserRole)
175 176 177 178 179 180 181 182 183 184 185 186
        if account_info.registration_state == 'succeeded':
            option.palette.setColor(QPalette.Text, Qt.black)
        else:
            option.palette.setColor(QPalette.Text, Qt.gray)
        super(AccountDelegate, self).paint(painter, option, index)


class AccountSelector(QComboBox):
    implements(IObserver)

    def __init__(self, parent=None):
        super(AccountSelector, self).__init__(parent)
187 188
        self.currentIndexChanged[int].connect(self._SH_SelectionChanged)
        self.model().dataChanged.connect(self._SH_DataChanged)
189 190 191 192 193 194 195
        self.view().setItemDelegate(AccountDelegate(self.view()))

        notification_center = NotificationCenter()
        notification_center.add_observer(self, name="SIPAccountManagerDidChangeDefaultAccount")
        notification_center.add_observer(self, name="SIPAccountManagerDidStart")

    def setModel(self, model):
196 197
        self.model().dataChanged.disconnect(self._SH_DataChanged)
        model.dataChanged.connect(self._SH_DataChanged)
198 199
        super(AccountSelector, self).setModel(model)

200
    def _SH_DataChanged(self, topLeft, bottomRight):
201 202
        index = self.currentIndex()
        if topLeft.row() <= index <= bottomRight.row():
203
            account_info = self.itemData(index)
204 205 206 207 208 209 210
            palette = self.palette()
            if account_info.registration_state == 'succeeded':
                palette.setColor(QPalette.Text, Qt.black)
            else:
                palette.setColor(QPalette.Text, Qt.gray)
            self.setPalette(palette)

211
    def _SH_SelectionChanged(self, index):
212 213
        if index == -1:
            return
214
        account_info = self.itemData(index)
215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241
        palette = self.palette()
        if account_info.registration_state == 'succeeded':
            palette.setColor(QPalette.Text, Qt.black)
        else:
            palette.setColor(QPalette.Text, Qt.gray)
        self.setPalette(palette)

    @run_in_gui_thread
    def handle_notification(self, notification):
        handler = getattr(self, '_NH_%s' % notification.name, Null)
        handler(notification)

    def _NH_SIPAccountManagerDidStart(self, notification):
        account = AccountManager().default_account
        if account is not None:
            model = self.model()
            source_model = model.sourceModel()
            account_index = source_model.accounts.index(account)
            self.setCurrentIndex(model.mapFromSource(source_model.index(account_index)).row())

    def _NH_SIPAccountManagerDidChangeDefaultAccount(self, notification):
        account = notification.data.account
        if account is not None:
            model = self.model()
            source_model = model.sourceModel()
            account_index = source_model.accounts.index(account)
            self.setCurrentIndex(model.mapFromSource(source_model.index(account_index)).row())
242 243 244 245 246 247 248


ui_class, base_class = uic.loadUiType(Resources.get('add_account.ui'))

class AddAccountDialog(base_class, ui_class):
    __metaclass__ = QSingleton

249 250
    implements(IObserver)

251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282
    def __init__(self, parent=None):
        super(AddAccountDialog, self).__init__(parent)
        with Resources.directory:
            self.setupUi(self)
        self.background_frame.setStyleSheet("")
        self.button_group = QButtonGroup(self)
        self.button_group.setObjectName("button_group")
        self.button_group.addButton(self.add_account_button, self.panel_view.indexOf(self.add_account_panel))
        self.button_group.addButton(self.create_account_button, self.panel_view.indexOf(self.create_account_panel))
        font = self.title_label.font()
        font.setPointSizeF(self.info_label.fontInfo().pointSizeF() + 3)
        font.setFamily("Sans Serif")
        self.title_label.setFont(font)
        font_metrics = self.create_status_label.fontMetrics()
        self.create_status_label.setMinimumHeight(font_metrics.height() + 2*(font_metrics.height() + font_metrics.leading())) # reserve space for 3 lines
        font_metrics = self.email_note_label.fontMetrics()
        self.email_note_label.setMinimumWidth(font_metrics.width(u'The E-mail address is used when sending voicemail')) # hack to make text justification look nice everywhere
        self.add_account_button.setChecked(True)
        self.panel_view.setCurrentWidget(self.add_account_panel)
        self.new_password_editor.textChanged.connect(self._SH_PasswordTextChanged)
        self.button_group.buttonClicked[int].connect(self._SH_PanelChangeRequest)
        self.accept_button.clicked.connect(self._SH_AcceptButtonClicked)
        self.display_name_editor.statusChanged.connect(self._SH_ValidityStatusChanged)
        self.name_editor.statusChanged.connect(self._SH_ValidityStatusChanged)
        self.username_editor.statusChanged.connect(self._SH_ValidityStatusChanged)
        self.sip_address_editor.statusChanged.connect(self._SH_ValidityStatusChanged)
        self.password_editor.statusChanged.connect(self._SH_ValidityStatusChanged)
        self.new_password_editor.statusChanged.connect(self._SH_ValidityStatusChanged)
        self.verify_password_editor.statusChanged.connect(self._SH_ValidityStatusChanged)
        self.email_address_editor.statusChanged.connect(self._SH_ValidityStatusChanged)
        self.display_name_editor.regexp = re.compile('^.*$')
        self.name_editor.regexp = re.compile('^.+$')
283
        self.username_editor.regexp = re.compile('^\w(?<=[^0_])[\w.-]{4,31}(?<=[^_.-])$', re.IGNORECASE) # in order to enable unicode characters add re.UNICODE to flags
284
        self.sip_address_editor.regexp = re.compile('^[^@\s]+@[^@\s]+$')
285 286 287
        self.password_editor.regexp = re.compile('^.*$')
        self.new_password_editor.regexp = re.compile('^.{8,}$')
        self.verify_password_editor.regexp = re.compile('^$')
288
        self.email_address_editor.regexp = re.compile('^[^@\s]+@[^@\s]+$')
289

290 291 292 293
        account_manager = AccountManager()
        notification_center = NotificationCenter()
        notification_center.add_observer(self, sender=account_manager)

294 295
    def _get_display_name(self):
        if self.panel_view.currentWidget() is self.add_account_panel:
296
            return self.display_name_editor.text()
297
        else:
298
            return self.name_editor.text()
299 300 301 302 303 304

    def _set_display_name(self, value):
        self.display_name_editor.setText(value)
        self.name_editor.setText(value)

    def _get_username(self):
305
        return self.username_editor.text()
306 307 308 309 310

    def _set_username(self, value):
        self.username_editor.setText(value)

    def _get_sip_address(self):
311
        return self.sip_address_editor.text()
312 313 314 315 316 317

    def _set_sip_address(self, value):
        self.sip_address_editor.setText(value)

    def _get_password(self):
        if self.panel_view.currentWidget() is self.add_account_panel:
318
            return self.password_editor.text()
319
        else:
320
            return self.new_password_editor.text()
321 322 323 324 325 326

    def _set_password(self, value):
        self.password_editor.setText(value)
        self.new_password_editor.setText(value)

    def _get_verify_password(self):
327
        return self.verify_password_editor.text()
328 329 330 331 332

    def _set_verify_password(self, value):
        self.verify_password_editor.setText(value)

    def _get_email_address(self):
333
        return self.email_address_editor.text()
334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352

    def _set_email_address(self, value):
        self.email_address_editor.setText(value)

    display_name    = property(_get_display_name, _set_display_name)
    username        = property(_get_username, _set_username)
    sip_address     = property(_get_sip_address, _set_sip_address)
    password        = property(_get_password, _set_password)
    verify_password = property(_get_verify_password, _set_verify_password)
    email_address   = property(_get_email_address, _set_email_address)

    del _get_display_name, _set_display_name, _get_username, _set_username
    del _get_sip_address, _set_sip_address, _get_email_address, _set_email_address
    del _get_password, _set_password, _get_verify_password, _set_verify_password

    def _SH_AcceptButtonClicked(self):
        if self.panel_view.currentWidget() is self.add_account_panel:
            account = Account(self.sip_address)
            account.enabled = True
353
            account.display_name = self.display_name or None
354
            account.auth.password = self.password
355
            account.save()
356 357 358 359 360 361 362 363 364 365 366 367 368 369
            account_manager = AccountManager()
            account_manager.default_account = account
            self.accept()
        else:
            self.setEnabled(False)
            self.create_status_label.value = Status('Creating account on server...')
            self._create_sip_account(self.username, self.password, self.email_address, self.display_name)

    def _SH_PanelChangeRequest(self, index):
        self.panel_view.setCurrentIndex(index)
        if self.panel_view.currentWidget() is self.add_account_panel:
            inputs = [self.display_name_editor, self.sip_address_editor, self.password_editor]
        else:
            inputs = [self.name_editor, self.username_editor, self.new_password_editor, self.verify_password_editor, self.email_address_editor]
370
        self.accept_button.setEnabled(all(input.text_valid for input in inputs))
371 372

    def _SH_PasswordTextChanged(self, text):
373
        self.verify_password_editor.regexp = re.compile(u'^%s$' % re.escape(text))
374 375 376 377

    def _SH_ValidityStatusChanged(self):
        red = '#cc0000'
        # validate the add panel
378
        if not self.display_name_editor.text_valid:
379
            self.add_status_label.value = Status("Display name cannot be empty", color=red)
380
        elif not self.sip_address_editor.text_correct:
381
            self.add_status_label.value = Status("SIP address should be specified as user@domain", color=red)
382
        elif not self.sip_address_editor.text_allowed:
Luci Stanescu's avatar
Luci Stanescu committed
383
            self.add_status_label.value = Status("An account with this SIP address was already added", color=red)
384
        elif not self.password_editor.text_valid:
385 386 387 388
            self.add_status_label.value = Status("Password cannot be empty", color=red)
        else:
            self.add_status_label.value = None
        # validate the create panel
389
        if not self.name_editor.text_valid:
Dan Pascu's avatar
Dan Pascu committed
390
            self.create_status_label.value = Status("Name cannot be empty", color=red)
391
        elif not self.username_editor.text_correct:
392
            self.create_status_label.value = Status("Username should have 5 to 32 characters, start with a letter or non-zero digit, contain only letters, digits or .-_ and end with a letter or digit", color=red)
393 394 395
        elif not self.username_editor.text_allowed:
            self.create_status_label.value = Status("The username you requested is already taken. Please choose another one and try again.", color=red)
        elif not self.new_password_editor.text_valid:
396
            self.create_status_label.value = Status("Password should contain at least 8 characters", color=red)
397
        elif not self.verify_password_editor.text_valid:
398
            self.create_status_label.value = Status("Passwords do not match", color=red)
399
        elif not self.email_address_editor.text_valid:
400 401 402 403 404 405 406 407
            self.create_status_label.value = Status("E-mail address should be specified as user@domain", color=red)
        else:
            self.create_status_label.value = None
        # enable the accept button if everything is valid in the current panel
        if self.panel_view.currentWidget() is self.add_account_panel:
            inputs = [self.display_name_editor, self.sip_address_editor, self.password_editor]
        else:
            inputs = [self.name_editor, self.username_editor, self.new_password_editor, self.verify_password_editor, self.email_address_editor]
408
        self.accept_button.setEnabled(all(input.text_valid for input in inputs))
409 410 411

    def _initialize(self):
        self.display_name = user_info.fullname
412
        self.username = user_info.username.lower().replace(u' ', u'.')
413 414 415 416 417
        self.sip_address = u''
        self.password = u''
        self.verify_password = u''
        self.email_address = u''

418
    @run_in_thread('network-io')
419 420
    def _create_sip_account(self, username, password, email_address, display_name, timezone=None):
        red = '#cc0000'
421
        if timezone is None and sys.platform != 'win32':
422 423 424 425 426 427 428
            try:
                timezone = open('/etc/timezone').read().strip()
            except (OSError, IOError):
                try:
                    timezone = '/'.join(os.readlink('/etc/localtime').split('/')[-2:])
                except (OSError, IOError):
                    pass
429
        enrollment_data = dict(username=username.lower().encode('utf-8'),
430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447
                               password=password.encode('utf-8'),
                               email=email_address.encode('utf-8'),
                               display_name=display_name.encode('utf-8'),
                               tzinfo=timezone)
        try:
            settings = SIPSimpleSettings()
            response = urllib2.urlopen(settings.server.enrollment_url, urllib.urlencode(dict(enrollment_data)))
            response_data = cjson.decode(response.read().replace(r'\/', '/'))
            response_data = defaultdict(lambda: None, response_data)
            if response_data['success']:
                from blink import Blink
                try:
                    certificate_path = None
                    passport = response_data['passport']
                    if passport is not None:
                        certificate_path = Blink().save_certificates(response_data['sip_address'], passport['crt'], passport['key'], passport['ca'])
                except (GNUTLSError, IOError, OSError):
                    pass
448 449 450
                account_manager = AccountManager()
                try:
                    account = Account(response_data['sip_address'])
451
                except DuplicateIDError:
452
                    account = account_manager.get_account(response_data['sip_address'])
453
                account.enabled = True
454
                account.display_name = display_name or None
455 456 457 458 459
                account.auth.password = password
                account.sip.outbound_proxy = response_data['outbound_proxy']
                account.nat_traversal.msrp_relay = response_data['msrp_relay']
                account.xcap.xcap_root = response_data['xcap_root']
                account.tls.certificate = certificate_path
460
                account.server.conference_server = response_data['conference_server']
461 462 463 464 465
                account.server.settings_url = response_data['settings_url']
                account.save()
                account_manager.default_account = account
                call_in_gui_thread(self.accept)
            elif response_data['error'] == 'user_exists':
466
                call_in_gui_thread(self.username_editor.addException, username)
467 468 469 470 471 472 473 474 475
            else:
                call_in_gui_thread(setattr, self.create_status_label, 'value', Status(response_data['error_message'], color=red))
        except (cjson.DecodeError, KeyError):
            call_in_gui_thread(setattr, self.create_status_label, 'value', Status('Illegal server response', color=red))
        except urllib2.URLError, e:
            call_in_gui_thread(setattr, self.create_status_label, 'value', Status('Failed to contact server: %s' % e.reason, color=red))
        finally:
            call_in_gui_thread(self.setEnabled, True)

476 477 478 479 480 481 482 483 484 485 486
    @run_in_gui_thread
    def handle_notification(self, notification):
        handler = getattr(self, '_NH_%s' % notification.name, Null)
        handler(notification)

    def _NH_SIPAccountManagerDidAddAccount(self, notification):
        self.sip_address_editor.addException(notification.data.account.id)

    def _NH_SIPAccountManagerDidRemoveAccount(self, notification):
        self.sip_address_editor.removeException(notification.data.account.id)

487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503
    def open_for_add(self):
        self.add_account_button.click()
        self.add_account_button.setFocus()
        self.accept_button.setEnabled(False)
        self._initialize()
        self.show()

    def open_for_create(self):
        self.create_account_button.click()
        self.create_account_button.setFocus()
        self.accept_button.setEnabled(False)
        self._initialize()
        self.show()

del ui_class, base_class


504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530
# Account server tools
#

class ServerToolsAccountModel(QSortFilterProxyModel):
    def __init__(self, model, parent=None):
        super(ServerToolsAccountModel, self).__init__(parent)
        self.setSourceModel(model)
        self.setDynamicSortFilter(True)

    def filterAcceptsRow(self, source_row, source_parent):
        source_model = self.sourceModel()
        source_index = source_model.index(source_row, 0, source_parent)
        account_info = source_model.data(source_index, Qt.UserRole)
        return bool(account_info.account is not BonjourAccount() and account_info.account.enabled and account_info.account.server.settings_url)


class ServerToolsWebView(QWebView):
    implements(IObserver)

    def __init__(self, parent=None):
        super(ServerToolsWebView, self).__init__(parent)
        self.access_manager = Null
        self.authenticated = False
        self.account = None
        self.user_agent = 'blink'
        self.tab = None
        self.task = None
531
        self.last_error = None
532
        self.realm = None
533 534 535 536
        self.urlChanged.connect(self._SH_URLChanged)

    @property
    def query_items(self):
537
        all_items = ('user_agent', 'tab', 'task', 'realm')
538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553
        return [(name, value) for name, value in self.__dict__.iteritems() if name in all_items and value is not None]

    def _get_account(self):
        return self.__dict__['account']

    def _set_account(self, account):
        notification_center = NotificationCenter()
        old_account = self.__dict__.get('account', Null)
        if account is old_account:
            return
        self.__dict__['account'] = account
        self.authenticated = False
        if old_account:
            notification_center.remove_observer(self, sender=old_account)
        if account:
            notification_center.add_observer(self, sender=account)
554 555 556
            self.realm = account.id.domain
        else:
            self.realm = None
557
        self.access_manager.authenticationRequired.disconnect(self._SH_AuthenticationRequired)
558
        self.access_manager.finished.disconnect(self._SH_Finished)
559 560
        self.access_manager = QNetworkAccessManager(self)
        self.access_manager.authenticationRequired.connect(self._SH_AuthenticationRequired)
561
        self.access_manager.finished.connect(self._SH_Finished)
562 563 564 565 566 567 568 569 570 571 572
        self.page().setNetworkAccessManager(self.access_manager)

    account = property(_get_account, _set_account)
    del _get_account, _set_account

    @run_in_gui_thread
    def handle_notification(self, notification):
        handler = getattr(self, '_NH_%s' % notification.name, Null)
        handler(notification)

    def _NH_CFGSettingsObjectDidChange(self, notification):
573
        if '__id__' in notification.data.modified or 'auth.password' in notification.data.modified:
574 575 576 577 578
            self.authenticated = False
            self.reload()

    def _SH_AuthenticationRequired(self, reply, auth):
        if self.account and not self.authenticated:
579
            auth.setUser(self.account.id.username)
580 581 582 583 584 585 586 587
            auth.setPassword(self.account.auth.password)
            self.authenticated = True
        else:
            # we were already authenticated, yet it asks for the auth again. this means our credentials are not good.
            # we do not provide credentials anymore in order to fail and not try indefinitely, but we also reset the
            # authenticated status so that we try again when the page is reloaded.
            self.authenticated = False

588 589 590 591 592 593
    def _SH_Finished(self, reply):
        if reply.error() != reply.NoError:
            self.last_error = reply.errorString()
        else:
            self.last_error = None

594
    def _SH_URLChanged(self, url):
595
        query_items = dict(url.queryItems())
596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617
        self.tab = query_items.get('tab') or self.tab
        self.task = query_items.get('task') or self.task

    def load_account_page(self, account, tab=None, task=None):
        self.tab = tab
        self.task = task
        self.account = account
        url = QUrl(account.server.settings_url)
        for name, value in self.query_items:
            url.addQueryItem(name, value)
        self.load(url)


ui_class, base_class = uic.loadUiType(Resources.get('server_tools.ui'))

class ServerToolsWindow(base_class, ui_class):
    __metaclass__ = QSingleton

    def __init__(self, model, parent=None):
        super(ServerToolsWindow, self).__init__(parent)
        with Resources.directory:
            self.setupUi(self)
618 619 620 621
        self.spinner_movie = QMovie(Resources.get('icons/servertools-spinner.mng'))
        self.spinner_label.setMovie(self.spinner_movie)
        self.spinner_label.hide()
        self.progress_bar.hide()
622 623 624 625
        while self.tab_widget.count():
            self.tab_widget.removeTab(0) # remove the tab(s) added in designer
        self.tab_widget.tabBar().hide()
        self.account_button.setMenu(QMenu(self.account_button))
Dan Pascu's avatar
Dan Pascu committed
626 627
        self.setWindowTitle('Blink Server Tools')
        self.setWindowIconText('Server Tools')
628 629 630 631 632 633 634 635 636
        self.model = model
        self.tab_widget.addTab(ServerToolsWebView(self), '')
        font = self.account_label.font()
        font.setPointSizeF(self.account_label.fontInfo().pointSizeF() + 2)
        font.setFamily("Sans Serif")
        self.account_label.setFont(font)
        self.model.rowsInserted.connect(self._SH_ModelChanged)
        self.model.rowsRemoved.connect(self._SH_ModelChanged)
        self.account_button.menu().triggered.connect(self._SH_AccountButtonMenuTriggered)
637 638 639 640
        web_view = self.tab_widget.currentWidget()
        web_view.loadStarted.connect(self._SH_WebViewLoadStarted)
        web_view.loadFinished.connect(self._SH_WebViewLoadFinished)
        web_view.loadProgress.connect(self._SH_WebViewLoadProgress)
641 642 643

    def _SH_AccountButtonMenuTriggered(self, action):
        view = self.tab_widget.currentWidget()
644
        account = action.data()
645 646 647 648
        self.account_label.setText(account.id)
        self.tab_widget.setTabText(self.tab_widget.currentIndex(), account.id)
        view.load_account_page(account, tab=view.tab, task=view.task)

649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685
    def _SH_WebViewLoadStarted(self):
        self.spinner_label.show()
        self.spinner_movie.start()
        self.progress_bar.setValue(0)
        #self.progress_bar.show()

    def _SH_WebViewLoadFinished(self, load_ok):
        self.spinner_movie.stop()
        self.spinner_label.hide()
        self.progress_bar.hide()
        if not load_ok:
            web_view = self.tab_widget.currentWidget()
            icon_path = Resources.get('icons/invalid.png')
            error_message = web_view.last_error or 'Unknown error'
            html = """
            <html>
             <head>
              <style>
                .icon    { width: 64px; height: 64px; float: left; }
                .message { margin-left: 74px; line-height: 64px; vertical-align: middle; }
              </style>
             </head>
             <body>
              <img class="icon" src="file:%s" />
              <div class="message">Failed to load web page: <b>%s</b></div>
             </body>
            </html>
            """ % (icon_path, error_message)
            web_view.loadStarted.disconnect(self._SH_WebViewLoadStarted)
            web_view.loadFinished.disconnect(self._SH_WebViewLoadFinished)
            web_view.setHtml(html)
            web_view.loadStarted.connect(self._SH_WebViewLoadStarted)
            web_view.loadFinished.connect(self._SH_WebViewLoadFinished)

    def _SH_WebViewLoadProgress(self, percent):
        self.progress_bar.setValue(percent)

686 687 688 689
    def _SH_ModelChanged(self, parent_index, start, end):
        menu = self.account_button.menu()
        menu.clear()
        for row in xrange(self.model.rowCount()):
690
            account_info = self.model.data(self.model.index(row, 0), Qt.UserRole)
691
            action = QAction(account_info.name, self)
692
            action.setData(account_info.account)
693 694 695 696 697 698
            menu.addAction(action)

    def open_settings_page(self, account):
        view = self.tab_widget.currentWidget()
        account = account or view.account
        if account is None or account.server.settings_url is None:
699
            account = self.account_button.menu().actions()[0].data()
700 701 702 703 704 705 706 707 708
        self.account_label.setText(account.id)
        self.tab_widget.setTabText(self.tab_widget.currentIndex(), account.id)
        view.load_account_page(account, tab='settings')
        self.show()

    def open_search_for_people_page(self, account):
        view = self.tab_widget.currentWidget()
        account = account or view.account
        if account is None or account.server.settings_url is None:
709
            account = self.account_button.menu().actions()[0].data()
710 711 712 713 714 715 716 717 718
        self.account_label.setText(account.id)
        self.tab_widget.setTabText(self.tab_widget.currentIndex(), account.id)
        view.load_account_page(account, tab='contacts', task='directory')
        self.show()

    def open_history_page(self, account):
        view = self.tab_widget.currentWidget()
        account = account or view.account
        if account is None or account.server.settings_url is None:
719
            account = self.account_button.menu().actions()[0].data()
720 721 722 723 724
        self.account_label.setText(account.id)
        self.tab_widget.setTabText(self.tab_widget.currentIndex(), account.id)
        view.load_account_page(account, tab='calls')
        self.show()

Saul Ibarra's avatar
Saul Ibarra committed
725
    def open_buy_pstn_access_page(self, account):
726 727 728
        view = self.tab_widget.currentWidget()
        account = account or view.account
        if account is None or account.server.settings_url is None:
729
            account = self.account_button.menu().actions()[0].data()
730 731 732 733 734
        self.account_label.setText(account.id)
        self.tab_widget.setTabText(self.tab_widget.currentIndex(), account.id)
        view.load_account_page(account, tab='payments')
        self.show()

735 736 737
del ui_class, base_class