First (half-baked) iteration of reporting.

This commit is contained in:
Faerbit 2020-02-20 15:22:40 +01:00
parent 69fef39bf7
commit ba18e2dc95
3 changed files with 146 additions and 17 deletions

67
data.py
View File

@ -1,6 +1,8 @@
import os import os
import json import json
import base64
import atexit import atexit
from datetime import datetime, date, time
from threading import Thread, Event from threading import Thread, Event
from collections.abc import MutableMapping from collections.abc import MutableMapping
@ -12,7 +14,8 @@ data_dir_path = os.path.join(QtCore.QStandardPaths.writableLocation(QtCore.QStan
tasks_path = os.path.join(data_dir_path, "tasks.json") tasks_path = os.path.join(data_dir_path, "tasks.json")
data_path = os.path.join(data_dir_path, "data_{}.json") data_path = os.path.join(data_dir_path, "data_{}.json")
save_delay = 3 * 60 #save_delay = 3 * 60
save_delay = 3
class Tasks: class Tasks:
@ -21,7 +24,8 @@ class Tasks:
os.mkdir(data_dir_path) os.mkdir(data_dir_path)
if os.path.exists(tasks_path): if os.path.exists(tasks_path):
with open(tasks_path, "r") as f: with open(tasks_path, "r") as f:
self._tasks = json.loads(f.read()) encoded_tasks = json.loads(f.read())
self._tasks = list(map(lambda x: base64.b64decode(x.encode("utf-8")).decode("utf-8"), encoded_tasks))
else: else:
self._tasks = [] self._tasks = []
@ -36,8 +40,9 @@ class Tasks:
def _save(self): def _save(self):
print("...saving tasks...") print("...saving tasks...")
encoded_tasks = list(map(lambda x: base64.b64encode(x.encode("utf-8")).decode("utf-8"), self._tasks))
with open(tasks_path, "w+") as f: with open(tasks_path, "w+") as f:
f.write(json.dumps(self._tasks)) f.write(json.dumps(encoded_tasks))
class Data(MutableMapping): class Data(MutableMapping):
@ -46,22 +51,23 @@ class Data(MutableMapping):
os.mkdir(data_dir_path) os.mkdir(data_dir_path)
self._cache = {} self._cache = {}
self._hot_keys = [] self._hot_keys = []
self._running = False self._trunning = False
self._tevent = Event() self._tevent = Event()
self._thread = None self._thread = None
def cleanup(): def cleanup():
self._running = False self._trunning = False
self._tevent.set() self._tevent.set()
if self._thread: if self._thread:
self._thread.join() self._thread.join()
atexit.register(cleanup) atexit.register(cleanup)
def __getitem__(self, key): def __getitem__(self, key):
dpath = data_path.format(key) dpath = data_path.format(key)
if key not in self._cache and os.path.exists(dpath): if key not in self._cache and os.path.exists(dpath):
with open(dpath, "r") as f: with open(dpath, "r") as f:
self._cache[key] = f.read() self._cache[key] = json.loads(f.read())
return self._cache[key] return self._cache[key]
def __setitem__(self, key, value): def __setitem__(self, key, value):
@ -70,14 +76,14 @@ class Data(MutableMapping):
self._schedule_save() self._schedule_save()
def _schedule_save(self): def _schedule_save(self):
if self._running: if self._trunning:
return return
self._running = True self._trunning = True
self._thread = Thread(target=self._executor, daemon=True) self._thread = Thread(target=self._executor, daemon=True)
self._thread.start() self._thread.start()
def _executor(self): def _executor(self):
while self._running: while self._trunning:
self._tevent.wait(save_delay) self._tevent.wait(save_delay)
self._save() self._save()
@ -91,13 +97,50 @@ class Data(MutableMapping):
self._saving = False self._saving = False
def __delitem__(self, key): def __delitem__(self, key):
print("WARNING: deletion of items not supported") return NotImplemented
def __iter__(self): def __iter__(self):
return iter(self._cache) return NotImplemented
def __len__(self): def __len__(self):
return len(self._cache) # TODO use glob?
return NotImplemented
def __repr__(self): def __repr__(self):
return f"{type(self).__name__}({self._cache})" return f"{type(self).__name__}({self._cache})"
class Log:
def __init__(self):
self._data = Data()
def log(self, task, ptime=datetime.now()):
month = self._data.setdefault(ptime.strftime("%Y-%m"), {})
month.setdefault(ptime.strftime("%d"), [])\
.append(f"{ptime.strftime('%H:%M:%S')} {base64.b64encode(task.encode('utf-8')).decode('utf-8')}")
self._data[ptime.strftime("%Y-%m")] = month # necessary to trigger Data.__setitem__
def last_log(self, pdate=date.today()):
if pdate.strftime("%Y-%m") not in self._data or pdate.strftime("%d") not in self._data[pdate.strftime("%Y-%m")]:
return None
return base64.b64decode(
self._data[pdate.strftime("%Y-%m")][pdate.strftime("%d")][-1].split()[1].encode("utf-8")).decode("utf-8")
def report(self, pdate=date.today()):
tmp = []
for e in self._data[pdate.strftime("%Y-%m")][pdate.strftime("%d")]:
tstr, b64str = e.split()
task = base64.b64decode(b64str.encode("utf-8")).decode("utf-8")
start_time = datetime.combine(pdate, datetime.strptime(tstr, "%H:%M:%S").time())
tmp.append((task, start_time))
ret = []
for i, t in enumerate(tmp):
task, start_time = t
if i < len(tmp) - 1:
end_time = tmp[i+1][1]
else:
end_time = datetime.now()
duration = end_time - start_time
ret.append((task, start_time, duration))
return ret

16
main.py
View File

@ -6,8 +6,9 @@ from functools import partial
from PySide2 import QtCore, QtGui, QtWidgets from PySide2 import QtCore, QtGui, QtWidgets
from data import Tasks, Data from data import Tasks, Log
from task_edit import TaskEdit from task_edit import TaskEdit
from report import Report
class App: class App:
@ -15,8 +16,8 @@ class App:
self.app = QtWidgets.QApplication(sys.argv) self.app = QtWidgets.QApplication(sys.argv)
self.tasks = Tasks() self.tasks = Tasks()
self.active_task = "Nothing" self.log = Log()
self.data = Data() self.active_task = self.log.last_log() or "Nothing"
icon = QtGui.QIcon.fromTheme("appointment-new") icon = QtGui.QIcon.fromTheme("appointment-new")
@ -32,13 +33,16 @@ class App:
self.taskEdit = TaskEdit(None) self.taskEdit = TaskEdit(None)
self.taskEdit.accepted.connect(self.tasks_edited) self.taskEdit.accepted.connect(self.tasks_edited)
self.report = Report(None)
@QtCore.Slot() @QtCore.Slot()
def tasks_edited(self): def tasks_edited(self):
self.tasks.tasks = self.taskEdit.tasks self.tasks.tasks = self.taskEdit.tasks
self.update_tray_menu() self.update_tray_menu()
def change_task(self, task): def change_task(self, task):
self.tasks.active = task self.active_task = task
self.log.log(task)
self.update_tray_menu() self.update_tray_menu()
def update_tray_menu(self): def update_tray_menu(self):
@ -76,10 +80,12 @@ class App:
signal.signal(signal.SIGTERM, self.sigterm_handler) signal.signal(signal.SIGTERM, self.sigterm_handler)
signal.signal(signal.SIGINT, self.sigterm_handler) signal.signal(signal.SIGINT, self.sigterm_handler)
self.app.exec_() self.app.exec_()
sys.exit()
@QtCore.Slot() @QtCore.Slot()
def report(self): def report(self):
pass self.report.set_data(self.log.report())
self.report.show()
@QtCore.Slot() @QtCore.Slot()
def edit_tasks(self): def edit_tasks(self):

80
report.py Normal file
View File

@ -0,0 +1,80 @@
from PySide2 import QtCore, QtGui, QtWidgets
class Report(QtWidgets.QDialog):
def __init__(self, parent, *args, **kwargs):
super().__init__(parent, *args, **kwargs)
self.setWindowTitle("Report")
self.tableWidget = QtWidgets.QTableWidget()
self.tableWidget.verticalHeader().hide()
self.tableWidget.setColumnCount(3)
self.tableWidget.setHorizontalHeaderLabels(["Task", "Start time", "Duration"])
self.header = QtWidgets.QHeaderView(QtCore.Qt.Orientation.Horizontal)
self.tableWidget.setHorizontalHeader(self.header)
new_button = QtWidgets.QPushButton()
new_button.setText("New item")
new_button.setIcon(QtGui.QIcon.fromTheme("list-add"))
new_button.pressed.connect(self.new_task)
del_button = QtWidgets.QPushButton()
del_button.setText("Delete item")
del_button.setIcon(QtGui.QIcon.fromTheme("list-remove"))
del_button.pressed.connect(self.del_task)
ok_button = QtWidgets.QPushButton()
ok_button.setText("OK")
ok_button.setIcon(QtGui.QIcon.fromTheme("dialog-ok-apply"))
ok_button.pressed.connect(self.accept)
blayout = QtWidgets.QHBoxLayout()
blayout.addWidget(new_button)
blayout.addWidget(del_button)
blayout.addWidget(ok_button)
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(self.tableWidget)
layout.addLayout(blayout)
self.setLayout(layout)
def set_data(self, data):
self.tableWidget.setRowCount(len(data))
for row, _ in enumerate(data):
self.tableWidget.setItem(row, 0, QtWidgets.QTableWidgetItem(data[row][0]))
self.tableWidget.setItem(row, 1, QtWidgets.QTableWidgetItem(str(data[row][1])))
self.tableWidget.setItem(row, 2, QtWidgets.QTableWidgetItem(str(data[row][2])))
self.tableWidget.resizeColumnsToContents()
min_width = 0
for i in range(3):
min_width += self.header.sectionSize(i)
self.tableWidget.setMinimumWidth(min_width * 1.33)
self.header.setSectionResizeMode(QtWidgets.QHeaderView.Stretch)
@QtCore.Slot()
def new_task(self):
l = self.list.stringList()
l.append("")
self.list.setStringList(l)
i = self.list.index(len(l)-1)
self.tableView.setCurrentIndex(i)
self.tableView.edit(i)
@QtCore.Slot()
def del_task(self):
l = self.list.stringList()
del l[self.tableView.currentIndex().row()]
self.list.setStringList(l)
@property
def tasks(self):
ret = self.list.stringList()
return list(filter(None, ret)) # filter empty strings
@tasks.setter
def tasks(self, tasks):
self.list.setStringList(tasks)