2023-10-27 00:13:08 +00:00
|
|
|
from textual.app import App, ComposeResult
|
2023-10-27 03:04:30 +00:00
|
|
|
from textual.binding import Binding
|
|
|
|
from textual.widgets import Header, Footer, DataTable, Input
|
|
|
|
from textual.containers import Horizontal, Vertical
|
2023-10-27 01:01:21 +00:00
|
|
|
from textual.coordinate import Coordinate
|
2023-10-27 01:26:15 +00:00
|
|
|
from textual.screen import Screen
|
2023-10-27 00:13:08 +00:00
|
|
|
|
2023-10-27 23:42:30 +00:00
|
|
|
from peewee import fn, JOIN
|
|
|
|
|
|
|
|
from .db import (
|
|
|
|
db,
|
|
|
|
HamsterCategory,
|
|
|
|
HamsterActivity,
|
|
|
|
HamsterFact,
|
|
|
|
KimaiProject,
|
|
|
|
KimaiCustomer,
|
|
|
|
KimaiActivity,
|
2023-10-29 13:37:40 +00:00
|
|
|
HamsterKimaiMapping,
|
2023-10-27 23:42:30 +00:00
|
|
|
)
|
|
|
|
from .kimai import (
|
|
|
|
KimaiAPI,
|
|
|
|
Customer as KimaiAPICustomer,
|
|
|
|
Project as KimaiAPIProject,
|
|
|
|
Activity as KimaiAPIActivity,
|
|
|
|
)
|
2023-10-27 00:13:08 +00:00
|
|
|
|
2023-10-27 15:40:25 +00:00
|
|
|
|
2023-10-27 15:39:58 +00:00
|
|
|
class ListScreen(Screen):
|
2023-10-27 01:01:21 +00:00
|
|
|
def compose(self) -> ComposeResult:
|
2023-10-27 01:26:15 +00:00
|
|
|
"""create child widgets for the app."""
|
2023-10-27 01:01:21 +00:00
|
|
|
yield Header()
|
2023-10-27 03:04:30 +00:00
|
|
|
with Vertical():
|
|
|
|
yield DataTable()
|
|
|
|
with Horizontal():
|
|
|
|
yield Input(id="filter")
|
2023-10-27 01:01:21 +00:00
|
|
|
yield Footer()
|
2023-10-27 00:13:08 +00:00
|
|
|
|
2023-10-27 01:01:21 +00:00
|
|
|
def action_refresh(self) -> None:
|
|
|
|
self._refresh()
|
|
|
|
|
|
|
|
def action_sort(self) -> None:
|
|
|
|
self.table.cursor_type = "column"
|
|
|
|
|
2023-10-27 15:28:46 +00:00
|
|
|
def on_data_table_column_selected(self, event):
|
|
|
|
self.sort = (event.column_key,)
|
|
|
|
event.data_table.sort(*self.sort)
|
|
|
|
event.data_table.cursor_type = "row"
|
|
|
|
|
2023-10-27 03:04:30 +00:00
|
|
|
def action_filter(self) -> None:
|
|
|
|
filter_input = self.query_one("#filter")
|
|
|
|
filter_input.display = True
|
2023-10-27 15:28:46 +00:00
|
|
|
self._refresh(filter_input.value)
|
2023-10-27 03:04:30 +00:00
|
|
|
filter_input.focus()
|
2023-10-27 15:28:46 +00:00
|
|
|
|
|
|
|
def on_input_submitted(self, event):
|
|
|
|
self.table.focus()
|
|
|
|
|
2023-10-27 03:04:30 +00:00
|
|
|
def action_cancelfilter(self) -> None:
|
|
|
|
filter_input = self.query_one("#filter")
|
|
|
|
filter_input.display = False
|
2023-10-27 15:28:46 +00:00
|
|
|
filter_input.clear()
|
|
|
|
self.table.focus()
|
2023-10-27 03:04:30 +00:00
|
|
|
self._refresh()
|
|
|
|
|
2023-10-27 15:39:58 +00:00
|
|
|
def on_input_changed(self, event):
|
|
|
|
self._refresh(event.value)
|
|
|
|
|
|
|
|
|
|
|
|
class ActivitiesScreen(ListScreen):
|
|
|
|
BINDINGS = [
|
|
|
|
("s", "sort", "Sort"),
|
|
|
|
("r", "refresh", "Refresh"),
|
|
|
|
("/", "filter", "Search"),
|
|
|
|
("d", "delete", "Delete activity"),
|
|
|
|
("f", "move_facts", "Move facts"),
|
|
|
|
Binding(key="escape", action="cancelfilter", show=False),
|
|
|
|
]
|
|
|
|
|
|
|
|
def _refresh(self, filter_query=None):
|
|
|
|
self.table.clear()
|
|
|
|
|
2023-10-29 13:37:40 +00:00
|
|
|
facts_count_query = (
|
|
|
|
HamsterFact
|
|
|
|
.select(HamsterFact.activity_id, fn.COUNT(HamsterFact.id).alias('facts_count'))
|
|
|
|
.group_by(HamsterFact.activity_id)
|
|
|
|
.alias('facts_count_query')
|
|
|
|
)
|
|
|
|
|
|
|
|
mappings_count_query = (
|
|
|
|
HamsterKimaiMapping
|
|
|
|
.select(HamsterKimaiMapping.hamster_activity_id,
|
|
|
|
fn.COUNT(HamsterKimaiMapping.id).alias('mappings_count'))
|
|
|
|
.group_by(HamsterKimaiMapping.hamster_activity_id)
|
|
|
|
.alias('mappings_count_query')
|
|
|
|
)
|
|
|
|
|
2023-10-27 23:42:30 +00:00
|
|
|
activities = (
|
|
|
|
HamsterActivity.select(
|
|
|
|
HamsterActivity,
|
2023-10-29 13:37:40 +00:00
|
|
|
HamsterCategory.id,
|
|
|
|
fn.COALESCE(HamsterCategory.name, 'None').alias("category_name"),
|
|
|
|
fn.COALESCE(facts_count_query.c.facts_count, 0).alias('facts_count'),
|
|
|
|
fn.COALESCE(mappings_count_query.c.mappings_count, 0).alias('mappings_count')
|
2023-10-27 23:42:30 +00:00
|
|
|
)
|
|
|
|
.join(HamsterCategory, JOIN.LEFT_OUTER)
|
|
|
|
.switch(HamsterActivity)
|
2023-10-29 13:37:40 +00:00
|
|
|
.join(facts_count_query, JOIN.LEFT_OUTER, on=(HamsterActivity.id == facts_count_query.c.activity_id))
|
|
|
|
.switch(HamsterActivity)
|
|
|
|
.join(mappings_count_query, JOIN.LEFT_OUTER, on=(HamsterActivity.id == mappings_count_query.c.hamster_activity_id))
|
2023-10-27 23:42:30 +00:00
|
|
|
.group_by(HamsterActivity)
|
|
|
|
)
|
|
|
|
|
|
|
|
if filter_query:
|
|
|
|
activities = activities.where(
|
|
|
|
HamsterActivity.name.contains(filter_query)
|
|
|
|
| HamsterCategory.name.contains(filter_query)
|
|
|
|
)
|
2023-10-27 15:39:58 +00:00
|
|
|
|
2023-10-27 15:40:25 +00:00
|
|
|
self.table.add_rows(
|
|
|
|
[
|
|
|
|
[
|
2023-10-28 22:40:27 +00:00
|
|
|
activity.category_id,
|
2023-10-29 13:37:40 +00:00
|
|
|
activity.category_name,
|
2023-10-27 15:40:25 +00:00
|
|
|
activity.id,
|
|
|
|
activity.name,
|
|
|
|
activity.facts_count,
|
2023-10-29 13:37:40 +00:00
|
|
|
activity.mappings_count,
|
2023-10-27 15:40:25 +00:00
|
|
|
]
|
|
|
|
for activity in activities
|
|
|
|
]
|
|
|
|
)
|
2023-10-27 15:39:58 +00:00
|
|
|
|
|
|
|
self.table.sort(*self.sort)
|
|
|
|
|
|
|
|
def on_mount(self) -> None:
|
|
|
|
self.table = self.query_one(DataTable)
|
|
|
|
self.table.cursor_type = "row"
|
2023-10-27 15:40:25 +00:00
|
|
|
self.columns = self.table.add_columns(
|
2023-10-29 13:37:40 +00:00
|
|
|
"category id", "category", "activity id", "activity", "entries", "mappings"
|
2023-10-27 15:40:25 +00:00
|
|
|
)
|
2023-10-27 15:39:58 +00:00
|
|
|
self.sort = (self.columns[1], self.columns[3])
|
|
|
|
self._refresh()
|
|
|
|
|
2023-10-27 01:26:15 +00:00
|
|
|
def action_delete(self) -> None:
|
2023-10-27 15:28:46 +00:00
|
|
|
# get the keys for the row and column under the cursor.
|
2023-10-27 01:01:21 +00:00
|
|
|
row_key, _ = self.table.coordinate_to_cell_key(self.table.cursor_coordinate)
|
|
|
|
|
|
|
|
activity_id = self.table.get_cell_at(
|
|
|
|
Coordinate(self.table.cursor_coordinate.row, 2),
|
|
|
|
)
|
|
|
|
|
2023-10-27 23:42:30 +00:00
|
|
|
activity = HamsterActivity.get(id=activity_id)
|
|
|
|
activity.delete_instance()
|
2023-10-27 01:01:21 +00:00
|
|
|
|
2023-10-27 01:26:15 +00:00
|
|
|
# supply the row key to `remove_row` to delete the row.
|
2023-10-27 01:01:21 +00:00
|
|
|
self.table.remove_row(row_key)
|
|
|
|
|
2023-10-27 15:28:46 +00:00
|
|
|
def action_move_facts(self) -> None:
|
|
|
|
row_idx: int = self.table.cursor_row
|
|
|
|
row_cells = self.table.get_row_at(row_idx)
|
2023-10-27 23:42:30 +00:00
|
|
|
self.move_from_activity = HamsterActivity.get(id=row_cells[2])
|
2023-10-27 15:28:46 +00:00
|
|
|
for col_idx, cell_value in enumerate(row_cells):
|
|
|
|
cell_coordinate = Coordinate(row_idx, col_idx)
|
|
|
|
self.table.update_cell_at(
|
|
|
|
cell_coordinate,
|
|
|
|
f"[red]{cell_value}[/red]",
|
|
|
|
)
|
2023-10-27 15:40:25 +00:00
|
|
|
self.table.move_cursor(row=self.table.cursor_coordinate[0] + 1)
|
2023-10-27 15:28:46 +00:00
|
|
|
|
|
|
|
def on_data_table_row_selected(self, event):
|
|
|
|
if getattr(self, "move_from_activity", None) is not None:
|
2023-10-27 23:42:30 +00:00
|
|
|
move_to_activity = HamsterActivity.get(
|
|
|
|
self.table.get_cell_at(Coordinate(event.cursor_row, 2))
|
2023-10-27 15:40:25 +00:00
|
|
|
)
|
2023-10-27 23:42:30 +00:00
|
|
|
HamsterFact.update({HamsterFact.activity:
|
|
|
|
move_to_activity}).where(HamsterFact.activity ==
|
|
|
|
self.move_from_activity).execute()
|
2023-10-27 15:48:03 +00:00
|
|
|
filter_input = self.query_one("#filter")
|
|
|
|
self._refresh(filter_input.value)
|
2023-10-27 15:28:46 +00:00
|
|
|
del self.move_from_activity
|
2023-10-27 01:26:15 +00:00
|
|
|
|
2023-10-27 15:40:25 +00:00
|
|
|
|
2023-10-27 15:39:58 +00:00
|
|
|
class CategoriesScreen(ListScreen):
|
2023-10-27 01:26:15 +00:00
|
|
|
BINDINGS = [
|
|
|
|
("s", "sort", "Sort"),
|
|
|
|
("r", "refresh", "Refresh"),
|
2023-10-27 03:04:30 +00:00
|
|
|
("/", "filter", "Search"),
|
2023-10-27 15:39:58 +00:00
|
|
|
("d", "delete", "Delete category"),
|
2023-10-27 03:04:30 +00:00
|
|
|
Binding(key="escape", action="cancelfilter", show=False),
|
2023-10-27 01:26:15 +00:00
|
|
|
]
|
|
|
|
|
2023-10-27 03:04:30 +00:00
|
|
|
def _refresh(self, filter_query=None):
|
2023-10-27 01:26:15 +00:00
|
|
|
self.table.clear()
|
|
|
|
|
2023-10-27 23:42:30 +00:00
|
|
|
categories = (
|
|
|
|
HamsterCategory.select(
|
|
|
|
HamsterCategory,
|
|
|
|
fn.Count(HamsterActivity.id).alias("activities_count")
|
|
|
|
)
|
|
|
|
.join(HamsterActivity, JOIN.LEFT_OUTER)
|
|
|
|
.group_by(HamsterCategory)
|
2023-10-27 15:40:25 +00:00
|
|
|
)
|
2023-10-27 03:04:30 +00:00
|
|
|
|
2023-10-27 23:42:30 +00:00
|
|
|
if filter_query:
|
|
|
|
categories = categories.where(
|
|
|
|
HamsterCategory.name.contains(filter_query)
|
|
|
|
)
|
|
|
|
|
2023-10-27 15:40:25 +00:00
|
|
|
self.table.add_rows(
|
|
|
|
[
|
|
|
|
[
|
|
|
|
category.id,
|
|
|
|
category.name,
|
2023-10-27 23:42:30 +00:00
|
|
|
category.activities_count,
|
2023-10-27 15:40:25 +00:00
|
|
|
]
|
|
|
|
for category in categories
|
|
|
|
]
|
|
|
|
)
|
2023-10-27 03:04:30 +00:00
|
|
|
|
|
|
|
self.table.sort(self.sort)
|
2023-10-27 01:26:15 +00:00
|
|
|
|
|
|
|
def on_mount(self) -> None:
|
|
|
|
self.table = self.query_one(DataTable)
|
|
|
|
self.table.cursor_type = "row"
|
2023-10-27 15:40:25 +00:00
|
|
|
self.columns = self.table.add_columns("category id", "category", "activities")
|
2023-10-27 03:04:30 +00:00
|
|
|
self.sort = self.columns[1]
|
2023-10-27 01:26:15 +00:00
|
|
|
self._refresh()
|
|
|
|
|
|
|
|
def action_delete(self) -> None:
|
|
|
|
# get the keys for the row and column under the cursor.
|
|
|
|
row_key, _ = self.table.coordinate_to_cell_key(self.table.cursor_coordinate)
|
|
|
|
|
|
|
|
category_id = self.table.get_cell_at(
|
|
|
|
Coordinate(self.table.cursor_coordinate.row, 0),
|
|
|
|
)
|
2023-10-27 23:42:30 +00:00
|
|
|
category = HamsterCategory.get(id=category_id)
|
|
|
|
category.delete_instance()
|
2023-10-27 01:26:15 +00:00
|
|
|
|
|
|
|
# supply the row key to `remove_row` to delete the row.
|
|
|
|
self.table.remove_row(row_key)
|
|
|
|
|
2023-10-27 15:40:25 +00:00
|
|
|
|
2023-10-27 21:00:03 +00:00
|
|
|
class KimaiScreen(ListScreen):
|
|
|
|
BINDINGS = [
|
|
|
|
("s", "sort", "Sort"),
|
|
|
|
("r", "refresh", "Refresh"),
|
|
|
|
("g", "get", "Get data"),
|
|
|
|
("/", "filter", "Search"),
|
|
|
|
Binding(key="escape", action="cancelfilter", show=False),
|
|
|
|
]
|
|
|
|
|
|
|
|
def _refresh(self, filter_query=None):
|
|
|
|
self.table.clear()
|
|
|
|
|
2023-10-27 23:42:30 +00:00
|
|
|
projects = (
|
|
|
|
KimaiProject.select(
|
|
|
|
KimaiProject,
|
|
|
|
KimaiCustomer,
|
|
|
|
fn.Count(KimaiActivity.id).alias("activities_count")
|
|
|
|
)
|
|
|
|
.join(KimaiCustomer, JOIN.LEFT_OUTER)
|
|
|
|
.switch(KimaiProject)
|
|
|
|
.join(KimaiActivity, JOIN.LEFT_OUTER)
|
2023-10-28 00:21:23 +00:00
|
|
|
.where(KimaiActivity.project.is_null(False))
|
2023-10-27 23:42:30 +00:00
|
|
|
.group_by(KimaiProject)
|
|
|
|
)
|
|
|
|
|
|
|
|
if filter_query:
|
|
|
|
projects = projects.where(
|
|
|
|
KimaiProject.name.contains(filter_query)
|
|
|
|
| KimaiCustomer.name.contains(filter_query)
|
|
|
|
)
|
|
|
|
|
2023-10-27 21:00:03 +00:00
|
|
|
self.table.add_rows(
|
|
|
|
[
|
|
|
|
[
|
2023-10-27 23:42:30 +00:00
|
|
|
project.customer.id,
|
|
|
|
project.customer.name,
|
2023-10-27 21:00:03 +00:00
|
|
|
project.id,
|
|
|
|
project.name,
|
2023-10-27 23:42:30 +00:00
|
|
|
project.activities_count
|
2023-10-27 21:00:03 +00:00
|
|
|
]
|
|
|
|
for project in projects
|
|
|
|
]
|
|
|
|
)
|
|
|
|
|
|
|
|
self.table.sort(self.sort)
|
|
|
|
|
|
|
|
def action_get(self) -> None:
|
|
|
|
api = KimaiAPI()
|
|
|
|
|
2023-10-28 00:21:23 +00:00
|
|
|
KimaiCustomer.delete().execute()
|
|
|
|
KimaiProject.delete().execute()
|
|
|
|
KimaiActivity.delete().execute()
|
|
|
|
|
2023-10-27 21:00:03 +00:00
|
|
|
customers = KimaiAPICustomer.list(api)
|
2023-10-27 23:42:30 +00:00
|
|
|
with db.atomic():
|
|
|
|
KimaiCustomer.insert_many([{
|
|
|
|
'id': customer.id,
|
|
|
|
'name': customer.name
|
|
|
|
} for customer in customers]).execute()
|
2023-10-27 21:00:03 +00:00
|
|
|
|
|
|
|
projects = KimaiAPIProject.list(api)
|
2023-10-27 23:42:30 +00:00
|
|
|
with db.atomic():
|
|
|
|
KimaiProject.insert_many([{
|
|
|
|
'id': project.id,
|
|
|
|
'name': project.name,
|
|
|
|
'customer_id': project.customer.id
|
|
|
|
} for project in projects]).execute()
|
2023-10-27 21:00:03 +00:00
|
|
|
|
2023-10-28 00:21:23 +00:00
|
|
|
activities = KimaiAPIActivity.list(api)
|
|
|
|
with db.atomic():
|
|
|
|
KimaiActivity.insert_many([{
|
|
|
|
'id': activity.id,
|
|
|
|
'name': activity.name,
|
|
|
|
'project_id': (activity.project and activity.project.id or None)
|
|
|
|
} for activity in activities]).execute()
|
|
|
|
|
2023-10-27 21:00:03 +00:00
|
|
|
self._refresh()
|
|
|
|
|
|
|
|
def on_mount(self) -> None:
|
|
|
|
self.table = self.query_one(DataTable)
|
|
|
|
self.table.cursor_type = "row"
|
2023-10-27 23:42:30 +00:00
|
|
|
self.columns = self.table.add_columns(
|
|
|
|
"customer id", "customer", "project id", "project", "activities"
|
|
|
|
)
|
2023-10-27 21:00:03 +00:00
|
|
|
# self.sort = (self.columns[1], self.columns[3])
|
|
|
|
self.sort = self.columns[1]
|
|
|
|
self._refresh()
|
|
|
|
|
|
|
|
|
2023-10-27 01:26:15 +00:00
|
|
|
class HamsterToolsApp(App):
|
2023-10-27 15:40:25 +00:00
|
|
|
CSS_PATH = "app.tcss"
|
2023-10-27 01:26:15 +00:00
|
|
|
BINDINGS = [
|
|
|
|
("a", "switch_mode('activities')", "Activities"),
|
|
|
|
("c", "switch_mode('categories')", "Categories"),
|
2023-10-27 21:00:03 +00:00
|
|
|
("k", "switch_mode('kimai')", "Kimai"),
|
2023-10-27 01:26:15 +00:00
|
|
|
("q", "quit", "Quit"),
|
|
|
|
]
|
2023-10-27 03:04:30 +00:00
|
|
|
|
|
|
|
def __init__(self):
|
2023-10-27 23:42:30 +00:00
|
|
|
db.init("hamster-testing.db")
|
2023-10-27 01:26:15 +00:00
|
|
|
|
|
|
|
self.MODES = {
|
2023-10-27 23:42:30 +00:00
|
|
|
"categories": CategoriesScreen(),
|
|
|
|
"activities": ActivitiesScreen(),
|
|
|
|
"kimai": KimaiScreen(),
|
2023-10-27 01:26:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
super().__init__()
|
|
|
|
|
|
|
|
def on_mount(self) -> None:
|
|
|
|
self.switch_mode("activities")
|
|
|
|
|
|
|
|
def action_quit(self) -> None:
|
|
|
|
self.exit()
|
2023-10-27 23:42:30 +00:00
|
|
|
db.close()
|