use toml for more

This commit is contained in:
jpt 2025-04-12 12:35:14 -05:00
parent 855d9ee0f8
commit 74ed6516b4
9 changed files with 53 additions and 129 deletions

View File

@ -3,7 +3,6 @@ import httpx
import lxml.html
from typing_extensions import Annotated
from .controller.tasks import add_task
from .import_csv import import_tasks_from_csv
from .db import initialize_db
from .tui.tasks import run as tasks_tui
from .tui.overview import run as overview_tui
@ -62,11 +61,5 @@ def overview():
overview_tui()
@app.command()
def import_csv(filename: str):
import_tasks_from_csv(filename)
print("Import complete!")
if __name__ == "__main__":
app()

View File

@ -17,10 +17,10 @@ def get_enum(name):
raise ValueError(f"no such enum! {name}")
def get_view_columns(name):
def get_view(name):
for view in _load_config().get("views", []):
if view["name"] == name:
return view["columns"]
return view
raise ValueError(f"no such view! {name}")

View File

@ -1,6 +1,6 @@
from datetime import datetime, timedelta
from peewee import fn, JOIN
from ..db import Task, Category
from ..db import Task
def get_category_summary(num: int = 5) -> list[dict]:
@ -13,81 +13,7 @@ def get_category_summary(num: int = 5) -> list[dict]:
Returns:
List of dicts containing category name and task statistics
"""
now = datetime.now()
week_from_now = now + timedelta(days=7)
# Subquery for overdue tasks
overdue_count = (
Task.select(Task.category, fn.COUNT(Task.id).alias("overdue"))
.where(
(~Task.deleted) & (Task.due < now) & (Task.status != "done")
)
.group_by(Task.category)
)
# Subquery for tasks due in next 7 days
due_soon_count = (
Task.select(Task.category, fn.COUNT(Task.id).alias("due_soon"))
.where(
(~Task.deleted)
& (Task.due >= now)
& (Task.due <= week_from_now)
& (Task.status != "done")
)
.group_by(Task.category)
)
# Main query joining all the information
query = (
Category.select(
Category.name,
fn.COALESCE(fn.SUM(Task.status == "zero"), 0).alias(
"zero_count"
),
fn.COALESCE(fn.SUM(Task.status == "wip"), 0).alias(
"wip_count"
),
fn.COALESCE(fn.SUM(Task.status == "blocked"), 0).alias(
"blocked_count"
),
fn.COALESCE(fn.SUM(Task.status == "done"), 0).alias(
"done_count"
),
fn.COALESCE(overdue_count.c.overdue, 0).alias("overdue"),
fn.COALESCE(due_soon_count.c.due_soon, 0).alias("due_soon"),
)
.join(Task, JOIN.LEFT_OUTER)
.join(
overdue_count,
JOIN.LEFT_OUTER,
on=(Category.id == overdue_count.c.category_id),
)
.join(
due_soon_count,
JOIN.LEFT_OUTER,
on=(Category.id == due_soon_count.c.category_id),
)
.where(~Task.deleted)
.group_by(Category.id)
.order_by(fn.COUNT(Task.id).desc())
.limit(num)
)
return [
{
"category": cat.name,
"tasks": {
"zero": cat.zero_count,
"wip": cat.wip_count,
"blocked": cat.blocked_count,
"done": cat.done_count,
"overdue": cat.overdue,
"due_soon": cat.due_soon,
},
}
for cat in query
]
return []
def get_recently_active(num: int = 5, category: str | None = None) -> list[dict]:
"""

View File

@ -2,7 +2,7 @@ import json
from datetime import datetime
from peewee import fn
from peewee import Case, Value
from ..db import db, Task, Category, SavedSearch
from ..db import db, Task, SavedSearch
from .. import config
@ -84,8 +84,8 @@ def get_tasks(
query = query.where(fn.Lower(Task.text).contains(search_text.lower()))
if statuses:
query = query.where(Task.status.in_(statuses))
if projects:
query = query.where(Task.project.in_(projects))
#if projects:
# query = query.where(Task.project.in_(projects))
sort_expressions = _parse_sort_string(sort, statuses)
query = query.order_by(*sort_expressions)
@ -93,10 +93,6 @@ def get_tasks(
return list(query)
def get_categories() -> list[Category]:
return list(Category.select().order_by(Category.name))
def save_view(name: str, *, filters: dict, sort_string: str) -> SavedSearch:
filters_json = json.dumps(filters)

View File

@ -1,11 +1,9 @@
import json
from datetime import date, timedelta, datetime
from enum import Enum
from peewee import (
BooleanField,
CharField,
DateTimeField,
ForeignKeyField,
Model,
SqliteDatabase,
TextField,

View File

@ -24,6 +24,8 @@ class NotifyValidationError(Exception):
"""will notify and continue if raised"""
ELLIPSIS = ""
class TableColumnConfig:
def __init__(
self,
@ -73,7 +75,6 @@ class EnumColumnConfig(TableColumnConfig):
def format_for_display(self, val):
return get_color_enum(val, self.enum)
def start_change(self, app, current_value):
# a weird hack? pass app here and correct modal gets pushed
app.push_screen(ChoiceModal(self.enum, current_value), app.apply_change)
@ -93,7 +94,6 @@ class DateColumnConfig(TableColumnConfig):
app.push_screen(DateModal(current_value), app.apply_change)
ELLIPSIS = ""
class TableEditor(App):
@ -167,18 +167,25 @@ class TableEditor(App):
("?", "show_keys", "show keybindings"),
]
def __init__(self):
def __init__(self, editor_config: str):
super().__init__()
self.filters = {}
self.sort_string = "" # TODO: default sort
self._load_config(editor_config)
self.search_query = ""
self.saved_cursor_pos = (1, 0)
self.save_on_move = None
def _load_config(self, name):
# column config
columns = []
for col in config.get_view_columns(name):
"""
Reads configuration from TOML and sets up:
- table_config
- filters
- sort_string
"""
self.table_config = []
view = config.get_view(name)
# set up columns
for col in view["columns"]:
field_type = col.get("field_type", "text")
field_cls = {
"text": TableColumnConfig,
@ -196,8 +203,24 @@ class TableEditor(App):
extras["enable_editor"] = True
cc = field_cls(field_name, display_name, default=default, **extras)
columns.append(cc)
return columns
self.table_config.append(cc)
# load default filters
self.filters = view["filters"]
self.sort_string = view["sort"]
def _db_item_to_row(self, item):
"""
Convert db item to a row for display.
"""
row = []
for col in self.table_config:
val = getattr(item, col.field)
display_val = col.format_for_display(val)
row.append(display_val)
return row
def compose(self):
self.header = Header()

View File

@ -45,10 +45,9 @@ class ChoiceModal(ModalScreen):
"""
BINDINGS = [
("j", "cursor_down", "Down"),
("j,tab", "cursor_down", "Down"),
("k", "cursor_up", "Up"),
Binding("enter", "select", "Select", priority=True),
("c", "select", "Select"),
Binding("enter", "select", "Select", priority=False),
("escape", "cancel", "cancel"),
]
@ -75,7 +74,8 @@ class ChoiceModal(ModalScreen):
def action_select(self):
rs = self.query_one(RadioSet)
rs.action_toggle_button()
# TODO: this doesn't work
#rs.action_toggle_button()
pressed = rs.pressed_button
self.dismiss(str(pressed.label))
@ -101,9 +101,8 @@ class DateModal(ModalScreen):
BINDINGS = [
("j", "cursor_down", "Down"),
("k", "cursor_up", "Up"),
("h", "cursor_left", "Left"),
("h,tab", "cursor_left", "Left"),
("l", "cursor_right", "Right"),
# ("0,1,2,3,4,5,6,7,8,9", "num_entry", "#"),
Binding("enter", "select", "Select", priority=True),
("escape", "cancel", "cancel"),
]

View File

@ -21,14 +21,12 @@ class TT(TableEditor):
]
def __init__(self, default_view="default"):
super().__init__()
self._load_view(default_view)
super().__init__("tasks")
self.update_item_callback = update_task
self.add_item_callback = add_task
self.get_item_callback = get_task
self.table_config = self._load_config("tasks")
def _load_view(self, name):
def _load_db_view(self, name):
try:
saved = get_saved_view(name)
self.filters = json.loads(saved.filters)
@ -55,19 +53,6 @@ class TT(TableEditor):
# if event isn't handled here it will bubble to parent
event.prevent_default()
def _db_item_to_row(self, item):
"""
convert db item to an item
"""
row = []
for col in self.table_config:
val = getattr(item, col.field)
display_val = col.format_for_display(val)
row.append(display_val)
return row
def refresh_items(self):
items = get_tasks(
self.search_query,

View File

@ -2,8 +2,8 @@
name = "status"
values = [
{ value = "zero", color = "#666666" },
{ value = "blocked", color = "#33a99" },
{ value = "wip", color = "#cc9900" },
{ value = "blocked", color = "#cc9900" },
{ value = "wip", color = "#33aa99" },
{ value = "done", color = "#009900" },
]
@ -28,6 +28,10 @@ values = [
[[views]]
name = "tasks"
sort = "due"
[views.filters]
status = "wip,blocked,zero"
[[views.columns]]
field_name = "id"