class AboutDialog(DialogBase): GITHUB_URL = 'https://github.com/ContinuumIO/anaconda-issues/issues' def __init__(self, *args, **kwargs): super(AboutDialog, self).__init__(*args, **kwargs) self.tracker = GATracker() text = """<b>Anaconda Navigator {version}</b><br> <br>Copyright © 2016 Continuum Analytics <p>Created by Continuum Analytics <br> <p>For bug reports and feature requests, please visit our """.format(version=__version__) self.label_icon = QLabel() self.label_about = QLabel(text) self.button_link = ButtonLink('Issue Tracker') self.button_label = ButtonLabel('on GitHub.') self.button_ok = QPushButton('Ok') # Widget setup self.button_ok.setMinimumWidth(70) self.label_about.setOpenExternalLinks(True) self.label_icon.setPixmap(QPixmap(images.ANACONDA_ICON_64_PATH)) self.setWindowTitle("About Anaconda Navigator") # Layouts h_layout = QHBoxLayout() h_layout.addWidget(self.label_icon, 0, Qt.AlignTop) h_layout.addSpacing(10) content_layout = QVBoxLayout() content_layout.addWidget(self.label_about, 0, Qt.AlignBottom) content_layout.setContentsMargins(0, 0, 0, 0) h_content_layout = QHBoxLayout() h_content_layout.addWidget(self.button_link, 0, Qt.AlignLeft) h_content_layout.addWidget(self.button_label, 0, Qt.AlignLeft) h_content_layout.addStretch(0) h_content_layout.setContentsMargins(0, 0, 0, 0) content_layout.addLayout(h_content_layout) h_layout.addLayout(content_layout) buttons_layout = QHBoxLayout() buttons_layout.addStretch() buttons_layout.addWidget(self.button_ok) main_layout = QVBoxLayout() main_layout.addLayout(h_layout) main_layout.addSpacing(24) main_layout.addLayout(buttons_layout) self.setLayout(main_layout) # Signals self.button_ok.clicked.connect(self.accept) self.button_link.clicked.connect( lambda: self.open_url(self.GITHUB_URL)) def open_url(self, url): self.tracker.track_event('content', 'click', url) QDesktopServices.openUrl(QUrl(url))
class ListWidgetContent(QListWidget): """ List Widget holding available videos in the learning tab. """ sig_view_video = Signal(str, str) def __init__(self, *args, **kwargs): self._main = kwargs.pop('main', None) super(ListWidgetContent, self).__init__(*args, **kwargs) self.tracker = GATracker() self._items = [] self.setObjectName('VideoListWidget') self.setResizeMode(QListWidget.Adjust) self.setMovement(QListWidget.Static) self.setFrameStyle(QListWidget.Plain) self.setSelectionMode(QAbstractItemView.NoSelection) self.setViewMode(QListWidget.IconMode) self.setFocusPolicy(Qt.NoFocus) self.setUniformItemSizes(True) def addItem(self, item): """ Add a content item to the list. """ super(ListWidgetContent, self).addItem(item) self._items.append(item) self.setItemWidget(item, item.widget) uri = item.uri title = item.title item.button_view.clicked.connect(lambda: self.launch(uri, title)) def launch(self, uri, title): """ Emit signal with youtube video identifier string. """ qurl = QUrl(uri) QDesktopServices.openUrl(qurl) self.tracker.track_event('content', 'click', uri) self.sig_view_video.emit(uri, title) def update_style_sheet(self, style_sheet=None): """ """ if style_sheet is None: style_sheet = load_style_sheet() for item in self._items: try: item_widget = self.itemWidget(item) item_widget.setStyleSheet(style_sheet) item.setSizeHint(item_widget.sizeHint()) except Exception: # This error is just in case the C++ object has been # deleted and it is not crucial to log. pass self.update() self.repaint()
class ListWidgetContent(ListWidgetBase): """List Widget holding available videos in the learning tab.""" sig_view_video = Signal(str, str) def __init__(self, *args, **kwargs): """List Widget holding available videos in the learning tab.""" self._main = kwargs.pop('main', None) # FIXME: super(ListWidgetContent, self).__init__(*args, **kwargs) self.tracker = GATracker() self.setViewMode(QListWidget.IconMode) def ordered_widgets(self): """Return a list of the ordered widgets.""" ordered_widgets = [] for item in self.items(): ordered_widgets += item.ordered_widgets() return ordered_widgets def setup_item(self, item): """Override base method.""" max_width = ( SASS_VARIABLES.WIDGET_CONTENT_TOTAL_WIDTH - 2 * SASS_VARIABLES.WIDGET_CONTENT_PADDING - 2 * SASS_VARIABLES.WIDGET_CONTENT_MARGIN ) uri = item.uri title = item.title item.button_text.clicked.connect(lambda: self.launch(uri, title)) item.button_text.sig_entered.connect( lambda: item.frame_hover.fade_in() ) item.button_text.sig_entered.connect(lambda: self.scroll_to_item(item)) item.button_text.sig_left.connect(lambda: item.frame_hover.fade_out()) item.frame_hover.sig_clicked.connect(lambda: self.launch(uri, title)) item.frame_hover.sig_clicked.connect( lambda: item.button_text.setFocus() ) item.widget.setStyleSheet(self.style_sheet) item.label_text.setText( '\n' + split_text(title, item.label_text, max_width) ) def launch(self, uri, title): """Emit signal with youtube video identifier string.""" qurl = QUrl(uri) QDesktopServices.openUrl(qurl) self.tracker.track_event('content', 'click', uri) self.sig_view_video.emit(uri, title)
class MainWindow(QMainWindow): sig_logged_in = Signal() sig_logged_out = Signal() DOCS_URL = 'https://docs.continuum.io/anaconda/navigator' VIDEOS_URL = "http://content.continuum.io/api/videos" EVENTS_URL = "http://content.continuum.io/api/events" WEBINARS_URL = "http://content.continuum.io/api/webinars" def __init__(self, splash=None): super(MainWindow, self).__init__() self.tracker = None self.splash = splash # Anaconda API self.api = AnacondaAPI() self.busy = False self.logged = False self.username = '' self._login_text = 'Sign in to Anaconda Cloud' self.first_run = CONF.get('main', 'first_run') self.application_update_version = None # Widgets self.frame_header = FrameHeader(self) self.frame_body = FrameBody(self) self.label_logo = LabelHeaderLogo('ANACONDA NAVIGATOR') self.button_logged_text = ButtonLabelLogin('') self.button_logged_username = ButtonLinkLogin('') self.label_update_available = LabelHeaderUpdate('Update available!') self.button_update_available = ButtonHeaderUpdate('Update') self.button_login = ButtonLogin(self._login_text) self.central_widget = QWidget() self.statusbar = self.statusBar() self.progressbar = QProgressBar() self.stack = TabWidgetBody(self) self.home_tab = HomeTab(parent=self) self.environments_tab = EnvironmentsTab(parent=self) self.learning_tab = CommunityTab( parent=self, tags=['webinar', 'documentation', 'video', 'training'], content_urls=[self.VIDEOS_URL, self.WEBINARS_URL]) self.community_tab = CommunityTab(parent=self, tags=['event', 'forum', 'social'], content_urls=[self.EVENTS_URL]) # self.projects_tab = ProjectsTab(parent=self) # Note: Icons are set in CSS self.stack.addTab(self.home_tab, text='Home') self.stack.addTab(self.environments_tab, text='Environments') self.stack.addTab(self.learning_tab, text='Learning') self.stack.addTab(self.community_tab, text='Community') # self.stack.addTab(self.projects_tab, 'Projects') # Widget setup self.button_login.setDefault(True) self.label_logo.setPixmap(QPixmap(images.ANACONDA_NAVIGATOR_LOGO)) self.setWindowTitle("Anaconda Navigator") self.statusbar.addPermanentWidget(self.progressbar) self.progressbar.setVisible(False) # Layout header_layout = QHBoxLayout() header_layout.addWidget(self.label_logo) header_layout.addSpacing(18) header_layout.addWidget(self.label_update_available, 0, Qt.AlignCenter) header_layout.addWidget(self.button_update_available, 0, Qt.AlignCenter) header_layout.addStretch() header_layout.addWidget(self.button_logged_text, 0, Qt.AlignTrailing) header_layout.addWidget(self.button_logged_username, 0, Qt.AlignTrailing) header_layout.addWidget(self.button_login, 0, Qt.AlignTrailing) header_layout.setContentsMargins(0, 0, 0, 0) self.frame_header.setLayout(header_layout) body_layout = QHBoxLayout() body_layout.addWidget(self.stack) body_layout.setContentsMargins(0, 0, 0, 0) self.frame_body.setLayout(body_layout) main_layout = QVBoxLayout() main_layout.addWidget(self.frame_header) main_layout.addWidget(self.frame_body) main_layout.setContentsMargins(0, 0, 0, 0) main_layout.setSpacing(0) self.central_widget.setLayout(main_layout) self.setContentsMargins(0, 0, 0, 0) self.setCentralWidget(self.central_widget) # Signals self.button_login.clicked.connect(self.login) self.button_logged_username.clicked.connect(self.open_login_page) self.button_update_available.clicked.connect(self.update_application) self.stack.currentChanged.connect(self._track_tab) # This needs to be reworked! # self.projects_tab.sig_apps_updated.connect( # self.home_tab.set_applications) # self.projects_tab.sig_apps_changed.connect( # self.home_tab.set_applications) # self.projects_tab.sig_project_updated.connect( # self.home_tab.set_applications) # self.projects_tab.sig_status_updated.connect(self.update_status_bar) # Setup self.api.set_data_directory(CHANNELS_PATH) self.update_style_sheet() # Helpers # ------------------------------------------------------------------------- def _track_tab(self, index=None): """ Tracks the active tab by index, or set `Home` when no index has been provided. """ if index is None: index = self.stack.currentIndex() text = self.stack.currentText().lower() if self.tracker: page = '/{0}'.format(text) self.tracker.track_page(page) def _metadata_updated(self, worker, path, error): self.set_splash('Updating repodata...') if error: logger.error(str(error)) if path and os.path.isfile(path): with open(path, 'r') as f: data = f.read() try: self._metadata = json.loads(data) except Exception: self._metadata = {} channels = CONF.get('main', 'conda_channels', default=tuple()) if not channels: channels = self.api.conda_get_condarc_channels() CONF.set('main', 'conda_channels', channels) CONF.set('main', 'conda_active_channels', channels) self.api.update_repodata(channels=channels) self.api.sig_repodata_updated.connect(self._repodata_updated) def _repodata_updated(self, paths): self.set_splash('Loading repodata...') self.api.sig_repodata_updated.disconnect(self._repodata_updated) if self.first_run: self.set_splash('Initial configuration...') self.api.create_default_project() worker = self.api.client_load_repodata(paths, self._metadata) worker.sig_finished.connect(self.create_application_projects) # --- Public API # ------------------------------------------------------------------------- def setup(self): """ Perform initial setup and configuration. """ self.set_splash('Updating metadata...') user = self.api.client_set_domain() self.update_login_status(user) self.setup_toolbars() self.set_application_icon() worker = self.api.update_metadata() worker.sig_finished.connect(self._metadata_updated) statusbar = self.statusBar() statusbar.setVisible(False) statusbar.setMaximumHeight(0) statusbar.hide() def create_application_projects(self, worker, output, error): if error: logger.error(str(error)) packages, apps = output self.api.create_application_projects( apps, add_project=self.first_run, ) self.post_setup(apps) self.check_for_updates(packages) def post_setup(self, apps): CONF.set('main', 'first_run', False) self.set_splash('Loading applications...') self.home_tab.setup_tab(apps) # self.set_splash('Loading projects...') # self.projects_tab.setup_tab() self.set_splash('Loading environments...') self.environments_tab.setup_tab(metadata=self._metadata) self.set_splash('Loading content...') self.community_tab.setup_tab() self.set_splash('Loading content...') self.learning_tab.setup_tab() self.update_style_sheet() self.showMaximized() self.post_visible_setup() def set_application_icon(self): """ """ app = QCoreApplication.instance() app_icon = QIcon() app_icon.addFile(images.ANACONDA_ICON_16_PATH, QSize(16, 16)) app_icon.addFile(images.ANACONDA_ICON_24_PATH, QSize(24, 24)) app_icon.addFile(images.ANACONDA_ICON_32_PATH, QSize(32, 32)) app_icon.addFile(images.ANACONDA_ICON_48_PATH, QSize(48, 48)) app_icon.addFile(images.ANACONDA_ICON_256_PATH, QSize(256, 256)) app.setWindowIcon(app_icon) def setup_toolbars(self): menubar = self.menuBar() file_menu = menubar.addMenu('&File') file_menu.addAction( create_action(self, "&Preferences", triggered=self.show_preferences, shortcut="Ctrl+P")) file_menu.addAction( create_action(self, "&Quit", triggered=self.close, shortcut="Ctrl+Q")) helpmenu = menubar.addMenu('&Help') helpmenu.addAction( create_action(self, "&Online Documentation", triggered=lambda: self.open_url(self.DOCS_URL))) helpmenu.addAction( create_action(self, "&Logs viewer", triggered=self.show_log_viewer, shortcut="F6")) helpmenu.addSeparator() helpmenu.addAction( create_action(self, "&About", triggered=self.show_about)) def post_visible_setup(self): if self.splash: self.splash.hide() CONF.set('main', 'first_run', False) # Start the tracker only after post_visible_setup self.tracker = GATracker() self._track_tab(0) # Start tracking home self.fix_tab_ordering() self.show_welcome_screen() def check_for_updates(self, packages=None): # Check if there is an update for navigator! version = self.api.conda_package_version(name='root', pkg='anaconda-navigator') # Temporal mock test # mock_versions = [version, '1.1.0'] # packages['anaconda-navigator'] = {'versions': mock_versions} self.button_update_available.setVisible(False) self.label_update_available.setVisible(False) text = '' if packages: package_data = packages.get('anaconda-navigator') if package_data: versions = package_data.get('versions') if versions and version != versions[-1]: self.application_update_version = versions[-1] self.label_update_available.setText(text) self.label_update_available.setVisible(True) self.button_update_available.setVisible(True) def fix_tab_ordering(self): return for tab in [self.community_tab, self.learning_tab]: self.setTabOrder(self.stack.tabbar.buttons[-1], tab.filter_widgets[0]) for i in range(len(tab.filter_widgets) - 1): self.setTabOrder(tab.filter_widgets[i], tab.filter_widgets[i + 1]) self.setTabOrder(tab.filter_widgets[-1], tab.text_filter) self.setTabOrder(tab.text_filter.button_icon, tab.list) self.setTabOrder(tab.list, self.button_login) # self.button_login.setFocus() self.setTabOrder(self.stack.tabbar.buttons[-1], self.environments_tab.text_search) self.environments_tab.packages_widget.table_last_row.add_focus_widget( self.button_login) self.setTabOrder(self.environments_tab.packages_widget.table_last_row, self.button_login) def update_style_sheet(self): style_sheet = load_style_sheet() # self.home_tab.update_style_sheet(style_sheet) self.environments_tab.update_style_sheet(style_sheet) # self.community_tab.update_style_sheet(style_sheet) # self.learning_tab.update_style_sheet(style_sheet) self.setStyleSheet(style_sheet) def set_splash(self, message): """ Set splash message. """ if self.splash: self.splash.show_message(message) QApplication.processEvents() # --- Login # ------------------------------------------------------------------------- def update_login_status(self, user_data=None): """ Update login button and information. """ if user_data: self.username = user_data.get('login', '') self.logged = True if self.logged: username = self.username anaconda_api_url = CONF.get('main', 'anaconda_api_url') token = self.api.client_load_token(anaconda_api_url) self.button_logged_text.setText('Signed in as') self.button_logged_username.setText(username) url = "{0}/{1}".format(CONF.get('main', 'conda_url'), username) self.button_logged_username.setToolTip(url) self.button_login.setText('Sign out') self.environments_tab.packages_widget.set_token(token) else: self.button_logged_text.setText('') self.button_logged_username.setText('') self.button_login.setText(self._login_text) QApplication.restoreOverrideCursor() def login(self): """ Open up login dialog or log out depending on logged status. """ if self.logged: QApplication.setOverrideCursor(Qt.WaitCursor) self.api.client_logout() self.api.client_remove_token() self.logged = False self.sig_logged_out.emit() self.tracker.track_event('authenticate', 'logout', label=self.username) else: dlg = AuthenticationDialog(self.api, parent=self) if self.tracker: self.tracker.track_page('/login', pagetitle='Login dialog') if dlg.exec_(): self.api.client_store_token(dlg.token) self.username = dlg.username self.logged = True self.sig_logged_in.emit() if self.tracker: self.tracker.track_event('authenticate', 'login', label=self.username) self._track_tab() self.update_login_status() logger.debug(str((self.logged, self.username))) # --- Dialogs # ------------------------------------------------------------------------- def show_preferences(self): """ Display the preferences dialog and apply the needed actions. """ dlg = PreferencesDialog(self) self.tracker.track_page('/preferences', pagetitle='Preferences dialog') set_domains = self.environments_tab.packages_widget.update_domains set_domains = self.environments_tab.packages_widget.update_domains dlg.sig_urls_updated.connect(set_domains) dlg.sig_urls_updated.connect(lambda au, cu: self.login()) dlg.exec_() self._track_tab() def show_about(self): """ Display the `About` dialog with information on the project. """ dlg = AboutDialog(self) self.tracker.track_page('/about', pagetitle='About dialog') dlg.exec_() self._track_tab() def show_log_viewer(self): """ Display the logs viewer to the user """ dlg = LogViewerDialog() self.tracker.track_page('/logs', pagetitle='Log Viewer Dialog') dlg.exec_() self._track_tab() def show_welcome_screen(self): if getattr(self, 'showme', True) and CONF.get('main', 'show_startup', True): from anaconda_navigator.widgets.splash import FirstSplash self.showme = False self.splash.hide() dlg = FirstSplash() dlg.raise_() dlg.exec_() # --- Update Navigator # ------------------------------------------------------------------------- def _update_application(self, worker, output, error): self.button_update_available.setDisabled(False) if error: text = 'Anaconda Navigator Update error:' dlg = MessageBoxError(text=text, error=error, title='Application Update Error') self.tracker.track_page('/update/error', pagetitle='Update Application Error ' 'Message Box') dlg.exec_() else: text = ('Anaconda Navigator Updated succefully.\n\n' 'Please restart the application') dlg = MessageBoxInformation(text=text, title='Application Update') self.tracker.track_page('/update/successful', pagetitle='Application Update Succesful ' 'Message Box') dlg.exec_() self._track_tab() def update_application(self): version = self.application_update_version if version: dlg = DialogUpdateApplication(version=version) self.tracker.track_page('/update', pagetitle='Update Application Dialog') reply = dlg.exec_() if reply: self.tracker.track_event('application', 'updated', version) self.busy = True pkg = 'anaconda-navigator={}'.format(version) worker = self.api.conda_install(name='root', pkgs=[pkg]) worker.sig_finished.connect(self._update_application) self.button_update_available.setDisabled(True) self._track_tab() def update_status_bar(self, message='', timeout=0, val=-1, max_val=-1): """ """ statusbar = self.statusBar() if val != -1 and max_val != -1: self.progressbar.setVisible(True) self.progressbar.setValue(val) self.progressbar.setMaximum(max_val) else: self.progressbar.setVisible(False) if message: statusbar.showMessage(message, timeout) else: statusbar.clearMessage() statusbar.setVisible(False) statusbar.setMaximumHeight(0) statusbar.hide() # --- Url handling # ------------------------------------------------------------------------- def open_url(self, url): qurl = QUrl(url) QDesktopServices.openUrl(qurl) self.tracker.track_event('help', 'documentation', url) def open_login_page(self): """ """ conda_url = CONF.get('main', 'conda_url') url = "{0}/{1}".format(conda_url, self.username) qurl = QUrl(url) QDesktopServices.openUrl(qurl) self.tracker.track_event('content', 'clicked', url) # --- Qt methods # ------------------------------------------------------------------------- def closeEvent(self, event): """ Catch close event. """ # TODO: check if an update is not in progress or things might break!! # if self.busy: show_dialog = not CONF.get('main', 'hide_quit_dialog') if show_dialog: if self.tracker: self.tracker.track_page('/quit', pagetitle='Quit dialog') dlg = QuitApplicationDialog() reply = dlg.exec_() if not reply: event.ignore() self._track_tab() def keyPressEvent(self, event): """ Qt override. """ # if event.key() in [Qt.Key_F5]: # self.update_style_sheet() super(MainWindow, self).keyPressEvent(event) def paintEvent(self, event): """ Qt override. Draw lower left border of the main Stacked Widget. """ super(MainWindow, self).paintEvent(event) tab = self.stack.tabbar tab_pos = self.mapTo(self, tab.pos()) pane_pos = self.mapTo(self, self.stack.pos()) stack_height = self.stack.height() menu_height = self.menuBar().height() header_height = 49 # From css padding = 20 # From css left = 1 # From css extra = 8 # Still wondering where this extra Y delta is deltay = menu_height + header_height + tab.height() + padding + extra x0 = tab_pos.x() + tab.width() + padding - left y0 = tab_pos.y() + deltay y1 = pane_pos.y() + stack_height + deltay - tab.height() - padding painter = QPainter(self) painter.setPen(QPen(QColor('#006f43'), 1, Qt.SolidLine, Qt.RoundCap)) painter.drawLine(x0, y0, x0, y1)
def _link_activated(self, url): QDesktopServices.openUrl(QUrl(url)) tracker = GATracker() tracker.track_event('content', 'link', url)
class AuthenticationDialog(DialogBase): FORGOT_USERNAME_URL = 'https://anaconda.org/account/forgot_username' FORGOT_PASWORD_URL = 'https://anaconda.org/account/forgot_password' REGISTER_URL = 'https://anaconda.org' def __init__(self, api, parent=None): super(AuthenticationDialog, self).__init__(parent) self.api = api self._parent = parent self.token = None self.error = None self.tracker = GATracker() # Widgets self.label_username = QLabel('Username:'******'Password:'******'You can register ') self.label_signin_text = QLabel('<hr><br><b>Already a member? ' 'Sign in!</b><br>') # For styling purposes the label next to a ButtonLink is also a button # so they align adequately self.button_register_text = ButtonLabel('You can register by ' 'visiting the') self.button_register = ButtonLink('Anaconda Cloud') self.button_register_after_text = ButtonLabel('website.') self.label_information = QLabel(''' <strong>Anaconda Cloud</strong> is where packages, notebooks, and <br> environments are shared. It provides powerful <br> collaboration and package management for open <br> source and private projects.<br> ''') self.label_message = QLabel('') self.button_forgot_username = ButtonLink('I forgot my username') self.button_forgot_password = ButtonLink('I forgot my password') self.button_login = QPushButton('Login') self.button_cancel = ButtonCancel('Cancel') self.bbox = QDialogButtonBox(Qt.Horizontal) # Widgets setup self.bbox.addButton(self.button_cancel, QDialogButtonBox.RejectRole) self.bbox.addButton(self.button_login, QDialogButtonBox.AcceptRole) self.text_username.setAttribute(Qt.WA_MacShowFocusRect, False) self.text_password.setAttribute(Qt.WA_MacShowFocusRect, False) self.setMinimumWidth(260) self.setWindowTitle('Sign in') # This allows to completely style the dialog with css using the frame self.text_password.setEchoMode(QLineEdit.Password) self.label_message.setVisible(False) # Layout grid_layout = QGridLayout() grid_layout.addWidget(self.label_username, 0, 0) grid_layout.addWidget(self.text_username, 0, 1) grid_layout.addWidget(self.label_password, 1, 0) grid_layout.addWidget(self.text_password, 1, 1) main_layout = QVBoxLayout() main_layout.addWidget(self.label_information) register_layout = QHBoxLayout() register_layout.addWidget(self.button_register_text, 0) register_layout.addWidget(self.button_register, 0, Qt.AlignLeft) register_layout.addWidget(self.button_register_after_text, 0, Qt.AlignLeft) register_layout.addStretch() register_layout.setContentsMargins(0, 0, 0, 0) main_layout.addLayout(register_layout) main_layout.addWidget(self.label_signin_text) main_layout.addLayout(grid_layout) main_layout.addSpacing(5) main_layout.addWidget(self.label_message) main_layout.addWidget(self.button_forgot_username, 0, Qt.AlignRight) main_layout.addWidget(self.button_forgot_password, 0, Qt.AlignRight) main_layout.addSpacing(15) main_layout.addWidget(self.bbox) self.setLayout(main_layout) # Signals self.button_forgot_username.clicked.connect( lambda: self.open_url(self.FORGOT_USERNAME_URL)) self.button_forgot_password.clicked.connect( lambda: self.open_url(self.FORGOT_PASWORD_URL)) self.button_register.clicked.connect( lambda: self.open_url(self.REGISTER_URL)) self.text_username.textEdited.connect(self.check_text) self.text_password.textEdited.connect(self.check_text) self.button_login.clicked.connect(self.login) self.button_cancel.clicked.connect(self.reject) # Setup self.check_text() self.update_style_sheet() self.text_username.setFocus() @property def username(self): return self.text_username.text() def update_style_sheet(self, style_sheet=None): if style_sheet is None: style_sheet = load_style_sheet() self.setStyleSheet(style_sheet) def check_text(self): """ Check that `username` and `password` are not empty and disabel/enable buttons accordingly. """ username = self.text_username.text() password = self.text_password.text() if len(username) == 0 or len(password) == 0: self.button_login.setDisabled(True) else: self.button_login.setDisabled(False) def login(self): self.button_login.setEnabled(False) QApplication.setOverrideCursor(Qt.WaitCursor) self.label_message.setText('') worker = self.api.client_login(self.text_username.text(), self.text_password.text(), 'Anaconda Navigator', '') worker.sig_finished.connect(self._finished) def _finished(self, worker, output, error): """ Method called when the anaconda-client Api has finished a process that runs in a separate worker thread. """ token = output if token: self.token = token self.accept() elif error: username = self.text_username.text() bold_username = '******'.format(username) # The error might come in (error_message, http_error) format try: error_message = eval(str(error))[0] except Exception: error_message = str(error) error_message = error_message.lower().capitalize() error_message = error_message.split(', ')[0] error_text = '<i>{0}</i>'.format(error_message) error_text = error_text.replace(username, bold_username) self.label_message.setText(error_text) self.label_message.setVisible(True) if error_message: domain = self.api.client_domain() label = '{0}/{1}: {2}'.format(domain, username, error_message.lower()) self.tracker.track_event('authenticate', 'login failed', label=label) self.text_password.setFocus() self.text_password.selectAll() self.button_login.setDisabled(False) self.check_text() QApplication.restoreOverrideCursor() def open_url(self, url): self.tracker.track_event('content', 'click', url) QDesktopServices.openUrl(QUrl(url))
class AuthenticationDialog(DialogBase): """Login dialog.""" # See https://github.com/Anaconda-Platform/anaconda-server settings USER_RE = QRegExp('^[A-Za-z0-9_][A-Za-z0-9_-]+$') FORGOT_USERNAME_URL = 'account/forgot_username' FORGOT_PASSWORD_URL = 'account/forgot_password' sig_authentication_succeeded = Signal() sig_authentication_failed = Signal() sig_url_clicked = Signal(object) def __init__(self, api, parent=None): """Login dialog.""" super(AuthenticationDialog, self).__init__(parent) self._parent = parent self.config = CONF self.api = api self.token = None self.error = None self.tracker = GATracker() self.forgot_username_url = None self.forgot_password_url = None # Widgets self.label_username = QLabel('Username:'******'Password:'******'<hr><br><b>Already a member? ' 'Sign in!</b><br>') # For styling purposes the label next to a ButtonLink is also a button # so they align adequately self.button_register_text = ButtonLabel('You can register by ' 'visiting the ') self.button_register = ButtonLink('Anaconda Cloud') self.button_register_after_text = ButtonLabel('website.') self.label_information = QLabel(''' <strong>Anaconda Cloud</strong> is where packages, notebooks, and <br> environments are shared. It provides powerful <br> collaboration and package management for open <br> source and private projects.<br> ''') self.label_message = QLabel('') self.button_forgot_username = ButtonLink('I forgot my username') self.button_forgot_password = ButtonLink('I forgot my password') self.button_login = ButtonPrimary('Login') self.button_cancel = ButtonNormal('Cancel') # Widgets setup self.button_login.setDefault(True) username_validator = QRegExpValidator(self.USER_RE) self.text_username.setValidator(username_validator) self.setMinimumWidth(260) self.setWindowTitle('Sign in') # This allows to completely style the dialog with css using the frame self.text_password.setEchoMode(QLineEdit.Password) self.label_message.setVisible(False) # Layout grid_layout = QVBoxLayout() grid_layout.addWidget(self.label_username) # grid_layout.addWidget(SpacerVertical()) grid_layout.addWidget(self.text_username) grid_layout.addWidget(SpacerVertical()) grid_layout.addWidget(self.label_password) # grid_layout.addWidget(SpacerVertical()) grid_layout.addWidget(self.text_password) main_layout = QVBoxLayout() main_layout.addWidget(self.label_information) register_layout = QHBoxLayout() register_layout.addWidget(self.button_register_text) register_layout.addWidget(self.button_register) register_layout.addWidget(self.button_register_after_text) register_layout.addStretch() main_layout.addLayout(register_layout) main_layout.addWidget(self.label_signin_text) main_layout.addLayout(grid_layout) main_layout.addWidget(SpacerVertical()) main_layout.addWidget(self.label_message) main_layout.addWidget(self.button_forgot_username, 0, Qt.AlignRight) main_layout.addWidget(self.button_forgot_password, 0, Qt.AlignRight) layout_buttons = QHBoxLayout() layout_buttons.addStretch() layout_buttons.addWidget(self.button_cancel) layout_buttons.addWidget(SpacerHorizontal()) layout_buttons.addWidget(self.button_login) main_layout.addWidget(SpacerVertical()) main_layout.addWidget(SpacerVertical()) main_layout.addLayout(layout_buttons) self.setLayout(main_layout) # Signals self.text_username.textEdited.connect(self.check_text) self.text_password.textEdited.connect(self.check_text) self.button_login.clicked.connect(self.login) self.button_cancel.clicked.connect(self.reject) # Setup self.check_text() self.update_style_sheet() self.text_username.setFocus() self.setup() def setup(self): """Setup login dialog.""" self.update_links() def update_links(self): """Update links.""" for button in [ self.button_forgot_username, self.button_forgot_password, self.button_register, ]: try: button.disconnect() except TypeError: # pragma: no cover pass # TODO, get this from anaconda client directly? # from binstar_client.utils import get_config, set_config # config = get_config() anaconda_api_url = self.config.get('main', 'anaconda_api_url', None) if anaconda_api_url: # Remove api if using a subdomain base_url = anaconda_api_url.lower().replace('//api.', '//') self.base_url = base_url # Remove api if not using a subdomain parts = base_url.lower().split('/') if parts[-1] == 'api': base_url = '/'.join(parts[:-1]) self.forgot_username_url = (base_url + '/' + self.FORGOT_USERNAME_URL) self.forgot_password_url = (base_url + '/' + self.FORGOT_PASSWORD_URL) self.button_register.clicked.connect( lambda: self.open_url(base_url)) self.button_forgot_username.clicked.connect( lambda: self.open_url(self.forgot_username_url)) self.button_forgot_password.clicked.connect( lambda: self.open_url(self.forgot_password_url)) @property def username(self): """Return the logged username.""" return self.text_username.text().lower() def update_style_sheet(self, style_sheet=None): """Update custom css style sheet.""" if style_sheet is None: style_sheet = load_style_sheet() self.setStyleSheet(style_sheet) def check_text(self): """Check that `username` and `password` are valid. If not empty and disable/enable buttons accordingly. """ username = self.text_username.text() password = self.text_password.text() if len(username) == 0 or len(password) == 0: self.button_login.setDisabled(True) else: self.button_login.setDisabled(False) def login(self): """Try to log the user in the specified anaconda api endpoint.""" self.button_login.setEnabled(False) self.text_username.setText(self.text_username.text().lower()) QApplication.setOverrideCursor(Qt.WaitCursor) self.label_message.setText('') worker = self.api.login(self.text_username.text().lower(), self.text_password.text()) worker.sig_finished.connect(self._finished) def _finished(self, worker, output, error): """Callback for the login procedure after worker has finished.""" token = output if token: self.token = token self.sig_authentication_succeeded.emit() self.accept() elif error: username = self.text_username.text().lower() bold_username = '******'.format(username) # The error might come in (error_message, http_error) format try: error_message = ast.literal_eval(str(error))[0] except Exception: # pragma: no cover error_message = str(error) error_message = error_message.lower().capitalize() error_message = error_message.split(', ')[0] error_text = '<i>{0}</i>'.format(error_message) error_text = error_text.replace(username, bold_username) self.label_message.setText(error_text) self.label_message.setVisible(True) if error_message: domain = self.api.client_domain() label = '{0}/{1}: {2}'.format(domain, username, error_message.lower()) self.tracker.track_event( 'authenticate', 'login failed', label=label, ) self.text_password.setFocus() self.text_password.selectAll() self.sig_authentication_failed.emit() self.button_login.setDisabled(False) self.check_text() QApplication.restoreOverrideCursor() def open_url(self, url): """Open given url in the default browser and log the action.""" self.tracker.track_event('content', 'click', url) self.sig_url_clicked.emit(url) QDesktopServices.openUrl(QUrl(url))
class MessageBox(DialogBase): """Base message box dialog.""" QUESTION_BOX = 100 INFORMATION_BOX = 101 ERROR_BOX = 102 REMOVE_BOX = 103 sig_url_clicked = Signal(object) def __init__(self, type_, error='', title='', text='', learn_more=None): """Base message box dialog.""" super(MessageBox, self).__init__() from anaconda_navigator.utils.analytics import GATracker self.tracker = GATracker() self.label_text = QLabel(to_text_string(text)) self.textbox_error = QTextEdit() self.button_ok = ButtonPrimary('Ok') self.button_yes = ButtonPrimary('Yes') self.button_no = ButtonNormal('No') self.button_copy = ButtonNormal('Copy text') self.button_learn = ButtonNormal('Learn more') self.button_remove = ButtonDanger('Remove') self.button_cancel = ButtonNormal('Cancel') self.button_send = ButtonNormal('Report Issue', parent=self) self.label_text.setOpenExternalLinks(False) self.label_text.setWordWrap(True) self.label_text.linkActivated.connect(self.url_clicked) self.textbox_error.setReadOnly(True) self.textbox_error.setFrameStyle(QTextEdit.Plain) self.textbox_error.setFrameShape(QTextEdit.NoFrame) self.setMinimumWidth(260) self.textbox_error.verticalScrollBar().show() self.setWindowTitle(to_text_string(title)) error = to_text_string(error).split('\n') error = '<br>'.join(error) self.textbox_error.setText(error) # Layouts layout = QVBoxLayout() layout.addWidget(self.label_text) layout.addWidget(SpacerVertical()) if error: layout.addWidget(self.textbox_error) layout.addWidget(SpacerVertical()) layout.addWidget(self.button_copy) layout.addWidget(SpacerVertical()) layout.addWidget(SpacerVertical()) layout_buttons = QHBoxLayout() layout_buttons.addStretch() layout.addLayout(layout_buttons) self.layout = layout self.setLayout(layout) # Signals self.button_copy.clicked.connect(self.copy_text) self.button_ok.clicked.connect(self.accept) self.button_yes.clicked.connect(self.accept) self.button_no.clicked.connect(self.reject) self.button_remove.clicked.connect(self.accept) self.button_cancel.clicked.connect(self.reject) self.button_send.clicked.connect(self.send) # Setup self.button_learn.setVisible(bool(learn_more)) if bool(learn_more): layout_buttons.addWidget(self.button_learn) layout_buttons.addWidget(SpacerHorizontal()) self.button_learn.clicked.connect( lambda: self.show_url(learn_more) ) if type_ == self.ERROR_BOX: layout_buttons.addWidget(self.button_send) layout_buttons.addWidget(SpacerHorizontal()) layout_buttons.addWidget(self.button_ok) self.button_yes.setVisible(False) self.button_no.setVisible(False) self.button_remove.setVisible(False) self.button_cancel.setVisible(False) elif type_ == self.INFORMATION_BOX: layout_buttons.addWidget(self.button_ok) self.button_yes.setVisible(False) self.button_no.setVisible(False) self.textbox_error.setVisible(False) self.button_copy.setVisible(False) self.button_remove.setVisible(False) self.button_cancel.setVisible(False) elif type_ == self.QUESTION_BOX: layout_buttons.addStretch() layout_buttons.addWidget(self.button_no) layout_buttons.addWidget(SpacerHorizontal()) layout_buttons.addWidget(self.button_yes) layout_buttons.addWidget(SpacerHorizontal()) self.textbox_error.setVisible(False) self.button_ok.setVisible(False) self.button_copy.setVisible(False) self.button_remove.setVisible(False) self.button_cancel.setVisible(False) elif type_ == self.REMOVE_BOX: layout_buttons.addStretch() layout_buttons.addWidget(self.button_cancel) layout_buttons.addWidget(SpacerHorizontal()) layout_buttons.addWidget(self.button_remove) layout_buttons.addWidget(SpacerHorizontal()) self.textbox_error.setVisible(False) self.button_ok.setVisible(False) self.button_copy.setVisible(False) self.button_yes.setVisible(False) self.button_no.setVisible(False) self.button_send.setVisible(False) self.layout_buttons = layout_buttons def url_clicked(self, url): """Emit url interaction.""" self.sig_url_clicked.emit(url) def copy_text(self): """Copy all the content of the displayed error message.""" self.textbox_error.selectAll() self.textbox_error.copy() def show_url(self, url=None): """Open url in default browser.""" if url: qurl = QUrl(url) QDesktopServices.openUrl(qurl) self.tracker.track_event('help', 'documentation', url) def send(self): """Send error report to github and create an issue with a template.""" import webbrowser from anaconda_navigator.utils.analytics import GATracker base = "https://github.com/ContinuumIO/anaconda-issues/issues/new?{0}" template = ''' ## Main error {text} ## Traceback ``` {trace} ``` ## System information ``` {info} ``` ''' info = GATracker().info info = '\n'.join('{}: {}'.format(k, v) for k, v in info.items()) query = parse.urlencode( { 'title': "Navigator Error", 'labels': "tag:navigator", 'body': template.format( text=self.text, trace=self.error, info=info ) } ) url = base.format(query) webbrowser.open_new_tab(url)
def _link_activated(url): QDesktopServices.openUrl(QUrl(url)) from anaconda_navigator.utils.analytics import GATracker tracker = GATracker() tracker.track_event('content', 'link', url)