From fd28955da0b5244fda7b85e3a89e3ded30a7d037 Mon Sep 17 00:00:00 2001 From: 3wc <3wc@doesthisthing.work> Date: Fri, 3 Nov 2023 23:42:11 +0000 Subject: [PATCH] Add kimai "visible" field, moar kimai screens --- hamstertools/__init__.py | 2 +- hamstertools/db.py | 3 + hamstertools/kimaiapi.py | 53 +++++++++++------- hamstertools/screens/hamster.py | 16 +++++- hamstertools/screens/kimai.py | 98 ++++++++++++++++++++++++++++++--- hamstertools/sync.py | 13 ++++- 6 files changed, 152 insertions(+), 33 deletions(-) diff --git a/hamstertools/__init__.py b/hamstertools/__init__.py index e014db1..6b042cf 100755 --- a/hamstertools/__init__.py +++ b/hamstertools/__init__.py @@ -25,7 +25,7 @@ from .db import ( from .sync import sync HAMSTER_DIR = Path.home() / ".local/share/hamster" -HAMSTER_FILE = HAMSTER_DIR / 'hamster.db' +HAMSTER_FILE = HAMSTER_DIR / "hamster.db" db.init(HAMSTER_FILE) diff --git a/hamstertools/db.py b/hamstertools/db.py index d8d5c18..b0bc465 100644 --- a/hamstertools/db.py +++ b/hamstertools/db.py @@ -43,6 +43,7 @@ class HamsterFact(Model): class KimaiCustomer(Model): + visible = BooleanField(default=True) name = CharField() class Meta: @@ -53,6 +54,7 @@ class KimaiCustomer(Model): class KimaiProject(Model): name = CharField() customer = ForeignKeyField(KimaiCustomer, backref="projects") + visible = BooleanField(default=True) allow_global_activities = BooleanField(default=True) class Meta: @@ -63,6 +65,7 @@ class KimaiProject(Model): class KimaiActivity(Model): name = CharField() project = ForeignKeyField(KimaiProject, backref="activities", null=True) + visible = BooleanField(default=True) class Meta: database = db diff --git a/hamstertools/kimaiapi.py b/hamstertools/kimaiapi.py index 9333d3b..828984f 100644 --- a/hamstertools/kimaiapi.py +++ b/hamstertools/kimaiapi.py @@ -44,24 +44,26 @@ class BaseAPI(object): setattr(self, key, value) +@dataclass class Customer(BaseAPI): - def __init__(self, api, id, name): - super().__init__(api, id=id, name=name) + api: KimaiAPI = field(repr=False) + id: int + name: str + visible: bool = field(default=True) @staticmethod def list(api): - return [Customer(api, c["id"], c["name"]) for c in api.customers_json] + return [ + Customer(api, c["id"], c["name"], c["visible"]) for c in api.customers_json + ] @staticmethod def get_by_id(api, id): - for value in api.customers_json: - if value["id"] == id: - return Customer(api, value["id"], value["name"]) + for c in api.customers_json: + if c["id"] == id: + return Customer(api, c["id"], c["name"], c["visible"]) raise NotFound() - def __repr__(self): - return f"Customer (id={self.id}, name={self.name})" - @dataclass class Project(BaseAPI): @@ -70,6 +72,7 @@ class Project(BaseAPI): name: str customer: Customer allow_global_activities: bool = field(default=True) + visible: bool = field(default=True) @staticmethod def list(api): @@ -80,20 +83,22 @@ class Project(BaseAPI): p["name"], Customer.get_by_id(api, p["customer"]), p["globalActivities"], + p["visible"], ) for p in api.projects_json ] @staticmethod def get_by_id(api, id, none=False): - for value in api.projects_json: - if value["id"] == id: + for p in api.projects_json: + if p["id"] == id: return Project( api, - value["id"], - value["name"], - Customer.get_by_id(api, value["customer"]), - value["globalActivities"], + p["id"], + p["name"], + Customer.get_by_id(api, p["customer"]), + p["globalActivities"], + p["visible"], ) if not none: raise NotFound() @@ -105,25 +110,31 @@ class Activity(BaseAPI): id: int name: str project: Project + visible: bool = field(default=True) @staticmethod def list(api): return [ Activity( - api, a["id"], a["name"], Project.get_by_id(api, a["project"], none=True) + api, + a["id"], + a["name"], + Project.get_by_id(api, a["project"], none=True), + a["visible"], ) for a in api.activities_json ] @staticmethod def get_by_id(api, id, none=False): - for value in api.activities_json: - if value["id"] == id: + for a in api.activities_json: + if a["id"] == id: return Activity( api, - value["id"], - value["name"], - Project.get_by_id(api, value["project"]), + a["id"], + a["name"], + Project.get_by_id(api, a["project"]), + a["visible"], ) if not none: raise NotFound() diff --git a/hamstertools/screens/hamster.py b/hamstertools/screens/hamster.py index 8e6f59b..56a06c5 100644 --- a/hamstertools/screens/hamster.py +++ b/hamstertools/screens/hamster.py @@ -7,7 +7,16 @@ from textual.coordinate import Coordinate from textual.containers import Horizontal, Vertical from textual.events import DescendantBlur from textual.screen import Screen, ModalScreen -from textual.widgets import Header, Footer, DataTable, Input, Label, Checkbox, TabbedContent, TabPane +from textual.widgets import ( + Header, + Footer, + DataTable, + Input, + Label, + Checkbox, + TabbedContent, + TabPane, +) from peewee import fn, JOIN @@ -558,6 +567,11 @@ class HamsterScreen(Screen): yield ActivityList() yield Footer() + def on_mount(self) -> None: + self.query_one("TabbedContent Tabs").can_focus = False + self.query_one("#activities DataTable").focus() + def action_show_tab(self, tab: str) -> None: """Switch to a new tab.""" self.get_child_by_type(TabbedContent).active = tab + self.query_one(f"#{tab} DataTable").focus() diff --git a/hamstertools/screens/kimai.py b/hamstertools/screens/kimai.py index 597c3e1..0ca0eaa 100644 --- a/hamstertools/screens/kimai.py +++ b/hamstertools/screens/kimai.py @@ -15,6 +15,10 @@ from ..db import ( from .list import ListPane +class KimaiCustomerList(ListPane): + pass + + class KimaiProjectList(ListPane): BINDINGS = [ ("s", "sort", "Sort"), @@ -24,9 +28,11 @@ class KimaiProjectList(ListPane): Binding(key="escape", action="cancelfilter", show=False), ] - def _refresh(self, filter_query=None): + def _refresh(self): self.table.clear() + filter_search = self.query_one("#filter #search").value + projects = ( KimaiProject.select( KimaiProject, @@ -40,10 +46,10 @@ class KimaiProjectList(ListPane): .group_by(KimaiProject) ) - if filter_query: + if filter_search: projects = projects.where( - KimaiProject.name.contains(filter_query) - | KimaiCustomer.name.contains(filter_query) + KimaiProject.name.contains(filter_search) + | KimaiCustomer.name.contains(filter_search) ) self.table.add_rows( @@ -54,6 +60,7 @@ class KimaiProjectList(ListPane): project.id, project.name, project.activities_count, + project.visible, ] for project in projects ] @@ -69,29 +76,104 @@ class KimaiProjectList(ListPane): self.table = self.query_one(DataTable) self.table.cursor_type = "row" self.columns = self.table.add_columns( - "customer id", "customer", "project id", "project", "activities" + "customer id", "customer", "project id", "project", "activities", "visible" ) # self.sort = (self.columns[1], self.columns[3]) self.sort = self.columns[1] self._refresh() +class KimaiActivityList(ListPane): + BINDINGS = [ + ("s", "sort", "Sort"), + ("r", "refresh", "Refresh"), + ("g", "get", "Get data"), + ("/", "filter", "Search"), + Binding(key="escape", action="cancelfilter", show=False), + ] + + def _refresh(self): + self.table.clear() + + filter_search = self.query_one("#filter #search").value + + activities = ( + KimaiActivity.select( + KimaiActivity, + fn.COALESCE(KimaiProject.name, "None").alias("project_name"), + fn.COALESCE(KimaiCustomer.name, "None").alias("customer_name"), + ) + .join(KimaiProject, JOIN.LEFT_OUTER) + .join(KimaiCustomer, JOIN.LEFT_OUTER) + .group_by(KimaiActivity) + ) + + if filter_search: + activities = activities.where(KimaiActivity.name.contains(filter_search)) + + self.table.add_rows( + [ + [ + # activity.project.customer_id if activity.project is not None else '', + # activity.customer_name, + str(activity.project_id) if activity.project_id is not None else "", + activity.project_name, + activity.id, + activity.name, + activity.visible, + ] + for activity in activities + ] + ) + + self.table.sort(self.sort) + + def action_get(self) -> None: + sync() + self._refresh() + + def on_mount(self) -> None: + self.table = self.query_one(DataTable) + self.table.cursor_type = "row" + self.columns = self.table.add_columns( + # "customer id", + # "customer", + "project id", + "project", + "id", + "name", + "visible", + ) + # self.sort = (self.columns[1], self.columns[3]) + self.sort = self.columns[3] + self._refresh() + + class KimaiScreen(Screen): BINDINGS = [ + ("c", "show_tab('customers')", "Customers"), ("p", "show_tab('projects')", "Projects"), + ("a", "show_tab('activities')", "Activities"), ] SUB_TITLE = "Kimai" def compose(self) -> ComposeResult: yield Header() - with TabbedContent(initial="activities"): - with TabPane("Categories", id="categories"): + with TabbedContent(initial="projects"): + with TabPane("Customers", id="customers"): + yield KimaiCustomerList() + with TabPane("Projects", id="projects"): yield KimaiProjectList() with TabPane("Activities", id="activities"): - yield KimaiProjectList() + yield KimaiActivityList() yield Footer() + def on_mount(self) -> None: + self.query_one("TabbedContent Tabs").can_focus = False + self.query_one("#projects DataTable").focus() + def action_show_tab(self, tab: str) -> None: """Switch to a new tab.""" self.get_child_by_type(TabbedContent).active = tab + self.query_one(f"#{tab} DataTable").focus() diff --git a/hamstertools/sync.py b/hamstertools/sync.py index 3ced975..fad0b42 100644 --- a/hamstertools/sync.py +++ b/hamstertools/sync.py @@ -22,7 +22,14 @@ def sync() -> None: customers = KimaiAPICustomer.list(api) with db.atomic(): KimaiCustomer.insert_many( - [{"id": customer.id, "name": customer.name} for customer in customers] + [ + { + "id": customer.id, + "name": customer.name, + "visible": customer.visible, + } + for customer in customers + ] ).execute() projects = KimaiAPIProject.list(api) @@ -34,6 +41,7 @@ def sync() -> None: "name": project.name, "customer_id": project.customer.id, "allow_global_activities": project.allow_global_activities, + "visible": project.visible, } for project in projects ] @@ -46,7 +54,8 @@ def sync() -> None: { "id": activity.id, "name": activity.name, - "project_id": (activity.project and activity.project.id or None), + "project_id": (activity.project.id if activity.project else None), + "visible": activity.visible, } for activity in activities ]