Compare commits

..

No commits in common. "aebdf2d7c9f157b9c6050aaa2bc6c49b0b9de863" and "32621bcc87ea665e1ab9972f1f7aa901c3770b32" have entirely different histories.

5 changed files with 107 additions and 191 deletions

View File

@ -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, Union
from typing import List, Tuple, Dict
try:
from PySide6 import QtCore
@ -290,9 +290,20 @@ class Log:
return None
return log[-1][1]
def report(self):
return Report(self._data)
def worklog(self):
return Worklog(self._data)
# TODO remove
dEV = False
def summary(lcd: LogCommentsData, pdate: date) -> Tuple[Dict[str, timedelta], timedelta]:
log = lcd.get_log(pdate)
if dEV:
if pdate == date.today():
log.append((datetime.now(), "End"))
tasks_sums = {}
@ -319,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_str(self) -> str:
return self.date.strftime("%Y-%m-%d")
def date(self) -> str:
return self._date.strftime("%Y-%m-%d")
class Report(PrevNextable):
@ -348,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):
@ -364,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))
@ -380,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):
@ -392,22 +403,22 @@ class Worklog(PrevNextable):
self._worklog = []
@property
def worklog(self) -> List[List[Union[str, timedelta]]]:
tasks_summary, total_sum = summary(self._data, self.date)
comments = self._data.get_comments(self.date)
def worklog(self) -> List[List[str]]:
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])
self._worklog.append(["Total sum", "", total_sum])
self._worklog.append([task, comments.setdefault(task, ""), duration_to_str(duration)])
self._worklog.append(["Total sum", "", duration_to_str(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)

View File

@ -4,7 +4,6 @@ import sys
from functools import partial
from fime.config import Config
from fime.worklog import WorklogDialog
try:
from PySide6 import QtCore, QtWidgets
@ -13,10 +12,10 @@ except ImportError:
from PySide2 import QtCore, QtWidgets
PYSIDE_6 = False
from fime.data import Tasks, Log, Data, LogCommentsData, Worklog, Report
from fime.data import Tasks, Log, Data, LogCommentsData
from fime.exceptions import FimeException
from fime.import_task import ImportTask
from fime.report import ReportDialog
from fime.report import Report
from fime.task_edit import TaskEdit
from fime.util import get_screen_height, get_icon
@ -32,25 +31,22 @@ class App:
self.log = Log(lcd)
self._active_task = self.log.last_log() or "Nothing"
config = Config()
if config.tray_theme == "light":
self.config = Config()
if self.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(config, None)
self.import_task = ImportTask(self.config, None)
self.import_task.accepted.connect(self.new_task_imported)
self.taskEdit = TaskEdit(None)
self.taskEdit.accepted.connect(self.tasks_edited)
self.reportDialog = ReportDialog(self.tasks, Report(lcd), None)
self.reportDialog.accepted.connect(self.log_edited)
self.worklogDialog = WorklogDialog(config, Worklog(lcd), None)
self.worklogDialog.accepted.connect(self.log_edited)
self.reportDialog = Report(self.tasks, None)
self.reportDialog.accepted.connect(self.report_done)
self.tray = QtWidgets.QSystemTrayIcon()
self.tray.setIcon(icon)
@ -71,7 +67,7 @@ class App:
self.update_tray_menu()
@QtCore.Slot()
def log_edited(self):
def report_done(self):
self.active_task = self.log.last_log() or "Nothing"
@property
@ -122,10 +118,7 @@ class App:
edit_action.triggered.connect(self.edit_tasks)
report_action = self.menu.addAction("Report")
report_action.triggered.connect(self.reportDialog.show)
worklog_action = self.menu.addAction("Worklog")
worklog_action.triggered.connect(self.worklogDialog.show)
report_action.triggered.connect(self.report)
self.menu.addSeparator()
@ -148,6 +141,11 @@ class App:
else:
self.app.exec_()
@QtCore.Slot()
def report(self):
self.reportDialog.set_data(self.log.report())
self.reportDialog.show()
@QtCore.Slot()
def new_task_slot(self):
self.import_task.reset_task_text()
@ -159,21 +157,14 @@ 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__":

View File

@ -11,7 +11,7 @@ from fime.data import Tasks, Report
from fime.util import get_screen_height, get_icon, EditStartedDetector
class ReportDialog(QtWidgets.QDialog):
class Report(QtWidgets.QDialog):
class TaskItemCompleter(EditStartedDetector):
def __init__(self, tasks: Tasks, parent=None):
super().__init__(parent)
@ -25,9 +25,9 @@ class ReportDialog(QtWidgets.QDialog):
editor.setCompleter(completer)
return editor
def __init__(self, tasks: Tasks, report: Report, parent, *args, **kwargs):
def __init__(self, tasks: Tasks, parent, *args, **kwargs):
super().__init__(parent, *args, **kwargs)
self._report = report
self._report: Optional[Report] = None
self._report_data: Optional[List[List[str]]] = None
self._changing_items = False
self._new_log_task = ""
@ -41,7 +41,7 @@ class ReportDialog(QtWidgets.QDialog):
self.tableWidget.setColumnCount(3)
self.tableWidget.setHorizontalHeaderLabels(["Task", "Start time", "Duration"])
self.tableWidget.cellChanged.connect(self.cell_changed)
taskItemCompleter = ReportDialog.TaskItemCompleter(tasks, self)
taskItemCompleter = Report.TaskItemCompleter(tasks, self)
taskItemCompleter.editStarted.connect(self.disable_buttons)
taskItemCompleter.editFinished.connect(self.enable_buttons)
self.tableWidget.setItemDelegateForColumn(0, taskItemCompleter)
@ -97,7 +97,8 @@ class ReportDialog(QtWidgets.QDialog):
layout.addLayout(blayout)
self.setLayout(layout)
def showEvent(self, _):
def set_data(self, data: Report):
self._report = data
self.update_title()
self.refresh_table()
self.update_prev_next()
@ -106,7 +107,7 @@ class ReportDialog(QtWidgets.QDialog):
self._report.save(self._report_data)
def update_title(self):
self.setWindowTitle(f"Report {self._report.date_str()}")
self.setWindowTitle(f"Report {self._report.date()}")
def refresh_table(self):
self._report_data, self._edit_len = self._report.report()

View File

@ -1,8 +1,7 @@
from functools import reduce, partial
from typing import Optional, List, Tuple
from fime.config import Config
from fime.data import Worklog, duration_to_str
from fime.data import Worklog
from fime.progressindicator import ProgressIndicator
from fime.task_completer import TaskCompleter
from fime.util import get_icon, EditStartedDetector, Status
@ -93,13 +92,13 @@ class WorklogDialog(QtWidgets.QDialog):
if not self.return_:
self.edit_finished_row.emit(row)
def __init__(self, config: Config, worklog: Worklog, parent, *args, **kwargs):
def __init__(self, config, parent, *args, **kwargs):
super().__init__(parent, *args, **kwargs)
self.rest = WorklogRest(config)
self._changing_items = False
self._worklog = worklog
self._worklog: Optional[Worklog] = None
self._worklog_data: List[List[str]] = []
self._statuses: List[Tuple[Status, str]] = []
self.row_height = None
@ -144,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.upload)
#self.upload_button.pressed.connect(self.del_log)
self.upload_button.setAutoDefault(False)
self.upload_button.setMinimumWidth(self.upload_button.minimumSizeHint().width() * 1.33)
self.upload_button.setEnabled(False)
@ -171,8 +170,8 @@ class WorklogDialog(QtWidgets.QDialog):
self.update_timer.setInterval(500)
self.update_timer.timeout.connect(self.update_statuses)
def showEvent(self, _):
self.rest.purge_cache()
def set_data(self, worklog: Worklog):
self._worklog = worklog
self.update_all()
def update_all(self):
@ -202,7 +201,7 @@ class WorklogDialog(QtWidgets.QDialog):
self._worklog.worklog = self._worklog_data
def update_title(self):
self.setWindowTitle(f"Worklog {self._worklog.date_str()}")
self.setWindowTitle(f"Worklog {self._worklog.date()}")
def refresh_table(self):
self._worklog_data = self._worklog.worklog
@ -232,7 +231,7 @@ class WorklogDialog(QtWidgets.QDialog):
if row == 0 and not self._focussed:
text_edit.setFocus()
self._focussed = True
item2 = QtWidgets.QTableWidgetItem(duration_to_str(self._worklog_data[row][2]))
item2 = QtWidgets.QTableWidgetItem(self._worklog_data[row][2])
self.tableWidget.setItem(row, 2, item2)
item2.setFlags(item2.flags() & QtCore.Qt.ItemIsEnabled)
self._changing_items = False
@ -299,7 +298,6 @@ class WorklogDialog(QtWidgets.QDialog):
self._worklog.previous()
self._focussed = False
self.update_all()
self.upload_button.setEnabled(False)
@QtCore.Slot()
def next(self):
@ -307,14 +305,6 @@ 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, _):
@ -323,7 +313,6 @@ 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):

View File

@ -1,10 +1,8 @@
import os
import sys
import traceback
from concurrent.futures import Future
from datetime import date, datetime, timedelta, time
from datetime import date, datetime
from functools import partial
from textwrap import dedent
from threading import Lock
from typing import List, Dict, Tuple
@ -22,46 +20,39 @@ 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_key}/worklog/{worklog_id}")
self.worklog_update_url = os.path.join(config.jira_url, "rest/api/2/issue/{issue}/worklog/{worklog}")
self.session = FuturesSession()
self._user = None
self._issue_state: Dict[str, Tuple[Status, str]] = dict()
self.user = self._req_user()
self._issue_state: Dict[str, Tuple[Status, str]] = {}
self._issue_state_lock = Lock()
self._req_user()
def _req_user(self):
future = self.session.get(
return 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 _resp_user(self, future):
def _get_user(self):
try:
self._user = future.result().json()["key"]
return self.user.result().json()["key"]
except Exception:
print("Could not get user key:\n", file=sys.stderr)
print(traceback.format_exc(), file=sys.stderr)
raise FimeException("Could not get user key:\n" + traceback.format_exc())
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, "Working")
self._issue_state[issue_key] = (Status.PROGRESS, "Fetching")
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",
@ -78,95 +69,28 @@ class WorklogRest:
else:
self._issue_state[issue_key] = (Status.ERROR, "Could not find specified issue")
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]),
def get(self, issue_key: str, date: date):
resp = requests.get(self.issue_url.format(issue_key),
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"]
worklog_id = None
found = False
for log in worklogs:
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"]
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
break
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)
print(found)