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

View File

@ -17,10 +17,10 @@ def get_enum(name):
raise ValueError(f"no such 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", []): for view in _load_config().get("views", []):
if view["name"] == name: if view["name"] == name:
return view["columns"] return view
raise ValueError(f"no such view! {name}") raise ValueError(f"no such view! {name}")

View File

@ -1,6 +1,6 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
from peewee import fn, JOIN from peewee import fn, JOIN
from ..db import Task, Category from ..db import Task
def get_category_summary(num: int = 5) -> list[dict]: def get_category_summary(num: int = 5) -> list[dict]:
@ -13,81 +13,7 @@ def get_category_summary(num: int = 5) -> list[dict]:
Returns: Returns:
List of dicts containing category name and task statistics List of dicts containing category name and task statistics
""" """
now = datetime.now() return []
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
]
def get_recently_active(num: int = 5, category: str | None = None) -> list[dict]: 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 datetime import datetime
from peewee import fn from peewee import fn
from peewee import Case, Value from peewee import Case, Value
from ..db import db, Task, Category, SavedSearch from ..db import db, Task, SavedSearch
from .. import config from .. import config
@ -84,8 +84,8 @@ def get_tasks(
query = query.where(fn.Lower(Task.text).contains(search_text.lower())) query = query.where(fn.Lower(Task.text).contains(search_text.lower()))
if statuses: if statuses:
query = query.where(Task.status.in_(statuses)) query = query.where(Task.status.in_(statuses))
if projects: #if projects:
query = query.where(Task.project.in_(projects)) # query = query.where(Task.project.in_(projects))
sort_expressions = _parse_sort_string(sort, statuses) sort_expressions = _parse_sort_string(sort, statuses)
query = query.order_by(*sort_expressions) query = query.order_by(*sort_expressions)
@ -93,10 +93,6 @@ def get_tasks(
return list(query) 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: def save_view(name: str, *, filters: dict, sort_string: str) -> SavedSearch:
filters_json = json.dumps(filters) filters_json = json.dumps(filters)

View File

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

View File

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

View File

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

View File

@ -21,14 +21,12 @@ class TT(TableEditor):
] ]
def __init__(self, default_view="default"): def __init__(self, default_view="default"):
super().__init__() super().__init__("tasks")
self._load_view(default_view)
self.update_item_callback = update_task self.update_item_callback = update_task
self.add_item_callback = add_task self.add_item_callback = add_task
self.get_item_callback = get_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: try:
saved = get_saved_view(name) saved = get_saved_view(name)
self.filters = json.loads(saved.filters) self.filters = json.loads(saved.filters)
@ -55,19 +53,6 @@ class TT(TableEditor):
# if event isn't handled here it will bubble to parent # if event isn't handled here it will bubble to parent
event.prevent_default() 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): def refresh_items(self):
items = get_tasks( items = get_tasks(
self.search_query, self.search_query,

View File

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