Implement "New issue" dialog

This commit is contained in:
Fabian 2021-11-16 19:57:09 +01:00
parent e89fbe18d7
commit 25c768ee2e
8 changed files with 444 additions and 8 deletions

2
.gitignore vendored
View File

@ -1,4 +1,4 @@
test.py
# Created by https://www.gitignore.io/api/python,pycharm+all # Created by https://www.gitignore.io/api/python,pycharm+all
# Edit at https://www.gitignore.io/?templates=python,pycharm+all # Edit at https://www.gitignore.io/?templates=python,pycharm+all

14
Pipfile Normal file
View File

@ -0,0 +1,14 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
requests-futures = "*"
pyside2 = "*"
[dev-packages]
ipython = "*"
[requires]
python_version = "3"

201
Pipfile.lock generated Normal file
View File

@ -0,0 +1,201 @@
{
"_meta": {
"hash": {
"sha256": "3b71908cf3ecbacde9e592c855ee3b65686e920c23df69d66dd2dff2cd4afe8b"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"certifi": {
"hashes": [
"sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872",
"sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"
],
"version": "==2021.10.8"
},
"charset-normalizer": {
"hashes": [
"sha256:e019de665e2bcf9c2b64e2e5aa025fa991da8720daa3c1138cadd2fd1856aed0",
"sha256:f7af805c321bfa1ce6714c51f254e0d5bb5e5834039bc17db7ebe3a4cec9492b"
],
"markers": "python_version >= '3'",
"version": "==2.0.7"
},
"idna": {
"hashes": [
"sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff",
"sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"
],
"markers": "python_version >= '3'",
"version": "==3.3"
},
"pyside2": {
"hashes": [
"sha256:0558ced3bcd7f9da638fa8b7709dba5dae82a38728e481aac8b9058ea22fcdd9",
"sha256:081d8c8a6c65fb1392856a547814c0c014e25ac04b38b987d9a3483e879e9634",
"sha256:087a0b719bb967405ea85fd202757c761f1fc73d0e2397bc3a6a15376782ee75",
"sha256:1316aa22dd330df096daf7b0defe9c00297a66e0b4907f057aaa3e88c53d1aff",
"sha256:4f17a0161995678110447711d685fcd7b15b762810e8f00f6dc239bffb70a32e",
"sha256:976cacf01ef3b397a680f9228af7d3d6273b9254457ad4204731507c1f9e6c3c"
],
"index": "pypi",
"version": "==5.15.2"
},
"requests": {
"hashes": [
"sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24",
"sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
"version": "==2.26.0"
},
"requests-futures": {
"hashes": [
"sha256:35547502bf1958044716a03a2f47092a89efe8f9789ab0c4c528d9c9c30bc148",
"sha256:633804c773b960cef009efe2a5585483443c6eac3c39cc64beba2884013bcdd9"
],
"index": "pypi",
"version": "==1.0.0"
},
"shiboken2": {
"hashes": [
"sha256:03f41b0693b91c7f89627f1085a4ecbe8591c03f904118a034854d935e0e766c",
"sha256:14a33169cf1bd919e4c4c4408fffbcd424c919a3f702df412b8d72b694e4c1d5",
"sha256:4aee1b91e339578f9831e824ce2a1ec3ba3a463f41fda8946b4547c7eb3cba86",
"sha256:89c157a0e2271909330e1655892e7039249f7b79a64a443d52c512337065cde0",
"sha256:ae8ca41274cfa057106268b6249674ca669c5b21009ec49b16d77665ab9619ed",
"sha256:edc12a4df2b5be7ca1e762ab94e331ba9e2fbfe3932c20378d8aa3f73f90e0af"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '3.10'",
"version": "==5.15.2"
},
"urllib3": {
"hashes": [
"sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece",
"sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
"version": "==1.26.7"
}
},
"develop": {
"backcall": {
"hashes": [
"sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e",
"sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"
],
"version": "==0.2.0"
},
"decorator": {
"hashes": [
"sha256:7b12e7c3c6ab203a29e157335e9122cb03de9ab7264b137594103fd4a683b374",
"sha256:e59913af105b9860aa2c8d3272d9de5a56a4e608db9a2f167a8480b323d529a7"
],
"markers": "python_version >= '3.5'",
"version": "==5.1.0"
},
"ipython": {
"hashes": [
"sha256:4f69d7423a5a1972f6347ff233e38bbf4df6a150ef20fbb00c635442ac3060aa",
"sha256:a658beaf856ce46bc453366d5dc6b2ddc6c481efd3540cb28aa3943819caac9f"
],
"index": "pypi",
"version": "==7.29.0"
},
"jedi": {
"hashes": [
"sha256:18456d83f65f400ab0c2d3319e48520420ef43b23a086fdc05dff34132f0fb93",
"sha256:92550a404bad8afed881a137ec9a461fed49eca661414be45059329614ed0707"
],
"markers": "python_version >= '3.6'",
"version": "==0.18.0"
},
"matplotlib-inline": {
"hashes": [
"sha256:a04bfba22e0d1395479f866853ec1ee28eea1485c1d69a6faf00dc3e24ff34ee",
"sha256:aed605ba3b72462d64d475a21a9296f400a19c4f74a31b59103d2a99ffd5aa5c"
],
"markers": "python_version >= '3.5'",
"version": "==0.1.3"
},
"parso": {
"hashes": [
"sha256:12b83492c6239ce32ff5eed6d3639d6a536170723c6f3f1506869f1ace413398",
"sha256:a8c4922db71e4fdb90e0d0bc6e50f9b273d3397925e5e60a717e719201778d22"
],
"markers": "python_version >= '3.6'",
"version": "==0.8.2"
},
"pexpect": {
"hashes": [
"sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937",
"sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"
],
"markers": "sys_platform != 'win32'",
"version": "==4.8.0"
},
"pickleshare": {
"hashes": [
"sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca",
"sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"
],
"version": "==0.7.5"
},
"prompt-toolkit": {
"hashes": [
"sha256:449f333dd120bd01f5d296a8ce1452114ba3a71fae7288d2f0ae2c918764fa72",
"sha256:48d85cdca8b6c4f16480c7ce03fd193666b62b0a21667ca56b4bb5ad679d1170"
],
"markers": "python_full_version >= '3.6.2'",
"version": "==3.0.22"
},
"ptyprocess": {
"hashes": [
"sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35",
"sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"
],
"version": "==0.7.0"
},
"pygments": {
"hashes": [
"sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380",
"sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6"
],
"markers": "python_version >= '3.5'",
"version": "==2.10.0"
},
"setuptools": {
"hashes": [
"sha256:94ee891f4759150cded601a6beb6b08400413aefd0267b692f3f8c6e0bb238e7",
"sha256:fb537610c2dfe77b5896e3ee53dd53fbdd9adc48076c8f28cee3a30fb59a5038"
],
"markers": "python_version >= '3.6'",
"version": "==59.1.1"
},
"traitlets": {
"hashes": [
"sha256:059f456c5a7c1c82b98c2e8c799f39c9b8128f6d0d46941ee118daace9eb70c7",
"sha256:2d313cc50a42cd6c277e7d7dc8d4d7fedd06a2c215f78766ae7b1a66277e0033"
],
"markers": "python_version >= '3.7'",
"version": "==5.1.1"
},
"wcwidth": {
"hashes": [
"sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784",
"sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"
],
"version": "==0.2.5"
}
}
}

39
config.py Normal file
View File

@ -0,0 +1,39 @@
import os
from configparser import ConfigParser
from pathlib import Path
from PySide2 import QtCore
from exceptions import FimeFrackingException
def dequotify(string):
if string.startswith(('"', "'")) and string.endswith(('"', "'")):
return string[1:-1]
else:
return string
class Config:
def __init__(self):
self._configparser = ConfigParser()
config_dir_path = Path(QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.ConfigLocation))
config_path = config_dir_path / "fimefracking" / "fimefracking.conf"
if config_path.exists():
with open(config_path) as f:
config_text = f.read()
config_text = "[DEFAULT]\n" + config_text
self._configparser.read_string(config_text)
if (not self._configparser.has_option("DEFAULT", "jira_url") or
not self._configparser.has_option("DEFAULT", "jira_token")):
raise FimeFrackingException(f'Please add config file {config_path} '
f'with config keys "jira_url" and "jira_token" in INI style')
@property
def jira_url(self):
return dequotify(self._configparser["DEFAULT"]["jira_url"])
@property
def jira_token(self):
return dequotify(self._configparser["DEFAULT"]["jira_token"])

2
exceptions.py Normal file
View File

@ -0,0 +1,2 @@
class FimeFrackingException(Exception):
pass

30
main.py
View File

@ -5,8 +5,11 @@ import signal
from functools import partial from functools import partial
from PySide2 import QtCore, QtGui, QtWidgets from PySide2 import QtCore, QtGui, QtWidgets
from PySide2.QtWidgets import QMessageBox
from data import Tasks, Log from data import Tasks, Log
from exceptions import FimeFrackingException
from new_task import NewTask
from task_edit import TaskEdit from task_edit import TaskEdit
from report import Report from report import Report
@ -23,6 +26,15 @@ class App:
self.menu = QtWidgets.QMenu() self.menu = QtWidgets.QMenu()
self.new_task = NewTask(None)
self.new_task.accepted.connect(self.new_task_selected)
self.taskEdit = TaskEdit(None)
self.taskEdit.accepted.connect(self.tasks_edited)
self.reportDialog = Report(None)
self.reportDialog.accepted.connect(self.report_done)
self.tray = QtWidgets.QSystemTrayIcon() self.tray = QtWidgets.QSystemTrayIcon()
self.tray.setIcon(icon) self.tray.setIcon(icon)
self.tray.setContextMenu(self.menu) self.tray.setContextMenu(self.menu)
@ -30,11 +42,9 @@ class App:
self.tray.setToolTip("fimefracking") self.tray.setToolTip("fimefracking")
self.update_tray_menu() self.update_tray_menu()
self.taskEdit = TaskEdit(None) @QtCore.Slot()
self.taskEdit.accepted.connect(self.tasks_edited) def new_task_selected(self):
print(f"dialog input: {self.new_task.task_text}")
self.reportDialog = Report(None)
self.reportDialog.accepted.connect(self.report_done)
@QtCore.Slot() @QtCore.Slot()
def tasks_edited(self): def tasks_edited(self):
@ -68,6 +78,9 @@ class App:
self.menu.addSeparator() self.menu.addSeparator()
new_action = self.menu.addAction("New task")
new_action.triggered.connect(self.new_task.show)
new_action = self.menu.addAction("Edit tasks") new_action = self.menu.addAction("Edit tasks")
new_action.triggered.connect(self.edit_tasks) new_action.triggered.connect(self.edit_tasks)
@ -103,5 +116,8 @@ class App:
if __name__ == "__main__": if __name__ == "__main__":
app = App() try:
app.run() app = App()
app.run()
except FimeFrackingException as e:
QMessageBox.critical(None, "Error", str(e), QMessageBox.Ok)

39
new_task.py Normal file
View File

@ -0,0 +1,39 @@
from PySide2 import QtGui, QtWidgets
from task_completer import TaskCompleter
class NewTask(QtWidgets.QDialog):
def __init__(self, parent, *args, **kwargs):
super().__init__(parent, *args, **kwargs)
self.setWindowTitle("New Tasks")
self.line_edit = QtWidgets.QLineEdit()
completer = TaskCompleter()
self.line_edit.setCompleter(completer)
self.line_edit.textChanged.connect(completer.update_picker)
cancel_button = QtWidgets.QPushButton()
cancel_button.setText("OK")
cancel_button.setIcon(QtGui.QIcon.fromTheme("dialog-ok-apply"))
cancel_button.pressed.connect(self.accept)
ok_button = QtWidgets.QPushButton()
ok_button.setText("Cancel")
ok_button.setIcon(QtGui.QIcon.fromTheme("dialog-cancel"))
ok_button.pressed.connect(self.reject)
blayout = QtWidgets.QHBoxLayout()
blayout.addSpacing(300)
blayout.addWidget(ok_button)
blayout.addWidget(cancel_button)
layout = QtWidgets.QVBoxLayout()
layout.addWidget(self.line_edit)
layout.addLayout(blayout)
self.setLayout(layout)
self.resize(500, 0)
@property
def task_text(self):
return self.line_edit.text()

125
task_completer.py Normal file
View File

@ -0,0 +1,125 @@
import os
import sys
import traceback
from functools import reduce
from queue import Queue, Empty
from urllib.parse import urlparse, parse_qs
from PySide2 import QtCore
from PySide2.QtCore import QTimer
from PySide2.QtWidgets import QCompleter
from requests_futures.sessions import FuturesSession
from config import Config
class TaskCompleter(QCompleter):
def __init__(self, parent=None, *args, **kwargs):
super().__init__([], parent, *args, **kwargs)
self.setFilterMode(QtCore.Qt.MatchFlag.MatchContains)
self.setCaseSensitivity(QtCore.Qt.CaseInsensitive)
self.session = FuturesSession()
self.config = Config()
self.picker_url = os.path.join(self.config.jira_url, "rest/api/2/issue/picker")
self.search_url = os.path.join(self.config.jira_url, "rest/api/2/search")
self.text = ""
self.response_text = ""
self.model_string_list = []
self.escalate = False
self.update_timer = QTimer(self)
self.update_timer.timeout.connect(self.process_response)
self.update_timer.setInterval(250)
self.queue = Queue()
@QtCore.Slot()
def process_response(self):
try:
while not self.queue.empty():
result_dict = self.queue.get_nowait()
if result_dict["response_text"] == self.text:
if self.text == self.response_text:
self.response_text = result_dict["response_text"]
self.model_string_list = result_dict["result"]
else:
self.model_string_list += result_dict["result"]
self.model().setStringList(self.model_string_list)
self.complete()
except Empty:
return
@QtCore.Slot(str)
def update_picker(self, text):
self.text = text
if self.text == self.currentCompletion():
# do not update, after auto completion was used
return
if self.escalate:
self.update_search()
if not self.update_timer.isActive():
self.update_timer.start()
future = self.session.get(
url=self.picker_url,
params={
"query": self.text
},
headers={
"Authorization": f"Bearer {self.config.jira_token}",
"Accept": "application/json",
},
)
future.add_done_callback(self.picker_response_callback)
def picker_response_callback(self, future):
try:
result = future.result()
parsed = urlparse(result.request.url)
response_text = parse_qs(parsed.query)["query"][0]
issues = reduce(lambda x, y: x + (y["issues"]), result.json()["sections"], [])
extracted = list(map(lambda x: f"{x['key']} {x['summaryText']}", issues))
if extracted:
self.queue.put({
"response_text": response_text,
"result": extracted,
})
else:
if not self.escalate:
print("No picker results. Escalating")
self.escalate = True
self.update_search()
except Exception:
print("Ignoring exception, as it only breaks autocompletion:", file=sys.stderr)
print(traceback.format_exc(), file=sys.stderr)
return
def update_search(self):
for jql in [f"text = {self.text}", f"key = {self.text}"]:
future = self.session.get(
url=self.search_url,
params={
"jql": jql,
"maxResults": 10,
"fields": "key,summary",
},
headers={
"Authorization": f"Bearer {self.config.jira_token}",
"Accept": "application/json",
},
)
future.add_done_callback(self.search_response_callback)
def search_response_callback(self, future):
try:
result = future.result()
json_result = result.json()
parsed = urlparse(result.request.url)
response_text = parse_qs(parsed.query)["jql"][0].split()[2]
if "issues" in json_result:
extracted = list(map(lambda x: f"{x['key']} {x['fields']['summary']}", json_result["issues"]))
self.queue.put({
"response_text": response_text,
"result": extracted,
})
except Exception:
print("Ignoring exception, as it only breaks autocompletion:", file=sys.stderr)
print(traceback.format_exc(), file=sys.stderr)
return