Finalize worklog implementation
This commit is contained in:
parent
32621bcc87
commit
2e348e68b4
@ -7,7 +7,7 @@ from collections.abc import MutableMapping
|
||||
from copy import deepcopy
|
||||
from datetime import datetime, date, timedelta
|
||||
from threading import Thread, Event
|
||||
from typing import List, Tuple, Dict
|
||||
from typing import List, Tuple, Dict, Union
|
||||
|
||||
try:
|
||||
from PySide6 import QtCore
|
||||
@ -330,27 +330,27 @@ def duration_to_str(duration: timedelta) -> str:
|
||||
class PrevNextable:
|
||||
def __init__(self, data: LogCommentsData):
|
||||
self._data = data
|
||||
self._date = date.today()
|
||||
self.date = date.today()
|
||||
self._prev = None
|
||||
self._next = None
|
||||
self._update_prev_next()
|
||||
|
||||
def _update_prev_next(self):
|
||||
self._prev, self._next = self._data.get_prev_next_avail(self._date)
|
||||
self._prev, self._next = self._data.get_prev_next_avail(self.date)
|
||||
|
||||
def prev_next_avail(self) -> Tuple[bool, bool]:
|
||||
return self._prev is not None, self._next is not None
|
||||
|
||||
def previous(self):
|
||||
self._date = self._prev
|
||||
self.date = self._prev
|
||||
self._update_prev_next()
|
||||
|
||||
def next(self):
|
||||
self._date = self._next
|
||||
self.date = self._next
|
||||
self._update_prev_next()
|
||||
|
||||
def date(self) -> str:
|
||||
return self._date.strftime("%Y-%m-%d")
|
||||
def date_str(self) -> str:
|
||||
return self.date.strftime("%Y-%m-%d")
|
||||
|
||||
|
||||
class Report(PrevNextable):
|
||||
@ -359,8 +359,8 @@ class Report(PrevNextable):
|
||||
self._not_log_len = 0
|
||||
|
||||
def report(self) -> Tuple[List[List[str]], int]:
|
||||
log = self._data.get_log(self._date)
|
||||
if self._date == date.today():
|
||||
log = self._data.get_log(self.date)
|
||||
if self.date == date.today():
|
||||
log.append((datetime.now(), "End"))
|
||||
ret = []
|
||||
for i, t in enumerate(log):
|
||||
@ -375,12 +375,12 @@ class Report(PrevNextable):
|
||||
ret.append(["", "", ""])
|
||||
ret.append(["", "Sums", ""])
|
||||
|
||||
tasks_summary, total_sum = summary(self._data, self._date)
|
||||
tasks_summary, total_sum = summary(self._data, self.date)
|
||||
for task, duration in tasks_summary.items():
|
||||
ret.append([task, "", duration_to_str(duration)])
|
||||
ret.append(["Total sum", "", duration_to_str(total_sum)])
|
||||
self._not_log_len = 3 + len(tasks_summary)
|
||||
if self._date == date.today():
|
||||
if self.date == date.today():
|
||||
self._not_log_len += 1
|
||||
editable_len = len(ret) - (4 + len(tasks_summary))
|
||||
|
||||
@ -391,10 +391,10 @@ class Report(PrevNextable):
|
||||
if not report:
|
||||
return
|
||||
report = list(map(
|
||||
lambda x: (datetime.combine(self._date, datetime.strptime(x[1], "%H:%M").time()), x[0]),
|
||||
lambda x: (datetime.combine(self.date, datetime.strptime(x[1], "%H:%M").time()), x[0]),
|
||||
report
|
||||
))
|
||||
self._data.set_log(self._date, report)
|
||||
self._data.set_log(self.date, report)
|
||||
|
||||
|
||||
class Worklog(PrevNextable):
|
||||
@ -403,22 +403,22 @@ class Worklog(PrevNextable):
|
||||
self._worklog = []
|
||||
|
||||
@property
|
||||
def worklog(self) -> List[List[str]]:
|
||||
tasks_summary, total_sum = summary(self._data, self._date)
|
||||
comments = self._data.get_comments(self._date)
|
||||
def worklog(self) -> List[List[Union[str, timedelta]]]:
|
||||
tasks_summary, total_sum = summary(self._data, self.date)
|
||||
comments = self._data.get_comments(self.date)
|
||||
self._worklog = []
|
||||
for task, duration in tasks_summary.items():
|
||||
self._worklog.append([task, comments.setdefault(task, ""), duration_to_str(duration)])
|
||||
self._worklog.append(["Total sum", "", duration_to_str(total_sum)])
|
||||
self._worklog.append([task, comments.setdefault(task, ""), duration])
|
||||
self._worklog.append(["Total sum", "", total_sum])
|
||||
return deepcopy(self._worklog)
|
||||
|
||||
@worklog.setter
|
||||
def worklog(self, worklog: List[List[str]]):
|
||||
log = self._data.get_log(self._date)
|
||||
log = self._data.get_log(self.date)
|
||||
set_comments = dict()
|
||||
for i, (task, comment, duration) in enumerate(worklog[:-1]):
|
||||
set_comments[task] = comment
|
||||
if self._worklog[i][0] != task:
|
||||
log = list(map(lambda x: (x[0], x[1].replace(self._worklog[i][0], task)), log))
|
||||
self._data.set_comments(self._date, set_comments)
|
||||
self._data.set_log(self._date, log)
|
||||
self._data.set_comments(self.date, set_comments)
|
||||
self._data.set_log(self.date, log)
|
||||
|
@ -4,6 +4,7 @@ import sys
|
||||
from functools import partial
|
||||
|
||||
from fime.config import Config
|
||||
from fime.worklog import WorklogDialog
|
||||
|
||||
try:
|
||||
from PySide6 import QtCore, QtWidgets
|
||||
@ -12,7 +13,7 @@ except ImportError:
|
||||
from PySide2 import QtCore, QtWidgets
|
||||
PYSIDE_6 = False
|
||||
|
||||
from fime.data import Tasks, Log, Data, LogCommentsData
|
||||
from fime.data import Tasks, Log, Data, LogCommentsData, Worklog
|
||||
from fime.exceptions import FimeException
|
||||
from fime.import_task import ImportTask
|
||||
from fime.report import Report
|
||||
@ -28,18 +29,19 @@ class App:
|
||||
data = Data()
|
||||
self.tasks = Tasks(data)
|
||||
lcd = LogCommentsData(data)
|
||||
self.worklogData = Worklog(lcd)
|
||||
self.log = Log(lcd)
|
||||
self._active_task = self.log.last_log() or "Nothing"
|
||||
|
||||
self.config = Config()
|
||||
if self.config.tray_theme == "light":
|
||||
config = Config()
|
||||
if config.tray_theme == "light":
|
||||
icon = get_icon("appointment-new-light")
|
||||
else:
|
||||
icon = get_icon("appointment-new")
|
||||
|
||||
self.menu = QtWidgets.QMenu(None)
|
||||
|
||||
self.import_task = ImportTask(self.config, None)
|
||||
self.import_task = ImportTask(config, None)
|
||||
self.import_task.accepted.connect(self.new_task_imported)
|
||||
|
||||
self.taskEdit = TaskEdit(None)
|
||||
@ -48,6 +50,9 @@ class App:
|
||||
self.reportDialog = Report(self.tasks, None)
|
||||
self.reportDialog.accepted.connect(self.report_done)
|
||||
|
||||
self.worklogDialog = WorklogDialog(config, None)
|
||||
self.worklogDialog.accepted.connect(self.report_done)
|
||||
|
||||
self.tray = QtWidgets.QSystemTrayIcon()
|
||||
self.tray.setIcon(icon)
|
||||
self.tray.setContextMenu(self.menu)
|
||||
@ -120,6 +125,9 @@ class App:
|
||||
report_action = self.menu.addAction("Report")
|
||||
report_action.triggered.connect(self.report)
|
||||
|
||||
worklog_action = self.menu.addAction("Worklog")
|
||||
worklog_action.triggered.connect(self.worklog)
|
||||
|
||||
self.menu.addSeparator()
|
||||
|
||||
exit_action = self.menu.addAction("Close")
|
||||
@ -146,6 +154,11 @@ class App:
|
||||
self.reportDialog.set_data(self.log.report())
|
||||
self.reportDialog.show()
|
||||
|
||||
@QtCore.Slot()
|
||||
def worklog(self):
|
||||
self.worklogDialog.set_data(self.worklogData)
|
||||
self.worklogDialog.show()
|
||||
|
||||
@QtCore.Slot()
|
||||
def new_task_slot(self):
|
||||
self.import_task.reset_task_text()
|
||||
@ -157,14 +170,21 @@ class App:
|
||||
self.taskEdit.show()
|
||||
|
||||
|
||||
def excepthook(original, e_type, e_value, tb_obj):
|
||||
if e_type is FimeException:
|
||||
QtWidgets.QMessageBox.critical(None, "Error", str(e_value), QtWidgets.QMessageBox.Ok)
|
||||
else:
|
||||
original(e_type, e_value, tb_obj)
|
||||
|
||||
|
||||
def main():
|
||||
try:
|
||||
# important for QStandardPath to be correct
|
||||
QtCore.QCoreApplication.setApplicationName("fime")
|
||||
# also catches exceptions in other threads
|
||||
original_excepthook = sys.excepthook
|
||||
sys.excepthook = partial(excepthook, original_excepthook)
|
||||
app = App()
|
||||
app.run()
|
||||
except FimeException as e:
|
||||
QtWidgets.QMessageBox.critical(None, "Error", str(e), QtWidgets.QMessageBox.Ok)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
@ -107,7 +107,7 @@ class Report(QtWidgets.QDialog):
|
||||
self._report.save(self._report_data)
|
||||
|
||||
def update_title(self):
|
||||
self.setWindowTitle(f"Report {self._report.date()}")
|
||||
self.setWindowTitle(f"Report {self._report.date_str()}")
|
||||
|
||||
def refresh_table(self):
|
||||
self._report_data, self._edit_len = self._report.report()
|
||||
|
@ -1,7 +1,7 @@
|
||||
from functools import reduce, partial
|
||||
from typing import Optional, List, Tuple
|
||||
|
||||
from fime.data import Worklog
|
||||
from fime.data import Worklog, duration_to_str
|
||||
from fime.progressindicator import ProgressIndicator
|
||||
from fime.task_completer import TaskCompleter
|
||||
from fime.util import get_icon, EditStartedDetector, Status
|
||||
@ -143,7 +143,7 @@ class WorklogDialog(QtWidgets.QDialog):
|
||||
self.upload_button = QtWidgets.QPushButton()
|
||||
self.upload_button.setText("Upload")
|
||||
self.upload_button.setIcon(get_icon("cloud-upload"))
|
||||
#self.upload_button.pressed.connect(self.del_log)
|
||||
self.upload_button.pressed.connect(self.upload)
|
||||
self.upload_button.setAutoDefault(False)
|
||||
self.upload_button.setMinimumWidth(self.upload_button.minimumSizeHint().width() * 1.33)
|
||||
self.upload_button.setEnabled(False)
|
||||
@ -172,6 +172,7 @@ class WorklogDialog(QtWidgets.QDialog):
|
||||
|
||||
def set_data(self, worklog: Worklog):
|
||||
self._worklog = worklog
|
||||
self.rest.purge_cache()
|
||||
self.update_all()
|
||||
|
||||
def update_all(self):
|
||||
@ -201,7 +202,7 @@ class WorklogDialog(QtWidgets.QDialog):
|
||||
self._worklog.worklog = self._worklog_data
|
||||
|
||||
def update_title(self):
|
||||
self.setWindowTitle(f"Worklog {self._worklog.date()}")
|
||||
self.setWindowTitle(f"Worklog {self._worklog.date_str()}")
|
||||
|
||||
def refresh_table(self):
|
||||
self._worklog_data = self._worklog.worklog
|
||||
@ -231,7 +232,7 @@ class WorklogDialog(QtWidgets.QDialog):
|
||||
if row == 0 and not self._focussed:
|
||||
text_edit.setFocus()
|
||||
self._focussed = True
|
||||
item2 = QtWidgets.QTableWidgetItem(self._worklog_data[row][2])
|
||||
item2 = QtWidgets.QTableWidgetItem(duration_to_str(self._worklog_data[row][2]))
|
||||
self.tableWidget.setItem(row, 2, item2)
|
||||
item2.setFlags(item2.flags() & QtCore.Qt.ItemIsEnabled)
|
||||
self._changing_items = False
|
||||
@ -298,6 +299,7 @@ class WorklogDialog(QtWidgets.QDialog):
|
||||
self._worklog.previous()
|
||||
self._focussed = False
|
||||
self.update_all()
|
||||
self.upload_button.setEnabled(False)
|
||||
|
||||
@QtCore.Slot()
|
||||
def next(self):
|
||||
@ -305,6 +307,14 @@ class WorklogDialog(QtWidgets.QDialog):
|
||||
self._worklog.next()
|
||||
self._focussed = False
|
||||
self.update_all()
|
||||
self.upload_button.setEnabled(False)
|
||||
|
||||
@QtCore.Slot()
|
||||
def upload(self):
|
||||
issues = list(map(lambda x: (x[0].split()[0], x[1], x[2]), self._worklog_data[:-1]))
|
||||
self.rest.upload(issues, self._worklog.date)
|
||||
self.update_statuses()
|
||||
self.update_timer.start()
|
||||
|
||||
@QtCore.Slot()
|
||||
def cell_changed(self, row, _):
|
||||
@ -313,6 +323,7 @@ class WorklogDialog(QtWidgets.QDialog):
|
||||
self._worklog_data[row][0] = self.tableWidget.item(row, 0).text()
|
||||
self.save()
|
||||
self.update_all()
|
||||
self.upload_button.setEnabled(False)
|
||||
|
||||
@QtCore.Slot()
|
||||
def text_edit_changed(self, text, row):
|
||||
|
@ -1,8 +1,10 @@
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
from concurrent.futures import Future
|
||||
from datetime import date, datetime
|
||||
from datetime import date, datetime, timedelta, time
|
||||
from functools import partial
|
||||
from textwrap import dedent
|
||||
from threading import Lock
|
||||
from typing import List, Dict, Tuple
|
||||
|
||||
@ -20,39 +22,46 @@ class WorklogRest:
|
||||
self.user_url = os.path.join(config.jira_url, "rest/api/2/myself")
|
||||
self.issue_url = os.path.join(config.jira_url, "rest/api/2/issue/{}")
|
||||
self.worklog_url = os.path.join(config.jira_url, "rest/api/2/issue/{}/worklog")
|
||||
self.worklog_update_url = os.path.join(config.jira_url, "rest/api/2/issue/{issue}/worklog/{worklog}")
|
||||
self.worklog_update_url = os.path.join(config.jira_url, "rest/api/2/issue/{issue_key}/worklog/{worklog_id}")
|
||||
self.session = FuturesSession()
|
||||
self.user = self._req_user()
|
||||
self._issue_state: Dict[str, Tuple[Status, str]] = {}
|
||||
self._user = None
|
||||
self._issue_state: Dict[str, Tuple[Status, str]] = dict()
|
||||
self._issue_state_lock = Lock()
|
||||
self._req_user()
|
||||
|
||||
def _req_user(self):
|
||||
return self.session.get(
|
||||
future = self.session.get(
|
||||
self.user_url,
|
||||
headers={
|
||||
"Authorization": f"Bearer {self.config.jira_token}",
|
||||
"Accept": "application/json",
|
||||
},
|
||||
)
|
||||
future.add_done_callback(self._resp_user)
|
||||
|
||||
def _get_user(self):
|
||||
def _resp_user(self, future):
|
||||
try:
|
||||
return self.user.result().json()["key"]
|
||||
self._user = future.result().json()["key"]
|
||||
except Exception:
|
||||
raise FimeException("Could not get user key:\n" + traceback.format_exc())
|
||||
print("Could not get user key:\n", file=sys.stderr)
|
||||
print(traceback.format_exc(), file=sys.stderr)
|
||||
|
||||
def get_issues_state(self, issue_keys: List[str]):
|
||||
ret = []
|
||||
with self._issue_state_lock:
|
||||
for issue_key in issue_keys:
|
||||
if issue_key not in self._issue_state:
|
||||
self._issue_state[issue_key] = (Status.PROGRESS, "Fetching")
|
||||
self._issue_state[issue_key] = (Status.PROGRESS, "Working")
|
||||
self._req_issue(issue_key)
|
||||
ret.append(self._issue_state[issue_key])
|
||||
return ret
|
||||
|
||||
def purge_cache(self):
|
||||
self._issue_state = dict()
|
||||
|
||||
def _req_issue(self, issue_key: str):
|
||||
future = self.session.get(self.issue_url.format(issue_key),
|
||||
future = self.session.get(
|
||||
self.issue_url.format(issue_key),
|
||||
headers={
|
||||
"Authorization": f"Bearer {self.config.jira_token}",
|
||||
"Accept": "application/json",
|
||||
@ -69,28 +78,95 @@ class WorklogRest:
|
||||
else:
|
||||
self._issue_state[issue_key] = (Status.ERROR, "Could not find specified issue")
|
||||
|
||||
def get(self, issue_key: str, date: date):
|
||||
resp = requests.get(self.issue_url.format(issue_key),
|
||||
def _upload_sanity_check(self, issue_keys: List[str]):
|
||||
if not self._user:
|
||||
raise FimeException("Could not get user key")
|
||||
for issue_key in issue_keys:
|
||||
if issue_key not in self._issue_state or self._issue_state[issue_key] is not Status.OK:
|
||||
raise FimeException(f"Issue with key {issue_key} in unexpected state")
|
||||
|
||||
def upload(self, issues: List[Tuple[str, str, timedelta]], pdate: date):
|
||||
"""@:param issues: [(
|
||||
"ISS-1234",
|
||||
"I did some stuff",
|
||||
timedelta(seconds=300),
|
||||
), ... ]
|
||||
"""
|
||||
self._upload_sanity_check(list(map(lambda x: x[0], issues)))
|
||||
for issue in issues:
|
||||
future = self.session.get(
|
||||
self.worklog_url.format(issue[0]),
|
||||
headers={
|
||||
"Authorization": f"Bearer {self.config.jira_token}",
|
||||
"Accept": "application/json",
|
||||
},
|
||||
)
|
||||
if resp.status_code != 200:
|
||||
raise RuntimeError("issue does not exist")
|
||||
resp = requests.get(self.worklog_url.format(issue_key),
|
||||
headers={
|
||||
"Authorization": f"Bearer {self.config.jira_token}",
|
||||
"Accept": "application/json",
|
||||
},
|
||||
}
|
||||
)
|
||||
future.add_done_callback(partial(self._worklog_check_resp, issue[0], issue[1], issue[2], pdate))
|
||||
self._issue_state[issue[0]] = (Status.PROGRESS, "Working")
|
||||
|
||||
def _worklog_check_resp(self, issue_key: str, comment: str, time_spent: timedelta, pdate: date, future: Future):
|
||||
resp = future.result()
|
||||
worklogs = resp.json()["worklogs"]
|
||||
found = False
|
||||
worklog_id = None
|
||||
for log in worklogs:
|
||||
if log["author"]["key"] == self.user \
|
||||
and datetime.strptime(log["started"], "%Y-%m-%dT%H:%M:%S.%f%z").date() == date:
|
||||
print(log["id"])
|
||||
print(log["comment"])
|
||||
found = True
|
||||
if log["author"]["key"] == self._user \
|
||||
and datetime.strptime(log["started"], "%Y-%m-%dT%H:%M:%S.%f%z").date() == pdate:
|
||||
worklog_id = log["id"]
|
||||
break
|
||||
print(found)
|
||||
if worklog_id:
|
||||
print(f"Found existing worklog for issue {issue_key} with id {worklog_id}")
|
||||
self._worklog_update(issue_key, worklog_id, comment, time_spent, pdate)
|
||||
else:
|
||||
print(f"Did not find existing worklog for issue {issue_key}")
|
||||
self._worklog_create(issue_key, comment, time_spent, pdate)
|
||||
|
||||
def _worklog_create(self, issue_key: str, comment: str, time_spent: timedelta, pdate: date):
|
||||
future = self.session.post(
|
||||
self.worklog_url.format(issue_key),
|
||||
headers={
|
||||
"Authorization": f"Bearer {self.config.jira_token}",
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
json={
|
||||
"started": datetime.combine(
|
||||
pdate, time(8, 0), datetime.now().astimezone().tzinfo).strftime("%Y-%m-%dT%H:%M:%S.000%z"),
|
||||
"timeSpentSeconds": time_spent.seconds,
|
||||
"comment": comment,
|
||||
}
|
||||
)
|
||||
future.add_done_callback(partial(self._worklog_resp, issue_key))
|
||||
|
||||
def _worklog_update(self, issue_key: str, worklog_id: str, comment: str, time_spent: timedelta, pdate: date):
|
||||
future = self.session.put(
|
||||
self.worklog_update_url.format(issue_key=issue_key, worklog_id=worklog_id),
|
||||
headers={
|
||||
"Authorization": f"Bearer {self.config.jira_token}",
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
json={
|
||||
"started": datetime.combine(
|
||||
pdate, time(8, 0), datetime.now().astimezone().tzinfo).strftime("%Y-%m-%dT%H:%M:%S.000%z"),
|
||||
"timeSpentSeconds": time_spent.seconds,
|
||||
"comment": comment,
|
||||
}
|
||||
)
|
||||
future.add_done_callback(partial(self._worklog_resp, issue_key))
|
||||
|
||||
def _worklog_resp(self, issue_key: str, future: Future):
|
||||
resp: requests.Response = future.result()
|
||||
with self._issue_state_lock:
|
||||
if resp.status_code == 200:
|
||||
self._issue_state[issue_key] = (Status.OK, "Successfully uploaded")
|
||||
print(f"Successfully uploaded issue {issue_key}")
|
||||
else:
|
||||
msg = dedent(f"""\
|
||||
Worklog upload failed:
|
||||
Method: {resp.request.method}
|
||||
URL: {resp.request.url}
|
||||
Response code: {resp.status_code}
|
||||
Response: {resp.text}
|
||||
""")
|
||||
self._issue_state[issue_key] = (Status.ERROR, msg)
|
||||
print(msg, file=sys.stderr)
|
||||
|
Loading…
Reference in New Issue
Block a user