Exemplo n.º 1
0
class SupportDialog(QObject):
    SHORT_FEEDBACK_INTERVAL = 5 * 60 * 1000
    DAYS_TO_FEEDBACK = 7
    DROPDOWN_BACKGROUND_COLOR = "#f78d1e"
    DROPDOWN_COLOR = "white"

    _sending_error = Signal()

    SUBJECT = {
        1: "TECHNICAL",
        2: "OTHER",
        3: "FEEDBACK",
    }

    def __init__(self, parent, parent_window, config, dp=1, selected_index=0):
        QObject.__init__(self, parent)

        self._parent = parent
        self._parent_window = parent_window
        self._config = config
        self._dp = dp
        self._selected_index = selected_index
        self._dialog = QDialog(parent_window)
        self._dialog.setWindowFlags(Qt.Dialog)
        self._dialog.setAttribute(Qt.WA_MacFrameworkScaled)

        self._is_opened = False
        self._pipe = None
        self._feedback_mode = False

        self._ui = Ui_Dialog()
        self._ui.setupUi(self._dialog)
        self._init_ui()
        self._old_close_event = self._dialog.closeEvent

        self._feedback_timer = QTimer(self)
        self._feedback_timer.setSingleShot(True)
        self._feedback_timer.timeout.connect(self._show_feedback_form)

        self._parent.service_started.connect(self._check_feedback_needed)
        self._parent.exit_request.connect(self._on_exit_request)

    def _init_ui(self):
        self._ui.pushButton.setEnabled(False)
        self._ui.comboBox.addItem(tr("---Select Subject---"))
        self._ui.comboBox.addItem(tr("Technical Question"))
        self._ui.comboBox.addItem(tr("Other Question"))
        self._ui.comboBox.addItem(tr("Feedback"))
        self._ui.comboBox.setCurrentIndex(self._selected_index)

        palette = self._ui.comboBox.palette()
        palette.setColor(QPalette.HighlightedText, QColor(self.DROPDOWN_COLOR))
        palette.setColor(QPalette.Highlight,
                         QColor(self.DROPDOWN_BACKGROUND_COLOR))
        self._ui.comboBox.setPalette(palette)
        palette = self._ui.comboBox.view().palette()
        palette.setColor(QPalette.HighlightedText, QColor(self.DROPDOWN_COLOR))
        palette.setColor(QPalette.Highlight,
                         QColor(self.DROPDOWN_BACKGROUND_COLOR))
        self._ui.comboBox.view().setPalette(palette)

        self._set_tooltip()

        self._ui.comboBox.currentIndexChanged.connect(self._on_index_changed)
        self._ui.plainTextEdit.textChanged.connect(self._set_tooltip)
        self._ui.pushButton.clicked.connect(self._on_send_clicked)
        self._ui.text_label.linkActivated.connect(self._on_link_activated)
        self._sending_error.connect(self._clear_pipe_state)

        self._set_fonts()

    def _set_fonts(self):
        ui = self._ui
        controls = [
            ui.plainTextEdit, ui.pushButton, ui.comboBox, ui.text_label,
            ui.checkBox
        ]

        for control in controls:
            font = control.font()
            font_size = control.font().pointSize() * self._dp
            if font_size > 0:
                control_font = QFont(font.family(), font_size)
                control_font.setBold(font.bold())
                control.setFont(control_font)

    def set_selected_index(self, selected_index):
        self._selected_index = selected_index
        if self._is_opened:
            self._ui.comboBox.setCurrentIndex(self._selected_index)

    def show(self):
        if self._parent.dialogs_opened():
            return

        self._is_opened = True
        logger.debug("Support dialog opening...")
        self._pipe = None
        self.set_selected_index(self._selected_index)
        self._ui.checkBox.setChecked(False)
        self._ui.comboBox.setEnabled(not self._feedback_mode)
        self._dialog.exec_()

        logger.debug("Support dialog closed")
        if self._pipe:
            try:
                self._pipe.stop()
                self._clear_pipe_state()
            except Exception as e:
                logger.error("Unexpected error stopping pipe: (%s)", e)
        self._is_opened = False
        self._selected_index = 0
        self._ui.plainTextEdit.document().clear()
        self._ui.checkBox.setChecked(False)

    def dialog_opened(self):
        return self._is_opened

    def close(self):
        self._dialog.close()

    def _set_tooltip(self):
        if not self._selected_index:
            tooltip = tr("Please select subject")
            self._ui.pushButton.setEnabled(False)
        elif not self._ui.plainTextEdit.document().toPlainText():
            tooltip = tr("Message can't be empty")
            self._ui.pushButton.setEnabled(False)
        else:
            tooltip = tr("Click to send message")
            self._ui.pushButton.setEnabled(True)
        self._ui.pushButton.setToolTip(tooltip)

    def _on_index_changed(self, selected_index):
        self._selected_index = selected_index
        self._set_tooltip()

    def _on_send_clicked(self):
        self._dialog.setEnabled(False)
        self._pipe = ProgressPipe(self,
                                  self._ui.pushButton,
                                  timeout=1000,
                                  final_text=tr("Sent"),
                                  final_timeout=500)
        self._pipe.pipe_finished.connect(self._on_pipe_finished)
        if self._ui.checkBox.isChecked():
            self._pipe.add_task(tr("Compressing"), self._archive_logs())
            self._pipe.add_task(tr("Uploading"), self._upload_file())
        self._pipe.add_task(tr("Sending"), self._send_message())
        self._pipe.start()

    def _on_pipe_finished(self):
        self._clear_feedback_flag()
        self.close()

    def _clear_pipe_state(self):
        self._dialog.setEnabled(True)
        try:
            self._pipe.pipe_finished.disconnect(self._on_pipe_finished)
        except Exception as e:
            logger.warning("Can't disconnect signal: %s", e)
        self._ui.pushButton.setText(tr("SEND"))

    def _send_message(self):
        def send(log_file_name=""):
            logger.debug("Support compressed log_file_name %s", log_file_name)
            if self._selected_index not in self.SUBJECT:
                logger.warning("Attempt to send message to support "
                               "with invalid subject")
                return

            subject = self.SUBJECT[self._selected_index]
            res = self._parent.web_api.send_support_message(
                subject,
                self._ui.plainTextEdit.document().toPlainText(), log_file_name)
            was_error = False
            msg = tr("Can't send message to support")
            if res and "result" in res:
                if res["result"] != "success":
                    was_error = True
                    msg = str(res.get("info", msg))
            else:
                was_error = True
            if was_error:
                self._parent.show_tray_notification(msg)
                self._sending_error.emit()
                raise SendingError(msg)

        return send

    def _archive_logs(self):
        def archive():
            # uses function attributes to track progress
            # archive.size, archive.progress, archive.stop
            logs_dir = get_bases_dir(self._config.sync_directory)
            log_files = glob("{}{}*.log".format(logs_dir, os.sep))
            log_sizes = list(map(os.path.getsize, log_files))
            # mark overall size
            archive.size = sum(log_sizes)

            old_archives = glob("{}{}2*_logs.zip".format(logs_dir, os.sep))
            try:
                list(map(remove_file, old_archives))
            except Exception as e:
                logger.warning("Can't delete old archives. Reason: (%s)", e)

            if get_free_space(logs_dir) < archive.size // 5:
                # archive.size // 5 is approx future archive size
                msg = tr("Insufficient disk space to archive logs. "
                         "Please clean disk")
                self._parent.show_tray_notification(msg)
                self._sending_error.emit()
                raise SendingError(msg)

            archive_name = time.strftime('%Y%m%d_%H%M%S_logs.zip')
            archive_path = "{}{}{}".format(logs_dir, os.sep, archive_name)
            archive_dir = op.dirname(archive_path)
            f = zipfile.ZipFile(archive_path,
                                "w",
                                compression=zipfile.ZIP_DEFLATED,
                                compresslevel=9)
            try:
                with cwd(archive_dir):
                    for i, log_file in enumerate(log_files):
                        if not op.isfile(log_file):
                            continue

                        f.write(op.basename(log_file))
                        # mark progress
                        archive.progress += log_sizes[i]
                        if archive.stop:
                            return

            except Exception as e:
                msg = tr("Can't archive logs.")
                logger.warning(msg + " Reason: (%s)", e)
                self._parent.show_tray_notification(msg)
                self._sending_error.emit()
                raise SendingError(msg)
            finally:
                f.close()
                if archive.stop:
                    remove_file(archive_path)

            return archive_path

        return archive

    def _upload_file(self):
        def upload(path):
            # uses function attributes to track progress
            # upload.size, upload.progress, upload.stop
            upload.size = op.getsize(path)

            res = self._parent.web_api.upload_file(path, "application/zip",
                                                   callback)
            was_error = False
            msg = tr("Can't upload archive file")
            if res and "result" in res:
                if res["result"] == "success":
                    filename = res.get("file_name", "")
                else:
                    was_error = True
                    msg = str(res.get("info", msg))
            else:
                was_error = True
            if was_error and not upload.stop:
                self._parent.show_tray_notification(msg)
                self._sending_error.emit()
                raise SendingError(msg)

            remove_file(path)
            return filename

        def callback(monitor):
            upload.progress = monitor.bytes_read
            if upload.stop:
                raise SendingError("Stopped")

        return upload

    def _on_link_activated(self):
        open_link(self._parent.get_help_uri())()
        self.close()

    def _check_feedback_needed(self):
        if self._feedback_timer.isActive():
            return

        start_date = get_init_done()
        now = datetime.datetime.now()
        logger.debug("Start date is %s", start_date)
        if start_date is None:
            return

        interval = (start_date - now) \
                   + datetime.timedelta(days=self.DAYS_TO_FEEDBACK)

        if interval.total_seconds() <= 0:
            logger.debug("Feedback form date time is now")
            self._show_feedback_form()
        else:
            self._feedback_timer.setInterval(interval.seconds * 1000)
            self._feedback_timer.start()
            logger.debug("Feedback form date time is %s", now + interval)

    def _show_feedback_form(self):
        if self._is_opened:
            self._feedback_timer.setInterval(self.SHORT_FEEDBACK_INTERVAL)
            self._feedback_timer.start()
            return

        self._feedback_mode = True
        self._selected_index = 3
        self._dialog.closeEvent = self._close_event
        window_title = self._dialog.windowTitle()
        label_text = self._ui.text_label.text()
        feedback_text = tr("Please leave your feedback for Pvtbox")
        self._ui.text_label.setText(
            "<html><head/><body><p>{}</p></body></html>".format(feedback_text))
        self._dialog.setWindowTitle(tr("Feedback"))
        self.show()
        self._dialog.setWindowTitle(window_title)
        self._ui.text_label.setText(label_text)

    def _close_event(self, event):
        if event.spontaneous():
            self._clear_feedback_flag()
        self._old_close_event(event)

    def _clear_feedback_flag(self):
        if self._feedback_mode:
            logger.debug("Feedback flag cleared")
            clear_init_done()
            self._feedback_mode = False
            self._dialog.closeEvent = self._old_close_event

    def _on_exit_request(self):
        if self._is_opened:
            self.close()
Exemplo n.º 2
0
class CollaborationSettingsDialog(object):
    ADD_BUTTON_ACTIVE_COLOR = "#f78d1e"
    ADD_BUTTON_PASSIVE_COLOR = "#9a9a9a"
    ERROR_COLOR = '#FF9999'
    LINE_EDIT_NORMAL_COLOR = "#EFEFF1"

    def __init__(self, parent, parent_window, colleagues, folder, dp):
        self._dialog = QDialog(parent_window)
        self._dp = dp
        self._colleagues = colleagues
        self._parent = parent
        self._parent_window = parent_window
        self._folder = folder

        self._is_owner = False
        self._dialog.setWindowIcon(QIcon(':/images/icon.png'))
        self._ui = Ui_Dialog()
        self._ui.setupUi(self._dialog)

        self._init_ui()

    def _init_ui(self):
        self._dialog.setWindowFlags(Qt.Dialog)
        self._dialog.setAttribute(Qt.WA_TranslucentBackground)
        self._dialog.setAttribute(Qt.WA_MacFrameworkScaled)
        self._dialog.setWindowTitle(self._dialog.windowTitle() + self._folder)

        self._ui.colleagues_list.setAlternatingRowColors(True)
        self._colleagues_list = ColleaguesList(self._parent,
                                               self._ui.colleagues_list,
                                               self._dp, self._show_menu)

        self._loader_movie = QMovie(":/images/loader.gif")
        self._ui.loader_label.setMovie(self._loader_movie)
        self._set_fonts()

        self._ui.add_frame.setVisible(False)
        self._set_add_button_background(self.ADD_BUTTON_PASSIVE_COLOR)
        self._ui.add_button.clicked.connect(self._on_add_button_clicked)
        self._ui.add_button.setVisible(False)
        self._ui.close_button.clicked.connect(self._on_close_button_clicked)
        self._ui.refresh_button.clicked.connect(self._on_refresh)

        self._line_edit_style = "background-color: {};"
        self._ui.error_label.setStyleSheet("color: {};".format(
            self.ERROR_COLOR))

    def _set_fonts(self):
        ui = self._ui
        controls = [
            ui.colleagues_label, ui.mail_edit, ui.edit_radio, ui.view_radio,
            ui.add_button
        ]

        for control in controls:
            font = control.font()
            font_size = control.font().pointSize() * self._dp
            if font_size > 0:
                control_font = QFont(font.family(), font_size)
                control_font.setBold(font.bold())
                control.setFont(control_font)

    def show(self):
        logger.debug("Opening collaboration settings dialog")

        screen_width = QApplication.desktop().width()
        parent_x = self._dialog.parent().x()
        parent_width = self._dialog.parent().width()
        width = self._dialog.width()
        offset = 16
        if parent_x + parent_width / 2 > screen_width / 2:
            x = parent_x - width - offset
            if x < 0:
                x = 0
        else:
            x = parent_x + parent_width + offset
            diff = x + width - screen_width
            if diff > 0:
                x -= diff
        self._dialog.move(x, self._dialog.parent().y())

        # Execute dialog
        self._dialog.raise_()
        self.show_cursor_loading(True)
        self._dialog.exec_()

    def close(self):
        self._dialog.reject()

    def show_cursor_loading(self, show_movie=False):
        if show_movie:
            self._ui.stackedWidget.setCurrentIndex(1)
            self._loader_movie.start()
        else:
            self._dialog.setCursor(Qt.WaitCursor)
            self._parent_window.setCursor(Qt.WaitCursor)

    def show_cursor_normal(self):
        self._dialog.setCursor(Qt.ArrowCursor)
        self._parent_window.setCursor(Qt.ArrowCursor)
        if self._loader_movie.state() == QMovie.Running:
            self._loader_movie.stop()

    def show_colleagues(self):
        if not self._colleagues:
            self._ui.stackedWidget.setCurrentIndex(2)
        else:
            self._ui.stackedWidget.setCurrentIndex(0)
            self._colleagues_list.show_colleagues(self._colleagues)
        self.show_cursor_normal()

    def set_owner(self, is_owner):
        self._is_owner = is_owner
        self._ui.add_button.setVisible(self._is_owner)

    def _on_add_button_clicked(self):
        if self._ui.add_frame.isVisible():
            if not self._validate_email():
                return

            to_edit = self._ui.edit_radio.isChecked()
            self._ui.add_frame.setVisible(False)
            self._set_add_button_background(self.ADD_BUTTON_PASSIVE_COLOR)
            self._parent.add_colleague(self._ui.mail_edit.text(), to_edit)
        else:
            self._ui.add_frame.setVisible(True)
            self._set_add_button_background(self.ADD_BUTTON_ACTIVE_COLOR)
            self._ui.mail_edit.setText("")

    def _set_add_button_background(self, color):
        self._ui.add_button.setStyleSheet(
            'background-color: {}; color: #fff; '
            'border-radius: 4px; font: bold "Gargi"'.format(color))

    def _on_close_button_clicked(self):
        self._ui.add_frame.setVisible(False)
        self._set_add_button_background(self.ADD_BUTTON_PASSIVE_COLOR)
        self._clear_error()
        self._ui.mail_edit.setText("")

    def _validate_email(self):
        email_control = self._ui.mail_edit
        email_control.setStyleSheet(
            self._line_edit_style.format(self.LINE_EDIT_NORMAL_COLOR))
        regex = '^.+@.{2,}$'

        email_control.setText(email_control.text().strip())
        if not re.match(regex, email_control.text()):
            self._ui.error_label.setText(tr("Please enter a valid e-mail"))
            email_control.setStyleSheet(
                self._line_edit_style.format(self.ERROR_COLOR))
            email_control.setFocus()
            return False

        self._clear_error()
        return True

    def _clear_error(self):
        self._ui.error_label.setText("")
        self._ui.mail_edit.setStyleSheet(
            self._line_edit_style.format(self.LINE_EDIT_NORMAL_COLOR))

    def _on_refresh(self):
        self.show_cursor_loading()
        self._parent.query_collaboration_info()

    def _show_menu(self, colleague, pos):
        if not self._is_owner and not colleague.is_you or colleague.is_deleting:
            return

        menu = QMenu(self._ui.colleagues_list)
        menu.setStyleSheet("background-color: #EFEFF4; ")
        if colleague.is_you:
            if colleague.is_owner:
                action = menu.addAction(tr("Quit collaboration"))
                action.triggered.connect(self._on_quit_collaboration)
            else:
                action = menu.addAction(tr("Leave collaboration"))
                action.triggered.connect(self._on_leave_collaboration)
        else:
            rights_group = QActionGroup(menu)
            rights_group.setExclusive(True)

            menu.addSection(tr("Access rights"))
            action = menu.addAction(tr("Can view"))
            action.setCheckable(True)
            rights_action = rights_group.addAction(action)
            rights_action.setData(False)
            rights_action.setChecked(not colleague.can_edit)
            action = menu.addAction(tr("Can edit"))
            action.setCheckable(True)
            rights_action = rights_group.addAction(action)
            rights_action.setChecked(colleague.can_edit)
            rights_action.setData(True)
            rights_group.triggered.connect(
                lambda a: self._on_grant_edit(colleague, a))
            menu.addSeparator()

            action = menu.addAction(tr("Remove user"))
            action.triggered.connect(lambda: self._on_remove_user(colleague))

        pos_to_show = QPoint(pos.x(), pos.y() + 10)
        menu.exec_(pos_to_show)

    def _on_quit_collaboration(self):
        alert_str = "Collaboration will be cancelled, " \
                    "collaboration folder will be deleted " \
                    "from all colleagues' Pvtbox secured sync folders " \
                    "on all nodes."
        if self._user_confirmed_action(alert_str):
            self._parent.cancel_collaboration()

    def _on_leave_collaboration(self):
        alert_str = "Collaboration folder will be deleted " \
                    "from Pvtbox secured sync folders " \
                    "on all your nodes."
        if self._user_confirmed_action(alert_str):
            self._parent.leave_collaboration()

    def _on_remove_user(self, colleague):
        alert_str = "Colleague {} will be removed from collaboration. " \
                    "Collaboration folder will be deleted from colleague's " \
                    "Pvtbox secured sync folders on all nodes." \
            .format(colleague.email)
        if self._user_confirmed_action(alert_str):
            self._parent.remove(colleague.id)

    def _on_grant_edit(self, colleague, action):
        to_edit = action.data()
        self._parent.grant_edit(colleague.id, to_edit)

    def _user_confirmed_action(self, alert_str):
        msg = tr("<b>Are</b> you <b>sure</b>?<br><br>{}".format(alert_str))
        user_answer = msgbox(msg,
                             title=' ',
                             buttons=[
                                 (tr('Cancel'), 'Cancel'),
                                 (tr('Yes'), 'Yes'),
                             ],
                             parent=self._dialog,
                             default_index=0,
                             enable_close_button=True)

        return user_answer == 'Yes'