use toml for more
This commit is contained in:
parent
855d9ee0f8
commit
74ed6516b4
@ -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()
|
||||||
|
@ -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}")
|
||||||
|
|
||||||
|
@ -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]:
|
||||||
"""
|
"""
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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()
|
||||||
|
@ -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"),
|
||||||
]
|
]
|
||||||
|
@ -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,
|
||||||
|
8
tt.toml
8
tt.toml
@ -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"
|
||||||
|
Loading…
Reference in New Issue
Block a user