Compare commits

..

4 Commits

Author SHA1 Message Date
3wc
f7edf18391 Fuckkk yeah, working mapping-adding 2023-10-29 21:49:26 +00:00
3wc
6b8b4c380e black reformat 2023-10-29 13:40:03 +00:00
3wc
ccbbc80116 Fix mapping import, show mapping count on list 2023-10-29 13:38:32 +00:00
3wc
f8f83ce4d4 Finish converting CLI commands to use peewee 2023-10-29 09:28:25 +00:00
4 changed files with 706 additions and 433 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,15 @@
from textual import on
from textual.app import App, ComposeResult from textual.app import App, ComposeResult
from textual.binding import Binding from textual.binding import Binding
from textual.widgets import Header, Footer, DataTable, Input from textual.containers import Grid
from textual.events import DescendantBlur
from textual.widgets import Header, Footer, DataTable, Input, Button, Label, Checkbox
from textual.containers import Horizontal, Vertical from textual.containers import Horizontal, Vertical
from textual.coordinate import Coordinate from textual.coordinate import Coordinate
from textual.screen import Screen from textual.reactive import reactive
from textual.screen import Screen, ModalScreen
from textual_autocomplete import AutoComplete, Dropdown, DropdownItem
from peewee import fn, JOIN from peewee import fn, JOIN
@ -15,6 +21,7 @@ from .db import (
KimaiProject, KimaiProject,
KimaiCustomer, KimaiCustomer,
KimaiActivity, KimaiActivity,
HamsterKimaiMapping,
) )
from .kimai import ( from .kimai import (
KimaiAPI, KimaiAPI,
@ -65,28 +72,220 @@ class ListScreen(Screen):
self._refresh(event.value) self._refresh(event.value)
class ActivitiesScreen(ListScreen): class ActivityEditScreen(ModalScreen):
BINDINGS = [
("escape", "cancel", "Cancel")
]
def compose(self) -> ComposeResult:
yield Grid(
Label("Are you sure you want to quit?", id="question"),
Button("Quit", variant="error", id="quit"),
Button("Cancel", variant="primary", id="cancel"),
id="dialog",
)
def action_cancel(self):
self.dismiss(None)
class ActivityMappingScreen(ModalScreen):
BINDINGS = [
("ctrl+g", "global", "Toggle global"),
("ctrl+s", "save", "Save"),
("escape", "cancel", "Cancel")
]
customer_id = None
project_id = None
activity_id = None
def __init__(self, category, activity):
self.category = category
self.activity = activity
super().__init__()
@staticmethod
def _filter_dropdowns(options, value):
matches = [c for c in options if value.lower() in c.main.plain.lower()]
return sorted(matches, key=lambda v: v.main.plain.startswith(value.lower()))
def _get_customers(self, input_state):
customers = [
DropdownItem(c.name, str(c.id))
for c in KimaiCustomer.select()
]
return ActivityMappingScreen._filter_dropdowns(customers,
input_state.value)
def _get_projects(self, input_state):
projects = [
DropdownItem(p.name, str(p.id))
for p in KimaiProject.select().where(
KimaiProject.customer_id == self.customer_id
)
]
return ActivityMappingScreen._filter_dropdowns(projects,
input_state.value)
def _get_activities(self, input_state):
activities = KimaiActivity.select()
if self.query_one('#global').value:
activities = activities.where(
KimaiActivity.project_id.is_null(),
)
else:
activities = activities.where(
KimaiActivity.project_id == self.project_id
)
return ActivityMappingScreen._filter_dropdowns([
DropdownItem(a.name, str(a.id))
for a in activities], input_state.value)
def compose(self) -> ComposeResult:
yield Vertical(
Horizontal(
Label(f"Mapping for {self.activity}@{self.category}"),
),
Horizontal(
Label("Customer"),
AutoComplete(
Input(placeholder="Type to search...", id="customer"),
Dropdown(items=self._get_customers),
)
),
Horizontal(
Label("Project"),
AutoComplete(
Input(placeholder="Type to search...", id='project'),
Dropdown(items=self._get_projects),
)
),
Horizontal(
Label("Activity"),
AutoComplete(
Input(placeholder="Type to search...", id='activity'),
Dropdown(items=self._get_activities),
)
),
Horizontal(
Label("Description"),
Input(id='description'),
),
Horizontal(
Label("Tags"),
Input(id='tags'),
),
Horizontal(Checkbox("Global", id='global')),
)
@on(Input.Submitted, '#customer')
def customer_submitted(self, event):
if event.control.parent.dropdown.selected_item is not None:
self.customer_id = str(event.control.parent.dropdown.selected_item.left_meta)
self.query_one('#project').focus()
@on(DescendantBlur, '#customer')
def customer_blur(self, event):
if event.control.parent.dropdown.selected_item is not None:
self.customer_id = str(event.control.parent.dropdown.selected_item.left_meta)
@on(Input.Submitted, '#project')
def project_submitted(self, event):
if event.control.parent.dropdown.selected_item is not None:
self.project_id = str(event.control.parent.dropdown.selected_item.left_meta)
self.query_one('#activity').focus()
@on(DescendantBlur, '#project')
def project_blur(self, event):
if event.control.parent.dropdown.selected_item is not None:
self.project_id = str(event.control.parent.dropdown.selected_item.left_meta)
@on(Input.Submitted, '#activity')
def activity_submitted(self, event):
if event.control.parent.dropdown.selected_item is not None:
self.activity_id = str(event.control.parent.dropdown.selected_item.left_meta)
self.query_one('#activity').focus()
@on(DescendantBlur, '#activity')
def activity_blur(self, event):
if event.control.parent.dropdown.selected_item is not None:
self.activity_id = str(event.control.parent.dropdown.selected_item.left_meta)
def action_global(self):
self.query_one('#global').value = not self.query_one('#global').value
def action_save(self):
self.dismiss({
'kimai_customer_id': self.customer_id,
'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,
'global': self.query_one('#global').value,
})
def action_cancel(self):
self.dismiss(None)
class ActivityListScreen(ListScreen):
BINDINGS = [ BINDINGS = [
("s", "sort", "Sort"), ("s", "sort", "Sort"),
("r", "refresh", "Refresh"), ("r", "refresh", "Refresh"),
("/", "filter", "Search"), ("/", "filter", "Search"),
("d", "delete", "Delete activity"), ("d", "delete", "Delete"),
("f", "move_facts", "Move facts"), ("f", "move_facts", "Move facts"),
("e", "edit", "Edit"),
("m", "mapping", "Mapping"),
Binding(key="escape", action="cancelfilter", show=False), Binding(key="escape", action="cancelfilter", show=False),
] ]
def _refresh(self, filter_query=None): def _refresh(self, filter_query=None):
self.table.clear() self.table.clear()
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")
)
activities = ( activities = (
HamsterActivity.select( HamsterActivity.select(
HamsterActivity, HamsterActivity,
HamsterCategory, HamsterCategory.id,
fn.Count(HamsterFact.id).alias("facts_count") 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"
),
) )
.join(HamsterCategory, JOIN.LEFT_OUTER) .join(HamsterCategory, JOIN.LEFT_OUTER)
.switch(HamsterActivity) .switch(HamsterActivity)
.join(HamsterFact, JOIN.LEFT_OUTER) .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),
)
.group_by(HamsterActivity) .group_by(HamsterActivity)
) )
@ -100,10 +299,11 @@ class ActivitiesScreen(ListScreen):
[ [
[ [
activity.category_id, activity.category_id,
(activity.category.name if (activity.category_id != -1) else ""), activity.category_name,
activity.id, activity.id,
activity.name, activity.name,
activity.facts_count, activity.facts_count,
activity.mappings_count,
] ]
for activity in activities for activity in activities
] ]
@ -115,7 +315,7 @@ class ActivitiesScreen(ListScreen):
self.table = self.query_one(DataTable) self.table = self.query_one(DataTable)
self.table.cursor_type = "row" self.table.cursor_type = "row"
self.columns = self.table.add_columns( self.columns = self.table.add_columns(
"category id", "category", "activity id", "activity", "entries" "category id", "category", "activity id", "activity", "entries", "mappings"
) )
self.sort = (self.columns[1], self.columns[3]) self.sort = (self.columns[1], self.columns[3])
self._refresh() self._refresh()
@ -151,15 +351,44 @@ class ActivitiesScreen(ListScreen):
move_to_activity = HamsterActivity.get( move_to_activity = HamsterActivity.get(
self.table.get_cell_at(Coordinate(event.cursor_row, 2)) self.table.get_cell_at(Coordinate(event.cursor_row, 2))
) )
HamsterFact.update({HamsterFact.activity: HamsterFact.update({HamsterFact.activity: move_to_activity}).where(
move_to_activity}).where(HamsterFact.activity == HamsterFact.activity == self.move_from_activity
self.move_from_activity).execute() ).execute()
filter_input = self.query_one("#filter") filter_input = self.query_one("#filter")
self._refresh(filter_input.value) self._refresh(filter_input.value)
del self.move_from_activity del self.move_from_activity
def action_edit(self):
def handle_edit(properties):
print(properties)
class CategoriesScreen(ListScreen): self.app.push_screen(ActivityEditScreen(), handle_edit)
def action_mapping(self):
selected_activity = HamsterActivity.select(
HamsterActivity,
fn.COALESCE(HamsterCategory.name, "None").alias("category_name"),
).join(HamsterCategory, JOIN.LEFT_OUTER).where(
HamsterActivity.id == self.table.get_cell_at(
Coordinate(self.table.cursor_coordinate.row, 2),
)
).get()
def handle_mapping(mapping):
if mapping is None:
return
m = HamsterKimaiMapping.create(hamster_activity=selected_activity, **mapping)
m.save()
filter_input = self.query_one("#filter")
self._refresh(filter_input.value)
self.app.push_screen(ActivityMappingScreen(
category=selected_activity.category_name,
activity=selected_activity.name
), handle_mapping)
class CategoryListScreen(ListScreen):
BINDINGS = [ BINDINGS = [
("s", "sort", "Sort"), ("s", "sort", "Sort"),
("r", "refresh", "Refresh"), ("r", "refresh", "Refresh"),
@ -173,17 +402,14 @@ class CategoriesScreen(ListScreen):
categories = ( categories = (
HamsterCategory.select( HamsterCategory.select(
HamsterCategory, HamsterCategory, fn.Count(HamsterActivity.id).alias("activities_count")
fn.Count(HamsterActivity.id).alias("activities_count")
) )
.join(HamsterActivity, JOIN.LEFT_OUTER) .join(HamsterActivity, JOIN.LEFT_OUTER)
.group_by(HamsterCategory) .group_by(HamsterCategory)
) )
if filter_query: if filter_query:
categories = categories.where( categories = categories.where(HamsterCategory.name.contains(filter_query))
HamsterCategory.name.contains(filter_query)
)
self.table.add_rows( self.table.add_rows(
[ [
@ -219,7 +445,7 @@ class CategoriesScreen(ListScreen):
self.table.remove_row(row_key) self.table.remove_row(row_key)
class KimaiScreen(ListScreen): class KimaiProjectListScreen(ListScreen):
BINDINGS = [ BINDINGS = [
("s", "sort", "Sort"), ("s", "sort", "Sort"),
("r", "refresh", "Refresh"), ("r", "refresh", "Refresh"),
@ -235,7 +461,7 @@ class KimaiScreen(ListScreen):
KimaiProject.select( KimaiProject.select(
KimaiProject, KimaiProject,
KimaiCustomer, KimaiCustomer,
fn.Count(KimaiActivity.id).alias("activities_count") fn.Count(KimaiActivity.id).alias("activities_count"),
) )
.join(KimaiCustomer, JOIN.LEFT_OUTER) .join(KimaiCustomer, JOIN.LEFT_OUTER)
.switch(KimaiProject) .switch(KimaiProject)
@ -257,7 +483,7 @@ class KimaiScreen(ListScreen):
project.customer.name, project.customer.name,
project.id, project.id,
project.name, project.name,
project.activities_count project.activities_count,
] ]
for project in projects for project in projects
] ]
@ -274,26 +500,37 @@ class KimaiScreen(ListScreen):
customers = KimaiAPICustomer.list(api) customers = KimaiAPICustomer.list(api)
with db.atomic(): with db.atomic():
KimaiCustomer.insert_many([{ KimaiCustomer.insert_many(
'id': customer.id, [{"id": customer.id, "name": customer.name} for customer in customers]
'name': customer.name ).execute()
} for customer in customers]).execute()
projects = KimaiAPIProject.list(api) projects = KimaiAPIProject.list(api)
with db.atomic(): with db.atomic():
KimaiProject.insert_many([{ KimaiProject.insert_many(
'id': project.id, [
'name': project.name, {
'customer_id': project.customer.id "id": project.id,
} for project in projects]).execute() "name": project.name,
"customer_id": project.customer.id,
}
for project in projects
]
).execute()
activities = KimaiAPIActivity.list(api) activities = KimaiAPIActivity.list(api)
with db.atomic(): with db.atomic():
KimaiActivity.insert_many([{ KimaiActivity.insert_many(
'id': activity.id, [
'name': activity.name, {
'project_id': (activity.project and activity.project.id or None) "id": activity.id,
} for activity in activities]).execute() "name": activity.name,
"project_id": (
activity.project and activity.project.id or None
),
}
for activity in activities
]
).execute()
self._refresh() self._refresh()
@ -321,9 +558,9 @@ class HamsterToolsApp(App):
db.init("hamster-testing.db") db.init("hamster-testing.db")
self.MODES = { self.MODES = {
"categories": CategoriesScreen(), "categories": CategoryListScreen(),
"activities": ActivitiesScreen(), "activities": ActivityListScreen(),
"kimai": KimaiScreen(), "kimai": KimaiProjectListScreen(),
} }
super().__init__() super().__init__()

View File

@ -13,3 +13,34 @@ DataTable:focus .datatable--cursor {
#filter { #filter {
display: none; display: none;
} }
ActivityEditScreen, ActivityMappingScreen {
align: center middle;
}
ActivityMappingScreen > Vertical {
padding: 0 1;
width: auto;
height: 30;
border: thick $background 80%;
background: $surface;
}
ActivityMappingScreen Horizontal {
align: left middle;
width: auto;
}
ActivityMappingScreen Label {
padding: 0 1;
width: auto;
border: blank;
}
ActivityMappingScreen AutoComplete {
width: 80;
}
#description, #tags {
width: 30;
}

View File

@ -1,9 +1,9 @@
import logging import logging
from peewee import SqliteDatabase, Model, CharField, ForeignKeyField from peewee import SqliteDatabase, Model, CharField, ForeignKeyField, DateTimeField
from textual.logging import TextualHandler from textual.logging import TextualHandler
logger = logging.getLogger('peewee') logger = logging.getLogger("peewee")
logger.addHandler(TextualHandler()) logger.addHandler(TextualHandler())
logger.setLevel(logging.DEBUG) logger.setLevel(logging.DEBUG)
@ -15,24 +15,27 @@ class HamsterCategory(Model):
class Meta: class Meta:
database = db database = db
table_name = 'categories' table_name = "categories"
class HamsterActivity(Model): class HamsterActivity(Model):
name = CharField() name = CharField()
category = ForeignKeyField(HamsterCategory, backref='activities') category = ForeignKeyField(HamsterCategory, backref="activities")
class Meta: class Meta:
database = db database = db
table_name = 'activities' table_name = "activities"
class HamsterFact(Model): class HamsterFact(Model):
activity = ForeignKeyField(HamsterActivity, backref='facts') activity = ForeignKeyField(HamsterActivity, backref="facts")
start_time = DateTimeField()
end_time = DateTimeField(null=True)
description = CharField()
class Meta: class Meta:
database = db database = db
table_name = 'facts' table_name = "facts"
class KimaiCustomer(Model): class KimaiCustomer(Model):
@ -40,35 +43,35 @@ class KimaiCustomer(Model):
class Meta: class Meta:
database = db database = db
table_name = 'kimai_customers' table_name = "kimai_customers"
class KimaiProject(Model): class KimaiProject(Model):
name = CharField() name = CharField()
customer = ForeignKeyField(KimaiCustomer, backref='projects') customer = ForeignKeyField(KimaiCustomer, backref="projects")
class Meta: class Meta:
database = db database = db
table_name = 'kimai_projects' table_name = "kimai_projects"
class KimaiActivity(Model): class KimaiActivity(Model):
name = CharField() name = CharField()
project = ForeignKeyField(KimaiProject, backref='activities', null=True) project = ForeignKeyField(KimaiProject, backref="activities", null=True)
class Meta: class Meta:
database = db database = db
table_name = 'kimai_activities' table_name = "kimai_activities"
class HamsterKimaiMapping(Model): class HamsterKimaiMapping(Model):
hamster_activity = ForeignKeyField(HamsterActivity, backref='mappings') hamster_activity = ForeignKeyField(HamsterActivity, backref="mappings")
kimai_customer = ForeignKeyField(KimaiCustomer, backref='mappings') kimai_customer = ForeignKeyField(KimaiCustomer, backref="mappings")
kimai_project = ForeignKeyField(KimaiProject, backref='mappings') kimai_project = ForeignKeyField(KimaiProject, backref="mappings")
kimai_activity = ForeignKeyField(KimaiActivity, backref='mappings') kimai_activity = ForeignKeyField(KimaiActivity, backref="mappings")
kimai_description = CharField() kimai_description = CharField()
kimai_tags = CharField() kimai_tags = CharField()
class Meta: class Meta:
database = db database = db
table_name = 'hamster_kimai_mappings' table_name = "hamster_kimai_mappings"