Show a message and update notifier on reports.

Fixes #340.
Fixes #447.
See #429.
This commit is contained in:
Florian Bruhin 2015-04-10 07:52:06 +02:00
parent e294e325f0
commit f865b87a74
3 changed files with 220 additions and 2 deletions

View File

@ -0,0 +1,101 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Classes related to auto-updating and getting the latest version."""
import json
import functools
from PyQt5.QtCore import pyqtSignal, QObject, QUrl
from PyQt5.QtNetwork import (QNetworkAccessManager, QNetworkRequest,
QNetworkReply)
class PyPIVersionClient(QObject):
"""A client for the PyPI API using QNetworkAccessManager.
It gets the latest version of qutebrowser from PyPI.
Attributes:
_nam: The QNetworkAccessManager used.
Class attributes:
API_URL: The base API URL.
Signals:
success: Emitted when getting the version info succeeded.
arg: The newest version.
error: Emitted when getting the version info failed.
arg: The error message, as string.
"""
API_URL = 'https://pypi.python.org/pypi/{}/json'
success = pyqtSignal(str)
error = pyqtSignal(str)
def __init__(self, parent=None):
super().__init__(parent)
self._nam = QNetworkAccessManager(self)
def get_version(self, package='qutebrowser'):
"""Get the newest version of a given package.
Emits success/error when done.
Args:
package: The name of the package to check.
"""
url = QUrl(self.API_URL.format(package))
request = QNetworkRequest(url)
reply = self._nam.get(request)
if reply.isFinished():
self.on_reply_finished(reply)
else:
reply.finished.connect(functools.partial(
self.on_reply_finished, reply))
def on_reply_finished(self, reply):
"""When the reply finished, load and parse the json data.
Then emits error/success.
Args:
reply: The QNetworkReply which finished.
"""
if reply.error() != QNetworkReply.NoError:
self.error.emit(reply.errorString())
return
try:
data = bytes(reply.readAll()).decode('utf-8')
except UnicodeDecodeError as e:
self.error.emit("Invalid UTF-8 data received in reply: "
"{}!".format(e))
return
try:
json_data = json.loads(data)
except ValueError as e:
self.error.emit("Invalid JSON received in reply: {}!".format(e))
return
try:
self.success.emit(json_data['info']['version'])
except KeyError as e:
self.error.emit("Malformed data recieved in reply "
"({!r} not found)!".format(e))
return

View File

@ -24,6 +24,8 @@ import sys
import html
import getpass
import traceback
import distutils.version # pylint: disable=no-name-in-module,import-error
# https://bitbucket.org/logilab/pylint/issue/73/
from PyQt5.QtCore import pyqtSlot, Qt, QSize, qVersion
from PyQt5.QtWidgets import (QDialog, QLabel, QTextEdit, QPushButton,
@ -32,7 +34,7 @@ from PyQt5.QtWidgets import (QDialog, QLabel, QTextEdit, QPushButton,
import qutebrowser
from qutebrowser.utils import version, log, utils, objreg, qtutils
from qutebrowser.misc import miscwidgets
from qutebrowser.misc import miscwidgets, autoupdate, msgbox
from qutebrowser.browser.network import pastebin
from qutebrowser.config import config
@ -103,6 +105,7 @@ class _CrashDialog(QDialog):
_url: Pastebin URL QLabel.
_crash_info: A list of tuples with title and crash information.
_paste_client: A PastebinClient instance to use.
_pypi_client: A PyPIVersionClient instance to use.
_paste_text: The text to pastebin.
"""
@ -125,6 +128,7 @@ class _CrashDialog(QDialog):
self.resize(QSize(640, 600))
self._vbox = QVBoxLayout(self)
self._paste_client = pastebin.PastebinClient(self)
self._pypi_client = autoupdate.PyPIVersionClient(self)
self._init_text()
contact = QLabel("I'd like to be able to follow up with you, to keep "
@ -293,10 +297,17 @@ class _CrashDialog(QDialog):
self._btn_report.setEnabled(False)
self._btn_cancel.setEnabled(False)
self._btn_report.setText("Reporting...")
self._paste_client.success.connect(self.finish)
self._paste_client.success.connect(self.on_paste_success)
self._paste_client.error.connect(self.show_error)
self.report()
@pyqtSlot()
def on_paste_success(self):
"""Get the newest version from PyPI when the paste is done."""
self._pypi_client.success.connect(self.on_version_success)
self._pypi_client.error.connect(self.on_version_error)
self._pypi_client.get_version()
@pyqtSlot(str)
def show_error(self, text):
"""Show a paste error dialog.
@ -308,6 +319,44 @@ class _CrashDialog(QDialog):
error_dlg.finished.connect(self.finish)
error_dlg.show()
@pyqtSlot(str)
def on_version_success(self, newest):
"""Called when the version was obtained from self._pypi_client.
Args:
newest: The newest version as a string.
"""
# pylint: disable=no-member
# https://bitbucket.org/logilab/pylint/issue/73/
new_version = distutils.version.StrictVersion(newest)
cur_version = distutils.version.StrictVersion(qutebrowser.__version__)
lines = ['The report has been sent successfully. Thanks!']
if new_version > cur_version:
lines.append("<b>Note:</b> The newest available version is v{}, "
"but you're currently running v{} - please "
"update!".format(newest, qutebrowser.__version__))
text = '<br/><br/>'.join(lines)
self.hide()
msgbox.information(self, "Report successfully sent!", text,
on_finished=self.finish, plain_text=False)
@pyqtSlot(str)
def on_version_error(self, msg):
"""Called when the version was not obtained from self._pypi_client.
Args:
msg: The error message to show.
"""
lines = ['The report has been sent successfully. Thanks!']
lines.append("There was an error while getting the newest version: "
"{}. Please check for a new version on "
"<a href=http://www.qutebrowser.org/>qutebrowser.org</a> "
"by yourself.".format(msg))
text = '<br/><br/>'.join(lines)
self.hide()
msgbox.information(self, "Report successfully sent!", text,
on_finished=self.finish, plain_text=False)
@pyqtSlot()
def finish(self):
"""Save contact info and close the dialog."""

View File

@ -0,0 +1,68 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Convenience functions to show message boxes."""
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QMessageBox
def msgbox(parent, title, text, *, icon, buttons=QMessageBox.Ok,
on_finished=None, plain_text=None):
"""Display an QMessageBox with the given icon.
Args:
parent: The parent to set for the message box.
title: The title to set.
text: The text to set.
buttons: The buttons to set (QMessageBox::StandardButtons)
on_finished: A slot to connect to the 'finished' signal.
plain_text: Whether to force plain text (True) or rich text (False).
None (the default) uses Qt's auto detection.
Return:
A new QMessageBox.
"""
box = QMessageBox(parent)
box.setIcon(icon)
box.setStandardButtons(buttons)
if on_finished is not None:
box.finished.connect(on_finished)
if plain_text:
box.setTextFormat(Qt.PlainText)
elif plain_text is not None:
box.setTextFormat(Qt.RichText)
box.setWindowTitle(title)
box.setText(text)
box.show()
return box
def information(*args, **kwargs):
"""Display an information box.
Args:
*args: Passed to msgbox.
**kwargs: Passed to msgbox.
Return:
A new QMessageBox.
"""
return msgbox(*args, icon=QMessageBox.Information, **kwargs)