This commit is contained in:
Fabian 2021-11-28 17:22:39 +01:00
parent e9fb29aa1b
commit 210aaba92b
4 changed files with 215 additions and 80 deletions

View File

@ -2,7 +2,9 @@ import atexit
import base64
import json
import os
import time
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
@ -193,10 +195,11 @@ class LogCommentsData:
month_str = pdate.strftime("%Y-%m")
day_str = pdate.strftime("%d")
month = self._data.setdefault(month_str, {})
if month.setdefault(day_str, self._init_day)["log"] == encoded:
return # no changes
month.setdefault(day_str, self._init_day)["log"] = encoded
# trigger save if necessary
if self._data[month_str] != month:
self._data[month_str] = month
# trigger save
self._data[month_str] = month
def add_log_entry(self, task: str):
now = datetime.now()
@ -241,7 +244,7 @@ class LogCommentsData:
return dict()
comment_data = self._data[month_str][day_str]["comments"]
ret = dict()
for k, v in comment_data:
for k, v in comment_data.items():
k_dec = base64.b64decode(k.encode("utf-8")).decode("utf-8")
v_dec = base64.b64decode(v.encode("utf-8")).decode("utf-8")
ret[k_dec] = v_dec
@ -250,17 +253,18 @@ class LogCommentsData:
def set_comments(self, pdate: date, comments: Dict[str, str]):
self._ensure_format(pdate)
encoded = dict()
for k, v in comments:
for k, v in comments.items():
k_enc = base64.b64encode(k.encode("utf-8")).decode("utf-8")
v_enc = base64.b64encode(v.encode("utf-8")).decode("utf-8")
encoded[k_enc] = v_enc
month_str = pdate.strftime("%Y-%m")
day_str = pdate.strftime("%d")
month = self._data.setdefault(month_str, {})
if month.setdefault(day_str, self._init_day)["comments"] == encoded:
return # no changes
month.setdefault(day_str, self._init_day)["comments"] = encoded
# trigger save if necessary
if self._data[month_str] != month:
self._data[month_str] = month
# trigger save
self._data[month_str] = month
class Log:
@ -293,10 +297,15 @@ class Log:
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 pdate == date.today():
log.append((datetime.now(), "End"))
if dEV:
if pdate == date.today():
log.append((datetime.now(), "End"))
tasks_sums = {}
total_sum = timedelta()
for i, le in enumerate(log):
@ -318,15 +327,37 @@ def duration_to_str(duration: timedelta) -> str:
return f"{dhours:02d}:{dmins:02d}"
class Report:
class PrevNextable:
def __init__(self, data: LogCommentsData):
self._data = data
self._date = date.today()
self._not_log_len = 0
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)
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._update_prev_next()
def next(self):
self._date = self._next
self._update_prev_next()
def date(self) -> str:
return self._date.strftime("%Y-%m-%d")
class Report(PrevNextable):
def __init__(self, data: LogCommentsData):
super().__init__(data)
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():
@ -365,24 +396,29 @@ class Report:
))
self._data.set_log(self._date, report)
def _update_prev_next(self):
self._prev, self._next = self._data.get_prev_next_avail(self._date)
def prev_next_avail(self):
return self._prev is not None, self._next is not None
def previous(self):
self._date = self._prev
self._update_prev_next()
def next(self):
self._date = self._next
self._update_prev_next()
def date(self):
return self._date.strftime("%Y-%m-%d")
class Worklog:
class Worklog(PrevNextable):
def __init__(self, data: LogCommentsData):
self._data = data
super().__init__(data)
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)
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)])
return deepcopy(self._worklog)
@worklog.setter
def worklog(self, worklog: List[List[str]]):
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)

View File

@ -1,3 +1,5 @@
from typing import List, Optional
try:
from PySide6 import QtCore, QtGui, QtWidgets
except ImportError:
@ -5,7 +7,7 @@ except ImportError:
from datetime import datetime
from fime.data import Tasks
from fime.data import Tasks, Report
from fime.util import get_screen_height, get_icon, EditStartedDetector
@ -25,8 +27,8 @@ class Report(QtWidgets.QDialog):
def __init__(self, tasks: Tasks, parent, *args, **kwargs):
super().__init__(parent, *args, **kwargs)
self._report = None
self._report_data = None
self._report: Optional[Report] = None
self._report_data: Optional[List[List[str]]] = None
self._changing_items = False
self._new_log_task = ""
self._new_log_pos = -1
@ -81,7 +83,7 @@ class Report(QtWidgets.QDialog):
self.ok_button.setText("OK")
self.ok_button.setIcon(get_icon("dialog-ok"))
self.ok_button.pressed.connect(self._accept)
self.ok_button.setAutoDefault(True)
self.ok_button.setAutoDefault(False)
blayout = QtWidgets.QHBoxLayout()
blayout.addWidget(self.previous_button)
@ -95,7 +97,7 @@ class Report(QtWidgets.QDialog):
layout.addLayout(blayout)
self.setLayout(layout)
def set_data(self, data):
def set_data(self, data: Report):
self._report = data
self.update_title()
self.refresh_table()
@ -137,9 +139,9 @@ class Report(QtWidgets.QDialog):
)
def update_prev_next(self):
prev, _next = self._report.prev_next_avail()
prev, next_ = self._report.prev_next_avail()
self.previous_button.setEnabled(prev)
self.next_button.setEnabled(_next)
self.next_button.setEnabled(next_)
@QtCore.Slot()
def del_log(self):

View File

@ -35,6 +35,5 @@ class EditStartedDetector(QtWidgets.QStyledItemDelegate):
def createEditor(self, parent, option, index):
editor = super().createEditor(parent, option, index)
editor.editingFinished.connect(self.editFinished)
editor.selectionChanged.connect(self.editFinished)
self.editStarted.emit()
return editor

View File

@ -1,6 +1,9 @@
import enum
from functools import reduce
from datetime import datetime
from functools import reduce, partial
from typing import Optional, List, Tuple
from fime.data import Worklog
from fime.progressindicator import ProgressIndicator
from fime.task_completer import TaskCompleter
from fime.util import get_icon, get_screen_height, EditStartedDetector
@ -15,20 +18,20 @@ from fime.util import get_screen_width
import fime.icons
class Worklog(QtWidgets.QDialog):
class WorklogDialog(QtWidgets.QDialog):
class TabTable(QtWidgets.QTableWidget):
def __init__(self, parent, *args, **kwargs):
super().__init__(parent, *args, **kwargs)
def focusNextPrevChild(self, next):
def focusNextPrevChild(self, next_):
if self.currentColumn() == 1:
event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
QtCore.Qt.Key_Down if next else QtCore.Qt.Key_Up,
QtCore.Qt.Key_Down if next_ else QtCore.Qt.Key_Up,
QtCore.Qt.NoModifier)
self.keyPressEvent(event)
if event.isAccepted():
return True
return super().focusNextPrevChild(next)
return super().focusNextPrevChild(next_)
class ClickWidget(QtWidgets.QWidget):
def __init__(self, item, table, parent, *args, **kwargs):
@ -64,18 +67,32 @@ class Worklog(QtWidgets.QDialog):
self.text_changed.emit(self.document().toPlainText(), self.row)
class TaskItemCompleter(EditStartedDetector):
edit_finished_row = QtCore.Signal(int)
def __init__(self, config, parent=None):
super().__init__(parent)
self.config = config
#self.row = -1
def createEditor(self, parent, option, index):
editor = super().createEditor(parent, option, index)
#self.row = index.row()
#editor.editingFinished.connect(partial(self.edit_finished_row_target, index.row()))
#editor.editingFinished.connect(self.edit_finished_row_target)
#self.editFinished.connect(self.edit_finished_row_target)
#self.editFinished.connect(partial(self.edit_finished_row_target, index.row()))
completer = TaskCompleter(self.config)
completer.setFilterMode(QtCore.Qt.MatchFlag.MatchContains)
completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive)
editor.setCompleter(completer)
return editor
@QtCore.Slot()
def edit_finished_row_target(self, row):
#def edit_finished_row_target(self):
self.edit_finished_row.emit(row)
#self.edit_finished_row.emit(self.row)
class Status(enum.Enum):
PROGRESS = enum.auto()
OK = enum.auto()
@ -87,23 +104,27 @@ class Worklog(QtWidgets.QDialog):
self.setWindowTitle("Worklog")
self._changing_items = False
self._report_data = []
self._status = []
self._worklog: Optional[Worklog] = None
self._worklog_data: List[List[str]] = []
self._status: List[Tuple[WorklogDialog.Status, str]] = []
self.row_height = None
self.focussed = False
self._focussed = False
self.tableWidget = Worklog.TabTable(self)
#self.tableWidget = WorklogDialog.TabTable(self)
self.tableWidget = QtWidgets.QTableWidget(self)
self.tableWidget.setColumnCount(3)
self.tableWidget.setHorizontalHeaderLabels(["Task", "Comment", "Duration"])
self.tableWidget.cellChanged.connect(self.cell_changed)
taskItemCompleter = Worklog.TaskItemCompleter(config, self)
taskItemCompleter = WorklogDialog.TaskItemCompleter(config, self)
taskItemCompleter.editStarted.connect(self.disable_buttons)
taskItemCompleter.editFinished.connect(self.enable_ok)
taskItemCompleter.editFinished.connect(self.enable_buttons)
#taskItemCompleter.editFinished.connect(self.ensure_stati)
#taskItemCompleter.edit_finished_row.connect(self.update_status_view)
self.tableWidget.setItemDelegateForColumn(0, taskItemCompleter)
self.hheader = QtWidgets.QHeaderView(QtCore.Qt.Orientation.Horizontal)
self.hheader.setMinimumSectionSize(10)
self.tableWidget.setHorizontalHeader(self.hheader)
self.hheader.setSectionResizeMode(self.hheader.logicalIndex(0), QtWidgets.QHeaderView.Stretch)
self.hheader.setSectionResizeMode(self.hheader.logicalIndex(0), QtWidgets.QHeaderView.Fixed)
self.hheader.setSectionResizeMode(self.hheader.logicalIndex(1), QtWidgets.QHeaderView.Fixed)
self.hheader.resizeSection(self.hheader.logicalIndex(1), get_screen_width(self) * 0.2)
self.hheader.setSectionResizeMode(self.hheader.logicalIndex(2), QtWidgets.QHeaderView.ResizeToContents)
@ -113,6 +134,18 @@ class Worklog(QtWidgets.QDialog):
self.tableWidget.setVerticalHeader(self.vheader)
self.tableWidget.setMaximumWidth(get_screen_width(self) * 0.8)
self.previous_button = QtWidgets.QPushButton()
self.previous_button.setText("Previous")
self.previous_button.setIcon(get_icon("arrow-left"))
self.previous_button.pressed.connect(self.previous)
self.previous_button.setAutoDefault(False)
self.next_button = QtWidgets.QPushButton()
self.next_button.setText("Next")
self.next_button.setIcon(get_icon("arrow-right"))
self.next_button.pressed.connect(self.next)
self.next_button.setAutoDefault(False)
self.upload_button = QtWidgets.QPushButton()
self.upload_button.setText("Upload")
self.upload_button.setIcon(get_icon("cloud-upload"))
@ -124,10 +157,12 @@ class Worklog(QtWidgets.QDialog):
self.ok_button = QtWidgets.QPushButton()
self.ok_button.setText("OK")
self.ok_button.setIcon(get_icon("dialog-ok"))
#self.ok_button.pressed.connect(self._accept)
self.ok_button.setAutoDefault(True)
self.ok_button.pressed.connect(self._accept)
self.ok_button.setAutoDefault(False)
alayout = QtWidgets.QHBoxLayout()
alayout.addWidget(self.previous_button)
alayout.addWidget(self.next_button)
alayout.addSpacerItem(QtWidgets.QSpacerItem(1, 1, QtWidgets.QSizePolicy.Expanding))
alayout.addWidget(self.upload_button)
alayout.addWidget(self.ok_button)
@ -137,64 +172,93 @@ class Worklog(QtWidgets.QDialog):
layout.addLayout(alayout)
self.setLayout(layout)
def set_data(self, data):
self._report_data = data
for i, _ in enumerate(self._report_data):
self._status.append((Worklog.Status.PROGRESS, "Fetching"))
def set_data(self, worklog: Worklog):
self._worklog = worklog
self.refresh_table()
self.update_title()
self.update_prev_next()
def update_status(self, row, new_status, status_text="Could not find specified issue"):
self._status[row] = (new_status, status_text)
self.update_status_view(row)
all_ok = reduce(lambda acc, it: acc and it[0] is Worklog.Status.OK, self._status, True)
all_ok = reduce(lambda acc, it: acc and it[0] is WorklogDialog.Status.OK, self._status, True)
if all_ok:
self.upload_button.setEnabled(True)
def save(self):
self._worklog.worklog = self._worklog_data
def update_title(self):
self.setWindowTitle(f"Worklog {self._worklog.date()}")
def refresh_table(self):
self._worklog_data = self._worklog.worklog
for i, _ in enumerate(self._worklog_data[:-1]):
self._status.append((WorklogDialog.Status.PROGRESS, "Fetching"))
self._changing_items = True
if not self.row_height:
self.tableWidget.setRowCount(1)
item = QtWidgets.QTableWidgetItem("test")
self.tableWidget.setItem(0, 0, item)
self.row_height = self.tableWidget.rowHeight(0)
self.tableWidget.setRowCount(len(self._report_data))
self.tableWidget.setRowCount(len(self._worklog_data))
for row, _ in enumerate(self._report_data):
item0 = QtWidgets.QTableWidgetItem(self._report_data[row][0])
for row, _ in enumerate(self._worklog_data):
item0 = QtWidgets.QTableWidgetItem(self._worklog_data[row][0])
self.tableWidget.setItem(row, 0, item0)
self.update_status_view(row)
text_edit = Worklog.TableTextEdit(self._report_data[row][1], row, self)
text_edit.textChanged.connect(self.on_resize)
text_edit.setFrameStyle(QtWidgets.QFrame.NoFrame)
text_edit.text_changed.connect(self.text_edit_changed)
self.tableWidget.setCellWidget(row, 1, text_edit)
if row == 0 and not self.focussed:
text_edit.setFocus()
self.focussed = True
item2 = QtWidgets.QTableWidgetItem(self._report_data[row][2])
if row == len(self._worklog_data) - 1:
self.tableWidget.removeCellWidget(row, 0)
item0.setFlags(item0.flags() & QtCore.Qt.ItemIsEnabled)
item1 = QtWidgets.QTableWidgetItem("")
self.tableWidget.setItem(row, 1, item1)
item1.setFlags(item1.flags() & QtCore.Qt.ItemIsEnabled)
else:
self.update_status_view(row)
text_edit = WorklogDialog.TableTextEdit(self._worklog_data[row][1], row, self)
text_edit.textChanged.connect(self.on_resize)
text_edit.setFrameStyle(QtWidgets.QFrame.NoFrame)
text_edit.text_changed.connect(self.text_edit_changed)
self.tableWidget.setCellWidget(row, 1, text_edit)
if row == 0 and not self._focussed:
text_edit.setFocus()
self._focussed = True
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
self.tableWidget.resizeColumnToContents(0)
desired_width = self.tableWidget.columnWidth(0) + self.tableWidget.columnWidth(1) + self.tableWidget.columnWidth(2)
self.tableWidget.setColumnWidth(0, self.tableWidget.columnWidth(0) + self.row_height)
desired_width = self.tableWidget.columnWidth(0) + self.tableWidget.columnWidth(1) + self.tableWidget.columnWidth(2) + 4
self.tableWidget.setMinimumWidth(min(desired_width, self.tableWidget.maximumWidth()))
self.adjustSize()
screen = QtGui.QGuiApplication.screenAt(self.pos())
x = ((screen.size().width() - self.tableWidget.width()) // 2) + screen.geometry().left()
y = ((screen.size().height() - self.height()) // 2) + screen.geometry().top()
self.move(x, y)
@QtCore.Slot()
def ensure_stati(self):
print("ensure_stati")
for i in range(len(self._worklog_data)-1):
cw = self.tableWidget.cellWidget(i, 0)
if type(cw) is not WorklogDialog.ClickWidget:
print(f"ensure_stati: {self.tableWidget.item(i, 0).text()}")
self.update_status_view(i)
#print(cw)
def update_status_view(self, row):
if self._status[row][0] is Worklog.Status.PROGRESS:
print(self.tableWidget.item(row, 0).text())
if self._status[row][0] is WorklogDialog.Status.PROGRESS:
icon = ProgressIndicator(self)
icon.setMaximumSize(QtCore.QSize(self.row_height * 0.75, self.row_height * 0.75))
icon.setAnimationDelay(70)
icon.startAnimation()
else:
if self._status[row][0] is Worklog.Status.OK:
if self._status[row][0] is WorklogDialog.Status.OK:
item_name = "dialog-ok"
elif self._status[row][0] is Worklog.Status.ERROR:
elif self._status[row][0] is WorklogDialog.Status.ERROR:
item_name = "edit-delete-remove"
icon = QtWidgets.QLabel()
icon.setPixmap(get_icon(item_name).pixmap(self.row_height * 0.75, self.row_height * 0.75))
@ -204,7 +268,7 @@ class Worklog(QtWidgets.QDialog):
layout.setContentsMargins(self.row_height * 0.1, self.row_height * 0.1,
self.row_height * 0.1, self.row_height * 0.1)
wdgt = Worklog.ClickWidget(self.tableWidget.item(row, 0), self.tableWidget, self)
wdgt = WorklogDialog.ClickWidget(self.tableWidget.item(row, 0), self.tableWidget, self)
wdgt.setLayout(layout)
self.tableWidget.setCellWidget(row, 0, wdgt)
@ -214,21 +278,55 @@ class Worklog(QtWidgets.QDialog):
@QtCore.Slot()
def disable_buttons(self):
self.previous_button.setEnabled(False)
self.next_button.setEnabled(False)
self.upload_button.setEnabled(False)
self.ok_button.setEnabled(False)
@QtCore.Slot()
def enable_ok(self):
def enable_buttons(self):
self.ok_button.setEnabled(True)
self.update_prev_next()
def update_prev_next(self):
prev, next_ = self._worklog.prev_next_avail()
self.previous_button.setEnabled(prev)
self.next_button.setEnabled(next_)
@QtCore.Slot()
def previous(self):
self.save()
self._worklog.previous()
self.update_title()
self._focussed = False
self.refresh_table()
self.update_prev_next()
@QtCore.Slot()
def next(self):
self.save()
self._worklog.next()
self.update_title()
self._focussed = False
self.refresh_table()
self.update_prev_next()
@QtCore.Slot()
def cell_changed(self, row, _):
#print(f"{datetime.now()} cell_changed: {self._changing_items}")
if self._changing_items:
return
self._report_data[row][0] = self.tableWidget.item(row, 0).text()
self.update_status(row, Worklog.Status.PROGRESS, "Fetching")
print(f"{datetime.now()} cell_changed")
self._worklog_data[row][0] = self.tableWidget.item(row, 0).text()
self.save()
self.update_status(row, WorklogDialog.Status.PROGRESS, "Fetching")
self.refresh_table()
@QtCore.Slot()
def text_edit_changed(self, text, row):
self._report_data[row][1] = text
self._worklog_data[row][1] = text
@QtCore.Slot()
def _accept(self):
self.save()
self.accept()