Add hamster tag management, reinstate tags during import, reorganise
code
This commit is contained in:
42
hamstertools/screens/hamster/__init__.py
Normal file
42
hamstertools/screens/hamster/__init__.py
Normal file
@ -0,0 +1,42 @@
|
||||
from textual.app import ComposeResult
|
||||
from textual.screen import Screen
|
||||
from textual.widgets import (
|
||||
Header,
|
||||
Footer,
|
||||
TabbedContent,
|
||||
TabPane,
|
||||
)
|
||||
|
||||
from .activities import ActivityList
|
||||
from .categories import CategoryList
|
||||
from .tags import TagList
|
||||
|
||||
|
||||
class HamsterScreen(Screen):
|
||||
BINDINGS = [
|
||||
("c", "show_tab('categories')", "Categories"),
|
||||
("a", "show_tab('activities')", "Activities"),
|
||||
("t", "show_tab('tags')", "Tags"),
|
||||
]
|
||||
|
||||
SUB_TITLE = "Hamster"
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Header()
|
||||
with TabbedContent(initial="activities"):
|
||||
with TabPane("Categories", id="categories"):
|
||||
yield CategoryList()
|
||||
with TabPane("Activities", id="activities"):
|
||||
yield ActivityList()
|
||||
with TabPane("Tags", id="tags"):
|
||||
yield TagList()
|
||||
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()
|
||||
@ -1,39 +1,18 @@
|
||||
from datetime import datetime
|
||||
|
||||
from datetime import datetime
|
||||
from peewee import JOIN, fn
|
||||
from textual import on
|
||||
from textual.app import ComposeResult
|
||||
from textual.binding import Binding
|
||||
from textual.containers import Grid, Horizontal, Vertical
|
||||
from textual.coordinate import Coordinate
|
||||
from textual.containers import Horizontal, Vertical, Grid
|
||||
from textual.events import DescendantBlur
|
||||
from textual.screen import Screen, ModalScreen
|
||||
from textual.widgets import (
|
||||
Header,
|
||||
Footer,
|
||||
DataTable,
|
||||
Input,
|
||||
Label,
|
||||
Checkbox,
|
||||
TabbedContent,
|
||||
TabPane,
|
||||
Button
|
||||
)
|
||||
|
||||
from peewee import fn, JOIN
|
||||
|
||||
from textual.screen import ModalScreen
|
||||
from textual.widgets import Button, Checkbox, DataTable, Input, Label
|
||||
from textual_autocomplete import AutoComplete, Dropdown, DropdownItem
|
||||
|
||||
from ..db import (
|
||||
HamsterCategory,
|
||||
HamsterActivity,
|
||||
HamsterFact,
|
||||
KimaiProject,
|
||||
KimaiCustomer,
|
||||
KimaiActivity,
|
||||
HamsterActivityKimaiMapping,
|
||||
)
|
||||
|
||||
from .list import ListPane
|
||||
from hamstertools.db import HamsterActivity, HamsterActivityKimaiMapping, HamsterCategory, HamsterFact, KimaiActivity, KimaiCustomer, KimaiProject
|
||||
from hamstertools.screens.list import ListPane
|
||||
|
||||
|
||||
class ActivityEditScreen(ModalScreen):
|
||||
@ -209,7 +188,7 @@ class ActivityMappingScreen(ModalScreen):
|
||||
),
|
||||
Horizontal(
|
||||
Label("Tags"),
|
||||
Input(id="tags", value=self.tags),
|
||||
Input(id="activity_tags", value=self.tags),
|
||||
),
|
||||
Horizontal(Checkbox("Global", id="global")),
|
||||
)
|
||||
@ -265,7 +244,7 @@ class ActivityMappingScreen(ModalScreen):
|
||||
"kimai_project_id": self.project_id,
|
||||
"kimai_activity_id": self.activity_id,
|
||||
"kimai_description": self.query_one("#description").value,
|
||||
"kimai_tags": self.query_one("#tags").value,
|
||||
"kimai_tags": self.query_one("#activity_tags").value,
|
||||
"global": self.query_one("#global").value,
|
||||
}
|
||||
)
|
||||
@ -513,98 +492,3 @@ class ActivityList(ListPane):
|
||||
),
|
||||
handle_mapping,
|
||||
)
|
||||
|
||||
|
||||
class CategoryList(ListPane):
|
||||
BINDINGS = [
|
||||
("s", "sort", "Sort"),
|
||||
("r", "refresh", "Refresh"),
|
||||
("/", "filter", "Search"),
|
||||
("d", "delete", "Delete category"),
|
||||
Binding(key="escape", action="cancelfilter", show=False),
|
||||
]
|
||||
|
||||
def _refresh(self):
|
||||
self.table.clear()
|
||||
|
||||
categories = (
|
||||
HamsterCategory.select(
|
||||
HamsterCategory,
|
||||
fn.Count(HamsterActivity.id).alias("activities_count"),
|
||||
HamsterFact.start_time,
|
||||
)
|
||||
.join(HamsterActivity, JOIN.LEFT_OUTER)
|
||||
.join(HamsterFact, JOIN.LEFT_OUTER)
|
||||
.group_by(HamsterCategory)
|
||||
)
|
||||
|
||||
filter_search = self.query_one("#filter #search").value
|
||||
if filter_search is not None:
|
||||
categories = categories.where(HamsterCategory.name.contains(filter_search))
|
||||
|
||||
filter_date = self.query_one("#filter #date").value
|
||||
if filter_date is not None:
|
||||
try:
|
||||
categories = categories.where(
|
||||
HamsterFact.start_time > datetime.strptime(filter_date, "%Y-%m-%d")
|
||||
)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
self.table.add_rows(
|
||||
[
|
||||
[
|
||||
category.id,
|
||||
category.name,
|
||||
category.activities_count,
|
||||
]
|
||||
for category in categories
|
||||
]
|
||||
)
|
||||
|
||||
self.table.sort(self.sort)
|
||||
|
||||
def on_mount(self) -> None:
|
||||
self.table = self.query_one(DataTable)
|
||||
self.table.cursor_type = "row"
|
||||
self.columns = self.table.add_columns("category id", "category", "activities")
|
||||
self.sort = self.columns[1]
|
||||
self._refresh()
|
||||
|
||||
def action_delete(self) -> None:
|
||||
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),
|
||||
)
|
||||
category = HamsterCategory.get(id=category_id)
|
||||
category.delete_instance()
|
||||
|
||||
self.table.remove_row(row_key)
|
||||
|
||||
|
||||
class HamsterScreen(Screen):
|
||||
BINDINGS = [
|
||||
("c", "show_tab('categories')", "Categories"),
|
||||
("a", "show_tab('activities')", "Activities"),
|
||||
]
|
||||
|
||||
SUB_TITLE = "Hamster"
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Header()
|
||||
with TabbedContent(initial="activities"):
|
||||
with TabPane("Categories", id="categories"):
|
||||
yield CategoryList()
|
||||
with TabPane("Activities", id="activities"):
|
||||
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()
|
||||
78
hamstertools/screens/hamster/categories.py
Normal file
78
hamstertools/screens/hamster/categories.py
Normal file
@ -0,0 +1,78 @@
|
||||
|
||||
from datetime import datetime
|
||||
from peewee import JOIN, fn
|
||||
from textual.binding import Binding
|
||||
from textual.coordinate import Coordinate
|
||||
from textual.widgets import DataTable
|
||||
from hamstertools.db import HamsterActivity, HamsterCategory, HamsterFact
|
||||
from hamstertools.screens.list import ListPane
|
||||
|
||||
|
||||
class CategoryList(ListPane):
|
||||
BINDINGS = [
|
||||
("s", "sort", "Sort"),
|
||||
("r", "refresh", "Refresh"),
|
||||
("/", "filter", "Search"),
|
||||
("d", "delete", "Delete category"),
|
||||
Binding(key="escape", action="cancelfilter", show=False),
|
||||
]
|
||||
|
||||
def _refresh(self):
|
||||
self.table.clear()
|
||||
|
||||
categories = (
|
||||
HamsterCategory.select(
|
||||
HamsterCategory,
|
||||
fn.Count(HamsterActivity.id).alias("activities_count"),
|
||||
HamsterFact.start_time,
|
||||
)
|
||||
.join(HamsterActivity, JOIN.LEFT_OUTER)
|
||||
.join(HamsterFact, JOIN.LEFT_OUTER)
|
||||
.group_by(HamsterCategory)
|
||||
)
|
||||
|
||||
filter_search = self.query_one("#filter #search").value
|
||||
if filter_search is not None:
|
||||
categories = categories.where(HamsterCategory.name.contains(filter_search))
|
||||
|
||||
filter_date = self.query_one("#filter #date").value
|
||||
if filter_date is not None:
|
||||
try:
|
||||
categories = categories.where(
|
||||
HamsterFact.start_time > datetime.strptime(filter_date, "%Y-%m-%d")
|
||||
)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
self.table.add_rows(
|
||||
[
|
||||
[
|
||||
category.id,
|
||||
category.name,
|
||||
category.activities_count,
|
||||
]
|
||||
for category in categories
|
||||
]
|
||||
)
|
||||
|
||||
self.table.sort(self.sort)
|
||||
|
||||
def on_mount(self) -> None:
|
||||
self.table = self.query_one(DataTable)
|
||||
self.table.cursor_type = "row"
|
||||
self.columns = self.table.add_columns("category id", "category", "activities")
|
||||
self.sort = self.columns[1]
|
||||
self._refresh()
|
||||
|
||||
def action_delete(self) -> None:
|
||||
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),
|
||||
)
|
||||
category = HamsterCategory.get(id=category_id)
|
||||
category.delete_instance()
|
||||
|
||||
self.table.remove_row(row_key)
|
||||
|
||||
|
||||
165
hamstertools/screens/hamster/tags.py
Normal file
165
hamstertools/screens/hamster/tags.py
Normal file
@ -0,0 +1,165 @@
|
||||
|
||||
from peewee import JOIN, fn
|
||||
from textual import on
|
||||
from textual.app import ComposeResult
|
||||
from textual.binding import Binding
|
||||
from textual.containers import Horizontal, Vertical
|
||||
from textual.coordinate import Coordinate
|
||||
from textual.events import DescendantBlur
|
||||
from textual.screen import ModalScreen
|
||||
from textual.widgets import DataTable, Input, Label
|
||||
from textual_autocomplete import AutoComplete, Dropdown
|
||||
from hamstertools.db import HamsterFactTag, HamsterTag
|
||||
from hamstertools.screens.list import ListPane
|
||||
|
||||
|
||||
class TagEditScreen(ModalScreen):
|
||||
BINDINGS = [
|
||||
("escape", "cancel", "Cancel"),
|
||||
("ctrl+s", "save", "Save"),
|
||||
]
|
||||
|
||||
def __init__(self, tag):
|
||||
self.tag_name = tag.name
|
||||
super().__init__()
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Vertical(
|
||||
Horizontal(
|
||||
Label("Tag:"), Input(value=self.tag_name, id="tag")
|
||||
),
|
||||
)
|
||||
|
||||
def action_cancel(self):
|
||||
self.dismiss(None)
|
||||
|
||||
def action_save(self):
|
||||
self.dismiss(
|
||||
{
|
||||
"tag": self.query_one("#tag").value,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
class TagList(ListPane):
|
||||
BINDINGS = [
|
||||
# ("s", "sort", "Sort"),
|
||||
# ("r", "refresh", "Refresh"),
|
||||
("/", "filter", "Search"),
|
||||
("d", "delete", "Delete"),
|
||||
("f", "move_facts", "Move"),
|
||||
("e", "edit", "Edit"),
|
||||
Binding(key="escape", action="cancelfilter", show=False),
|
||||
]
|
||||
|
||||
move_from_tag = None
|
||||
|
||||
def _refresh(self):
|
||||
self.table.clear()
|
||||
|
||||
facts_count_query = (
|
||||
HamsterFactTag.select(
|
||||
HamsterFactTag.tag_id, fn.COUNT(HamsterFactTag.tag_id).alias("facts_count")
|
||||
)
|
||||
.group_by(HamsterFactTag.tag_id)
|
||||
.alias("facts_count_query")
|
||||
)
|
||||
|
||||
tags = (
|
||||
HamsterTag.select(
|
||||
HamsterTag,
|
||||
HamsterTag.name,
|
||||
fn.COALESCE(facts_count_query.c.facts_count, 0).alias("facts_count"),
|
||||
)
|
||||
.join(HamsterFactTag, JOIN.LEFT_OUTER)
|
||||
.switch(HamsterTag)
|
||||
.join(
|
||||
facts_count_query,
|
||||
JOIN.LEFT_OUTER,
|
||||
on=(HamsterTag.id == facts_count_query.c.tag_id),
|
||||
)
|
||||
.group_by(HamsterTag)
|
||||
)
|
||||
|
||||
filter_search = self.query_one("#filter #search").value
|
||||
if filter_search is not None:
|
||||
tags = tags.where(
|
||||
HamsterTag.name.contains(filter_search)
|
||||
)
|
||||
|
||||
self.table.add_rows(
|
||||
[
|
||||
[
|
||||
tag.id,
|
||||
tag.name,
|
||||
tag.facts_count,
|
||||
]
|
||||
for tag in tags
|
||||
]
|
||||
)
|
||||
|
||||
self.table.sort(*self.sort)
|
||||
|
||||
def action_delete(self) -> None:
|
||||
row_key, _ = self.table.coordinate_to_cell_key(self.table.cursor_coordinate)
|
||||
|
||||
tag_id = self.table.get_cell_at(
|
||||
Coordinate(self.table.cursor_coordinate.row, 0),
|
||||
)
|
||||
tag = HamsterTag.get(id=tag_id)
|
||||
tag.delete_instance()
|
||||
|
||||
self.table.remove_row(row_key)
|
||||
|
||||
def action_move_facts(self) -> None:
|
||||
row_idx: int = self.table.cursor_row
|
||||
row_cells = self.table.get_row_at(row_idx)
|
||||
self.move_from_tag = HamsterTag.get(id=row_cells[0])
|
||||
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]",
|
||||
)
|
||||
self.table.move_cursor(row=self.table.cursor_coordinate[0] + 1)
|
||||
|
||||
def on_data_table_row_selected(self, event):
|
||||
if getattr(self, "move_from_tag", None) is not None:
|
||||
move_to_tag = HamsterTag.get(
|
||||
self.table.get_cell_at(Coordinate(event.cursor_row, 0))
|
||||
)
|
||||
HamsterFactTag.update({HamsterFactTag.tag: move_to_tag}).where(
|
||||
HamsterFactTag.tag == self.move_from_tag
|
||||
).execute()
|
||||
self._refresh()
|
||||
del self.move_from_tag
|
||||
|
||||
def action_edit(self):
|
||||
row_idx: int = self.table.cursor_row
|
||||
row_cells = self.table.get_row_at(row_idx)
|
||||
|
||||
tag = HamsterTag.get(id=row_cells[0])
|
||||
|
||||
def handle_edit(properties):
|
||||
if properties is None:
|
||||
return
|
||||
tag.name = properties["tag"]
|
||||
tag.save()
|
||||
self._refresh()
|
||||
|
||||
self.app.push_screen(
|
||||
TagEditScreen(tag=tag), handle_edit
|
||||
)
|
||||
|
||||
|
||||
def on_mount(self) -> None:
|
||||
self.table = self.query_one(DataTable)
|
||||
self.table.cursor_type = "row"
|
||||
self.columns = self.table.add_columns(
|
||||
"tag id", "tag", "facts"
|
||||
)
|
||||
self.sort = (self.columns[1],)
|
||||
self._refresh()
|
||||
|
||||
@ -6,16 +6,16 @@ from textual.widgets import DataTable, TabbedContent, TabPane, Header, Footer
|
||||
|
||||
from peewee import fn, JOIN
|
||||
|
||||
from ..utils import truncate
|
||||
from ..sync import sync
|
||||
from ..db import (
|
||||
from ...utils import truncate
|
||||
from ...sync import sync
|
||||
from ...db import (
|
||||
KimaiProject,
|
||||
KimaiCustomer,
|
||||
KimaiActivity,
|
||||
)
|
||||
from ..kimaiapi import Timesheet as KimaiAPITimesheet
|
||||
from ...kimaiapi import Timesheet as KimaiAPITimesheet
|
||||
|
||||
from .list import ListPane
|
||||
from ..list import ListPane
|
||||
|
||||
|
||||
class KimaiCustomerList(ListPane):
|
||||
Reference in New Issue
Block a user