First (half-baked) iteration of reporting.
This commit is contained in:
parent
69fef39bf7
commit
ba18e2dc95
67
data.py
67
data.py
@ -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
16
main.py
@ -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
80
report.py
Normal 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)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user