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.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.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
@ -15,6 +21,7 @@ from .db import (
KimaiProject,
KimaiCustomer,
KimaiActivity,
HamsterKimaiMapping,
)
from .kimai import (
KimaiAPI,
@ -65,28 +72,220 @@ class ListScreen(Screen):
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 = [
("s", "sort", "Sort"),
("r", "refresh", "Refresh"),
("/", "filter", "Search"),
("d", "delete", "Delete activity"),
("d", "delete", "Delete"),
("f", "move_facts", "Move facts"),
("e", "edit", "Edit"),
("m", "mapping", "Mapping"),
Binding(key="escape", action="cancelfilter", show=False),
]
def _refresh(self, filter_query=None):
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 = (
HamsterActivity.select(
HamsterActivity,
HamsterCategory,
fn.Count(HamsterFact.id).alias("facts_count")
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"
),
)
.join(HamsterCategory, JOIN.LEFT_OUTER)
.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)
)
@ -100,10 +299,11 @@ class ActivitiesScreen(ListScreen):
[
[
activity.category_id,
(activity.category.name if (activity.category_id != -1) else ""),
activity.category_name,
activity.id,
activity.name,
activity.facts_count,
activity.mappings_count,
]
for activity in activities
]
@ -115,7 +315,7 @@ class ActivitiesScreen(ListScreen):
self.table = self.query_one(DataTable)
self.table.cursor_type = "row"
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._refresh()
@ -151,15 +351,44 @@ class ActivitiesScreen(ListScreen):
move_to_activity = HamsterActivity.get(
self.table.get_cell_at(Coordinate(event.cursor_row, 2))
)
HamsterFact.update({HamsterFact.activity:
move_to_activity}).where(HamsterFact.activity ==
self.move_from_activity).execute()
HamsterFact.update({HamsterFact.activity: move_to_activity}).where(
HamsterFact.activity == self.move_from_activity
).execute()
filter_input = self.query_one("#filter")
self._refresh(filter_input.value)
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 = [
("s", "sort", "Sort"),
("r", "refresh", "Refresh"),
@ -173,17 +402,14 @@ class CategoriesScreen(ListScreen):
categories = (
HamsterCategory.select(
HamsterCategory,
fn.Count(HamsterActivity.id).alias("activities_count")
HamsterCategory, fn.Count(HamsterActivity.id).alias("activities_count")
)
.join(HamsterActivity, JOIN.LEFT_OUTER)
.group_by(HamsterCategory)
)
if filter_query:
categories = categories.where(
HamsterCategory.name.contains(filter_query)
)
categories = categories.where(HamsterCategory.name.contains(filter_query))
self.table.add_rows(
[
@ -219,7 +445,7 @@ class CategoriesScreen(ListScreen):
self.table.remove_row(row_key)
class KimaiScreen(ListScreen):
class KimaiProjectListScreen(ListScreen):
BINDINGS = [
("s", "sort", "Sort"),
("r", "refresh", "Refresh"),
@ -235,7 +461,7 @@ class KimaiScreen(ListScreen):
KimaiProject.select(
KimaiProject,
KimaiCustomer,
fn.Count(KimaiActivity.id).alias("activities_count")
fn.Count(KimaiActivity.id).alias("activities_count"),
)
.join(KimaiCustomer, JOIN.LEFT_OUTER)
.switch(KimaiProject)
@ -257,7 +483,7 @@ class KimaiScreen(ListScreen):
project.customer.name,
project.id,
project.name,
project.activities_count
project.activities_count,
]
for project in projects
]
@ -274,26 +500,37 @@ class KimaiScreen(ListScreen):
customers = KimaiAPICustomer.list(api)
with db.atomic():
KimaiCustomer.insert_many([{
'id': customer.id,
'name': customer.name
} for customer in customers]).execute()
KimaiCustomer.insert_many(
[{"id": customer.id, "name": customer.name} for customer in customers]
).execute()
projects = KimaiAPIProject.list(api)
with db.atomic():
KimaiProject.insert_many([{
'id': project.id,
'name': project.name,
'customer_id': project.customer.id
} for project in projects]).execute()
KimaiProject.insert_many(
[
{
"id": project.id,
"name": project.name,
"customer_id": project.customer.id,
}
for project in projects
]
).execute()
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()
KimaiActivity.insert_many(
[
{
"id": activity.id,
"name": activity.name,
"project_id": (
activity.project and activity.project.id or None
),
}
for activity in activities
]
).execute()
self._refresh()
@ -321,9 +558,9 @@ class HamsterToolsApp(App):
db.init("hamster-testing.db")
self.MODES = {
"categories": CategoriesScreen(),
"activities": ActivitiesScreen(),
"kimai": KimaiScreen(),
"categories": CategoryListScreen(),
"activities": ActivityListScreen(),
"kimai": KimaiProjectListScreen(),
}
super().__init__()

View File

@ -13,3 +13,34 @@ DataTable:focus .datatable--cursor {
#filter {
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
from peewee import SqliteDatabase, Model, CharField, ForeignKeyField
from peewee import SqliteDatabase, Model, CharField, ForeignKeyField, DateTimeField
from textual.logging import TextualHandler
logger = logging.getLogger('peewee')
logger = logging.getLogger("peewee")
logger.addHandler(TextualHandler())
logger.setLevel(logging.DEBUG)
@ -15,24 +15,27 @@ class HamsterCategory(Model):
class Meta:
database = db
table_name = 'categories'
table_name = "categories"
class HamsterActivity(Model):
name = CharField()
category = ForeignKeyField(HamsterCategory, backref='activities')
category = ForeignKeyField(HamsterCategory, backref="activities")
class Meta:
database = db
table_name = 'activities'
table_name = "activities"
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:
database = db
table_name = 'facts'
table_name = "facts"
class KimaiCustomer(Model):
@ -40,35 +43,35 @@ class KimaiCustomer(Model):
class Meta:
database = db
table_name = 'kimai_customers'
table_name = "kimai_customers"
class KimaiProject(Model):
name = CharField()
customer = ForeignKeyField(KimaiCustomer, backref='projects')
customer = ForeignKeyField(KimaiCustomer, backref="projects")
class Meta:
database = db
table_name = 'kimai_projects'
table_name = "kimai_projects"
class KimaiActivity(Model):
name = CharField()
project = ForeignKeyField(KimaiProject, backref='activities', null=True)
project = ForeignKeyField(KimaiProject, backref="activities", null=True)
class Meta:
database = db
table_name = 'kimai_activities'
table_name = "kimai_activities"
class HamsterKimaiMapping(Model):
hamster_activity = ForeignKeyField(HamsterActivity, backref='mappings')
kimai_customer = ForeignKeyField(KimaiCustomer, backref='mappings')
kimai_project = ForeignKeyField(KimaiProject, backref='mappings')
kimai_activity = ForeignKeyField(KimaiActivity, backref='mappings')
hamster_activity = ForeignKeyField(HamsterActivity, backref="mappings")
kimai_customer = ForeignKeyField(KimaiCustomer, backref="mappings")
kimai_project = ForeignKeyField(KimaiProject, backref="mappings")
kimai_activity = ForeignKeyField(KimaiActivity, backref="mappings")
kimai_description = CharField()
kimai_tags = CharField()
class Meta:
database = db
table_name = 'hamster_kimai_mappings'
table_name = "hamster_kimai_mappings"