summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Gabriel <mike.gabriel@das-netzwerkteam.de>2016-12-13 21:34:53 +0100
committerMike Gabriel <mike.gabriel@das-netzwerkteam.de>2016-12-13 21:34:53 +0100
commit9edd45d40f48ea7cc1a5dbccd4a005e9446e516f (patch)
tree5623612225ed942802118722b599f8c176e1bce1
parenteae185f27861928b4c447cc1f858f75390070c0d (diff)
downloadtips-og-hjelp-9edd45d40f48ea7cc1a5dbccd4a005e9446e516f.tar.gz
tips-og-hjelp-9edd45d40f48ea7cc1a5dbccd4a005e9446e516f.tar.bz2
tips-og-hjelp-9edd45d40f48ea7cc1a5dbccd4a005e9446e516f.zip
rapporter-feil: Port Linnea Skogtvedt's rapporter-feil to Qt5, add i18n support.HEADmaster
-rwxr-xr-xbin/rapporter-feil452
-rw-r--r--rapporter-feil.cfg87
2 files changed, 539 insertions, 0 deletions
diff --git a/bin/rapporter-feil b/bin/rapporter-feil
new file mode 100755
index 0000000..6e710c8
--- /dev/null
+++ b/bin/rapporter-feil
@@ -0,0 +1,452 @@
+#!/usr/bin/python3
+# -*- coding: utf-8 -*-
+
+# Copyright 2009-2011 Linnea Skogtvedt <linnea@linuxavdelingen.no>
+# Copyright 2016 Mike Gabriel <mike.gabriel@das-netzwerkteam.de>
+#
+# This program 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.
+#
+# This program 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 this program; if not, write to the
+# Free Software Foundation, Inc.,
+# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
+
+import locale
+import re
+import sys
+import configparser
+from optparse import OptionParser
+
+from PyQt5.QtCore import *
+from PyQt5.QtGui import *
+from PyQt5.QtWidgets import QApplication, QDialog, QLabel, QPlainTextEdit, QDialogButtonBox, QGridLayout, QLineEdit, QCheckBox, QMessageBox
+
+class ConfigException(BaseException):
+ pass
+
+class RapporterFeilDialog(QDialog):
+ def __init__(self, title, userMsg, urlOpener, idleTimeout, customFields, onAccept, onTimeout):
+ super(RapporterFeilDialog, self).__init__(None)
+
+ def openLink(link):
+ import shlex
+ argv = shlex.split(urlOpener) + [link]
+ QProcess.startDetached(argv[0], argv[1:])
+
+ def updateTitle():
+ assert isinstance(title, str)
+ self.setWindowTitle(u'%s (%d...)' % (title, self.idleTimeout-self.idleCount))
+
+ def updateIdleCount():
+ self.idleCount += 1
+ updateTitle()
+ if self.idleCount == self.idleTimeout:
+ onTimeout()
+
+ def resetIdleCount(ignored=None):
+ """
+ Called when the user edits anything. Stops the idle timer and starts
+ a timer to restart the idle timer after idleTimeout seconds.
+ """
+ if idleTimeout == 0:
+ return
+ self.idleCount = 0
+ self.timer.stop()
+ self.resetTimer.stop()
+ self.resetTimer.start(self.idleTimeout*1000)
+ self.setWindowTitle(title)
+
+ msgLabel = QLabel(userMsg)
+ msgLabel.setTextFormat(Qt.RichText)
+ msgLabel.linkActivated.connect(openLink)
+
+ self.msgTextEdit = QPlainTextEdit()
+ self.msgTextEdit.textChanged.connect(resetIdleCount)
+
+ buttonBox = QDialogButtonBox()
+ buttonBox.addButton('Send', QDialogButtonBox.AcceptRole)
+ buttonBox.addButton(QDialogButtonBox.Cancel)
+
+ layout = QGridLayout()
+ # w, row, col, rowspan, colspan
+ layout.addWidget(msgLabel, 0, 0, 1, 2)
+
+ self.idleTimeout = idleTimeout
+ self.idleCount = 0
+ self.timer = QTimer()
+ self.timer.setInterval(1000)
+ self.timer.timeout.connect(updateIdleCount)
+ if self.idleTimeout != 0:
+ self.timer.start()
+ self.resetTimer = QTimer()
+ self.resetTimer.setSingleShot(True)
+ self.resetTimer.timeout.connect(self.timer.start)
+
+ row = 1
+ self.customFieldWidgets = [] # [ [label, widget] ]
+ for label, initValue in customFields:
+ if initValue in ('0|1', '1|0'): # checkbox
+ checkbox = QCheckBox(label)
+ checkbox.setChecked(initValue.split('|')[0] == '1')
+ checkbox.stateChanged.connect(resetIdleCount)
+ self.customFieldWidgets.append([checkbox, checkbox])
+ layout.addWidget(checkbox, row, 0, 1, 2) # span 1 row and 2 columns
+ else:
+ label, lineEdit = QLabel(label), QLineEdit(initValue)
+ lineEdit.textChanged.connect(resetIdleCount)
+ self.customFieldWidgets.append([label, lineEdit])
+ layout.addWidget(label, row, 0)
+ layout.addWidget(lineEdit, row, 1)
+ row += 1
+
+ layout.addWidget(self.msgTextEdit, row, 0, 5, 2)
+ row += 5
+ layout.addWidget(buttonBox, row, 0, 1, 2)
+ self.setLayout(layout)
+
+ buttonBox.accepted.connect(onAccept)
+ buttonBox.rejected.connect(self.reject)
+
+ self.setWindowTitle(title)
+
+ def get(self):
+ '''
+ Return tuple (msg, customFields) where
+ customFields is a list of (label, text) tuples.
+ '''
+ def custom():
+ for label, widget in self.customFieldWidgets:
+ if isinstance(widget, QCheckBox):
+ yield label.text(), int(widget.isChecked())
+ else:
+ yield label.text(), widget.text()
+ return self.msgTextEdit.toPlainText(), list(custom())
+
+ def showConfirmation(self, title, msg):
+ QMessageBox.information(self, title, msg)
+
+ def showError(self, title, msg, exc):
+ msg = '%s\n%s: %s' % (msg, type(exc).__name__, exc)
+ QMessageBox.critical(self, title, msg)
+
+class LocalConfigParser(configparser.RawConfigParser):
+ """
+ RawConfigParser subclass which expands shell $() syntax in values.
+ In addition, option names are not case sensitive.
+
+ >>> p = LocalConfigParser()
+ >>> p.add_section('test')
+ >>> p.set('test', 'Test æ', '$(echo "æ") $(echo "ø")')
+ >>> p.set('test', 'test \xff', '')
+ >>> p.items('test')
+ [(u'Test \\xe6', u'\\xe6 \\xf8'), (u'test \ufffd', u'')]
+ """
+ optionxform = str
+
+ def _getoutput(self, cmd):
+ import subprocess
+ p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
+ output = p.stdout.read().decode().rstrip()
+ p.wait()
+ return output
+
+ def _expand(self, value):
+ def replace(match):
+ return self._getoutput(match.group(1))
+ assert isinstance(value, str)
+ return re.sub('\$\((.*?)\)', replace, value)
+
+ def get(self, section, option):
+ return self._expand(configparser.RawConfigParser.get(self, section, option))
+
+ def items(self, section):
+ return [ (k, self._expand(v)) for (k, v) in configparser.RawConfigParser.items(self, section) ]
+
+def createEmail(fromAddr, toAddrs, ccAddrs, subject, msg, custom, info):
+ """
+ Create email message, returned in plain text and MIME encoded.
+
+ >>> t, m = createEmail(u'from@example.org', [u'to@example.org'], None, u'Test', u'',
+ ... [(u'a', u'\xe5')], [(u'c', u'd')])
+ >>> assert 'a: å' in t
+ >>> assert 'From: from@example.org' in m
+ """
+ assert isinstance(msg, str)
+
+ emailText = msg + '\n\n' + \
+ '\n'.join(['%s: %s' % (f, v) for (f,v) in info]) + '\n' + \
+ '\n'.join(['%s: %s' % (f, v) for (f,v) in custom])
+
+ import email.mime.text
+ emailText = emailText
+ m = email.mime.text.MIMEText(emailText, 'plain', 'utf-8')
+ m.add_header('From', fromAddr)
+ m.add_header('To', ', '.join(toAddrs))
+ if ccAddrs:
+ m.add_header('Cc', ', '.join(ccAddrs))
+ m.add_header('Subject', subject)
+ return (emailText, m.as_string())
+
+def sendEmail(fromAddr, toAddrs, smtpServer, emailMime):
+ """
+ Send the email.
+
+ >>> from minimock import Mock, restore
+ >>> import smtplib
+ >>> smtplib.SMTP = Mock('smtplib.SMTP')
+ >>> smtplib.SMTP.mock_returns = Mock('smtp_connection')
+ >>> sendEmail(u'from@example.org', [u'to@example.org'], u'smtp.example.org', 'body')
+ Called smtplib.SMTP('smtp.example.org')
+ Called smtp_connection.sendmail('from@example.org', ['to@example.org'], 'body')
+ Called smtp_connection.quit()
+ >>>
+ """
+ fromAddr = fromAddr
+ toAddrs = toAddrs
+ smtpServer = smtpServer
+
+ import smtplib
+ server = smtplib.SMTP(smtpServer)
+ server.sendmail(fromAddr, toAddrs, emailMime)
+ server.quit()
+
+def exportFields(fields):
+ """
+ Export the fields to the environment, prepending 'RF_' to the variable name
+ and encoding variable and value to utf-8. Everything not matching [A-Z] is
+ stripped from the variable name.
+
+ >>> exportFields([[u'Te09_.\xe5st', u'\xe5']])
+ >>> import os
+ >>> os.environ['RF_TEST']
+ '\\xc3\\xa5'
+ >>>
+ """
+ import os
+ import re
+ for k, v in fields:
+ k = 'RF_' + re.sub(r'[^A-Z]', '', k.upper())
+ os.environ[k] = str(v)
+
+def runCallbacks(callbacks, input):
+ """
+ Run commands, writing input to their stdin.
+
+ >>> import os
+ >>> pipe_r, pipe_w = os.pipe()
+ >>> runCallbacks(['cat >&%d' % pipe_w], 'å')
+ >>> os.read(pipe_r, 4096)
+ '\\xc3\\xa5'
+ """
+ def runCallback(cmd, input):
+ assert isinstance(cmd, str)
+ import subprocess
+ print ("CALLBACK: ", cmd, input)
+ p = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE)
+ try:
+ p.stdin.write(bytes(input, 'UTF-8'))
+ except(IOError):
+ pass
+ p.stdin.close()
+ p.wait()
+ assert isinstance(input, str)
+
+ for cmd in callbacks:
+ runCallback(cmd, input)
+
+class RapporterFeil(object):
+
+ dialogcls = RapporterFeilDialog
+
+ def __init__(self):
+ self.config_tips = LocalConfigParser()
+ self.config_rapporter = LocalConfigParser()
+ self.dialog = None
+ import sys
+ self.app = QApplication(sys.argv)
+ locale = QLocale.system().name()
+ self.qtTranslator = QTranslator()
+ if self.qtTranslator.load('qt_%s' % locale, '/usr/share/qt5/translations'):
+ self.app.installTranslator(self.qtTranslator)
+
+ def main(self):
+ parser = OptionParser(usage="usage: %prog [options]")
+ parser.add_option('-t', '--timeout', help='idle timeout in seconds', metavar='SECONDS', type='int', default=0)
+ parser.add_option('-c', '--cfg-tips', help='configuration file for user hints', metavar='FILE', type='string', default='/etc/tips.cfg')
+ parser.add_option('-r', '--cfg-rapporter', help='configuration file for error reporting', metavar='FILE', type='string', default='/etc/rapporter-feil.cfg')
+ options, args = parser.parse_args()
+ if args:
+ parser.error('no positional arguments allowed')
+
+ self.config_tips.readfp(open(options.cfg_tips, 'r'))
+ self.config_rapporter.readfp(open(options.cfg_rapporter, 'r'))
+
+ lang, encoding = locale.getlocale()
+ lang, region = lang.split("_")
+
+ title_options = [
+ 'title[{lang}_{region}]'.format(lang=lang, region=region),
+ 'title[{lang}]'.format(lang=lang),
+ 'title',
+ ]
+ for option in title_options:
+ if self.config_rapporter.has_option('rapporter-feil', option):
+ title = self.config_rapporter.get('rapporter-feil', option)
+ break
+ else:
+ raise ConfigException
+
+ msg_options = [
+ 'msg[{lang}_{region}]'.format(lang=lang, region=region),
+ 'msg[{lang}]'.format(lang=lang),
+ 'msg',
+ ]
+ for option in msg_options:
+ if self.config_tips.has_option('tips', option):
+ msg = self.config_tips.get('tips', option)
+ break
+ else:
+ raise ConfigException
+
+ custom_fields_section = 'custom_fields:{lang}_{region}'.format(lang=lang, region=region)
+
+ if not self.config_rapporter.has_section(custom_fields_section):
+ custom_fields_section = 'custom_fields:{lang}'.format(lang=lang)
+ if not self.config_rapporter.has_section(custom_fields_section):
+ custom_fields_section = 'custom_fields'
+
+ customFields = self.config_rapporter.items(custom_fields_section)
+
+ try:
+ urlOpener = self.config_tips.get('tips', 'open_urls_with')
+ except(configparser.NoOptionError):
+ urlOpener = 'xdg-open'
+ self.dialog = self.dialogcls(title, msg, urlOpener, options.timeout, customFields, self.onAccept, self.onTimeout)
+ self.dialog.show()
+ self.app.exec_()
+
+ def onAccept(self):
+
+ lang, encoding = locale.getlocale()
+ lang, region = lang.split("_")
+
+ msg, customFields = self.dialog.get()
+ if not msg:
+ return
+ exportFields(customFields)
+
+ info_section = 'info:{lang}_{region}'.format(lang=lang, region=region)
+ if not self.config_rapporter.has_section(info_section):
+ info_section = 'info:{lang}'.format(lang=lang)
+ if not self.config_rapporter.has_section(info_section):
+ info_section = 'info'
+
+ exportFields(self.config_rapporter.items(info_section))
+
+ fromAddr = self.config_rapporter.get('email', 'from_address')
+ toAddrs = self.config_rapporter.get('email', 'to_addresses').split()
+ try:
+ ccAddrs = self.config_rapporter.get('email', 'cc_addresses').split()
+ except(configparser.NoOptionError):
+ ccAddrs = []
+ try:
+ bccAddrs = self.config_rapporter.get('email', 'bcc_addresses').split()
+ except configparser.NoOptionError:
+ bccAddrs = []
+
+ title_options = [
+ 'title[{lang}_{region}]'.format(lang=lang, region=region),
+ 'title[{lang}]'.format(lang=lang),
+ 'title',
+ ]
+ for option in title_options:
+ if self.config_rapporter.has_option('rapporter-feil', option):
+ title = self.config_rapporter.get('rapporter-feil', option)
+ break
+ else:
+ raise ConfigException
+
+ errormsg_options = [
+ 'error_msg[{lang}_{region}]'.format(lang=lang, region=region),
+ 'error_msg[{lang}]'.format(lang=lang),
+ 'error_msg',
+ ]
+ for option in errormsg_options:
+ if self.config_rapporter.has_option('rapporter-feil', option):
+ error_msg = self.config_rapporter.get('rapporter-feil', option)
+ break
+ else:
+ raise ConfigException
+
+ confirmmsg_options = [
+ 'confirmation_msg[{lang}_{region}]'.format(lang=lang, region=region),
+ 'confirmation_msg[{lang}]'.format(lang=lang),
+ 'confirmation_msg',
+ ]
+ for option in confirmmsg_options:
+ if self.config_rapporter.has_option('rapporter-feil', option):
+ confirmation_msg = self.config_rapporter.get('rapporter-feil', option)
+ break
+ else:
+ raise ConfigException
+
+ subject_options = [
+ 'subject[{lang}_{region}]'.format(lang=lang, region=region),
+ 'subject[{lang}]'.format(lang=lang),
+ 'subject',
+ ]
+ for option in subject_options:
+ if self.config_rapporter.has_option('email', option):
+ subject = self.config_rapporter.get('email', option)
+ break
+ else:
+ raise ConfigException
+
+
+ smtpServer = self.config_rapporter.get('email', 'smtp_server')
+
+ print (info_section)
+ emailText, emailMime = createEmail(fromAddr, toAddrs, ccAddrs, subject,
+ msg, customFields, self.config_rapporter.items(info_section))
+
+ plain_callbacks_section = 'plain_callbacks:{lang}_{region}'.format(lang=lang, region=region)
+ if not self.config_rapporter.has_section(plain_callbacks_section):
+ plain_callbacks_section = 'plain_callbacks:{lang}'.format(lang=lang)
+ if not self.config_rapporter.has_section(plain_callbacks_section):
+ plain_callbacks_section = 'plain_callbacks'
+
+ mime_callbacks_section = 'mime_callbacks:{lang}_{region}'.format(lang=lang, region=region)
+ if not self.config_rapporter.has_section(mime_callbacks_section):
+ mime_callbacks_section = 'mime_callbacks:{lang}'.format(lang=lang)
+ if not self.config_rapporter.has_section(mime_callbacks_section):
+ mime_callbacks_section = 'mime_callbacks'
+
+ for section, data in ((plain_callbacks_section, emailText), (mime_callbacks_section, emailMime)):
+ commands = [ cmd for (ign, cmd) in self.config_rapporter.items(section)]
+ runCallbacks(commands, data)
+
+ if smtpServer:
+ try:
+ sendEmail(fromAddr, toAddrs + ccAddrs + bccAddrs, smtpServer, emailMime)
+ except Exception as e:
+ self.dialog.showError(title,
+ error_msg,
+ e)
+ return
+ self.dialog.showConfirmation(title,
+ confirmation_msg)
+ self.app.exit()
+
+ def onTimeout(self):
+ self.app.exit()
+
+if __name__ == '__main__':
+ RapporterFeil().main()
diff --git a/rapporter-feil.cfg b/rapporter-feil.cfg
new file mode 100644
index 0000000..2c1f997
--- /dev/null
+++ b/rapporter-feil.cfg
@@ -0,0 +1,87 @@
+[rapporter-feil]
+title = Report an Error
+title[de] = Fehlerbericht erstellen
+title[nb_NO] = Feilrapportering
+
+error_msg = An error occurred while sending the message.
+error_msg[nb_NO] = En feil oppsto ved sending av beskjed.
+error_msg[de] = Ein Fehler ist beim Versenden der Nachricht aufgetreten.
+
+confirmation_msg = Message sent. You will receive response to your email account.
+confirmation_msg[de] = Nachricht wurde gesendet. Rückmeldung auf Ihre Anfrage wird an Ihr e-Mail Konto gesendet.
+confirmation_msg[nb_NO] = Beskjed sendt. Videre oppfølging og svar kommer til din epostadresse.
+
+# SMTP server kan settes blank for å disable innebygd sending
+# av mail.
+[email]
+smtp_server = tjener.intern
+subject = Error report (school: $(cat /etc/skole))
+subject[de] = Fehlermeldung (Schule: $(cat /etc/skole))
+subject[nb_NO] = Feilrapport (skole: $(cat /etc/skole))
+from_address = no-reply@example.com
+to_addresses = support@your-provider.com
+#cc_addresses = ...
+#bcc_addresses = ...
+
+[custom_fields]
+Your email address (optional) = $(cat ~/.local/rf-mail 2>/dev/null)
+You are allowed to take over my desktop from remote to fix the problem = 0|1
+
+[custom_fields:de]
+Ihre E-Mailadresse (optional) = $(cat ~/.local/rf-mail 2>/dev/null)
+Ich gestatte, meine Arbeitsoverfläche fernzusteuern, um das Problem zu beheben = 0|1
+
+[custom_fields:nb_NO]
+Din epostadresse (valgfritt)= $(cat ~/.local/rf-mail 2>/dev/null)
+Dere kan ta over skrivebordet for å løse problemet = 0|1
+
+[info]
+Username = $(echo $USER)
+Full name = $(getent passwd $USER | cut -d: -f5)
+School = $(cat /etc/skole)
+Load = $(cat /proc/loadavg)
+Groups = $(groups)
+Host = $(hostname)
+
+[info:de]
+Benutzername = $(echo $USER)
+Vor- und Nachname = $(getent passwd $USER | cut -d: -f5)
+Schule = $(cat /etc/skole)
+Systemlast = $(cat /proc/loadavg)
+Gruppen = $(groups)
+Rechner = $(hostname)
+
+[info:nb_NO]
+Brukernavn = $(echo $USER)
+Navn = $(getent passwd $USER | cut -d: -f5)
+Skole = $(cat /etc/skole)
+Last = $(cat /proc/loadavg)
+Grupper = $(groups)
+Maskin = $(hostname)
+
+# Det brukeren skrev inn (pluss ekstra info) blir skrevet til stdin.
+# All values from [custom fields] and [info] get exported to the environment:
+# e.g. $RF_DINEPOSTADRESSE, $RF_BRUKERNAVN; however all characters not matching [a-zA-Z]
+# will get removed from the variable name.
+# The text before and after "=" can be any kind of text.
+
+[plain_callbacks]
+syslog = logger -t "rapporter-feil"
+
+[plain_callbacks:en]
+syslog = logger -t "rapporter-feil"
+#local_email = mail -a "From: $USER@intern" -s "Error report (school: $RF_SCHOOL)" support@intern
+save_mailaddress = echo $RF_YOUREMAILADDRESSOPTIONAL > ~/.local/rf-mail
+
+[plain_callbacks:de]
+syslog = logger -t "rapporter-feil"
+#local_email = mail -a "From: $USER@intern" -s "Fehlerbericht (Schule: $RF_SCHULE)" support@intern
+save_mailaddress = echo $RF_IHREEMAILADRESSEOPTIONAL > ~/.local/rf-mail
+
+[plain_callbacks:nb_NO]
+syslog = logger -t "rapporter-feil"
+#local_email = mail -a "From: $USER@intern" -s "Feilrapport (skole: $RF_SKOLE)" feil@intern
+save_mailaddress = echo $RF_DINEPOSTADRESSEVALGFRITT > ~/.local/rf-mail
+
+[mime_callbacks]
+#send_email = /usr/lib/sendmail feil@intern