commenting/cleanup
This commit is contained in:
parent
b974b2f386
commit
913b56da99
@ -13,13 +13,17 @@ app = typer.Typer()
|
|||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
def new(
|
def new(
|
||||||
|
type: str,
|
||||||
name: Annotated[str, typer.Option("-n", "--name", help="name")] = "",
|
name: Annotated[str, typer.Option("-n", "--name", help="name")] = "",
|
||||||
category: Annotated[str, typer.Option("-c", "--category", help="name")] = "",
|
category: Annotated[str, typer.Option("-c", "--category", help="name")] = "",
|
||||||
due: Annotated[str, typer.Option("-d", "--due", help="due")] = "",
|
due: Annotated[str, typer.Option("-d", "--due", help="due")] = "",
|
||||||
url: Annotated[str, typer.Option("-u", "--url", help="URL")] = "",
|
url: Annotated[str, typer.Option("-u", "--url", help="URL")] = "",
|
||||||
):
|
):
|
||||||
|
"""
|
||||||
|
Add new thing without opening TUI.
|
||||||
|
"""
|
||||||
initialize_db()
|
initialize_db()
|
||||||
required = ["name", "category"]
|
required = ["name"]
|
||||||
|
|
||||||
if url and not name:
|
if url and not name:
|
||||||
resp = httpx.get(url, follow_redirects=True)
|
resp = httpx.get(url, follow_redirects=True)
|
||||||
@ -37,7 +41,7 @@ def new(
|
|||||||
# due = typer.prompt("Due (YYYY-MM-DD):")
|
# due = typer.prompt("Due (YYYY-MM-DD):")
|
||||||
# TODO: validate/allow blank
|
# TODO: validate/allow blank
|
||||||
|
|
||||||
add_thing(name, category, due)
|
add_thing(type=type, text=name, category=category, due=due)
|
||||||
typer.echo("Created new thing!")
|
typer.echo("Created new thing!")
|
||||||
|
|
||||||
|
|
||||||
@ -45,6 +49,9 @@ def new(
|
|||||||
def table(
|
def table(
|
||||||
view: Annotated[str, typer.Option("-v", "--view", help="saved view")] = "tasks",
|
view: Annotated[str, typer.Option("-v", "--view", help="saved view")] = "tasks",
|
||||||
):
|
):
|
||||||
|
"""
|
||||||
|
Open default table view.
|
||||||
|
"""
|
||||||
initialize_db()
|
initialize_db()
|
||||||
things_tui(view)
|
things_tui(view)
|
||||||
|
|
||||||
|
@ -1,3 +1,10 @@
|
|||||||
|
"""
|
||||||
|
Actual constants, used minimally.
|
||||||
|
|
||||||
|
Most belong in config.py so they can be overriden.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# special purpose for certain dates
|
||||||
SPECIAL_DATES_PIECES = {
|
SPECIAL_DATES_PIECES = {
|
||||||
"future": (3000, 1, 1),
|
"future": (3000, 1, 1),
|
||||||
"unclassified": (1999, 1, 1),
|
"unclassified": (1999, 1, 1),
|
||||||
|
@ -1,3 +1,20 @@
|
|||||||
|
"""
|
||||||
|
Controller layer for Things.
|
||||||
|
|
||||||
|
This layer can rely on `peewee` but no layer above it may.
|
||||||
|
|
||||||
|
For TUI Editor, each controller must have functions to:
|
||||||
|
|
||||||
|
- get_one(int) -> Obj
|
||||||
|
- get_many(search_text, filters, sort) -> list[Obj]
|
||||||
|
- add(**kwargs) -> Obj
|
||||||
|
- update(id, deleted, **kwargs) -> Obj
|
||||||
|
|
||||||
|
(names may vary!)
|
||||||
|
|
||||||
|
These will be exposed directly to editor TUI (and web UI).
|
||||||
|
"""
|
||||||
|
|
||||||
from peewee import fn
|
from peewee import fn
|
||||||
from peewee import Case, Value
|
from peewee import Case, Value
|
||||||
from ..db import db, Thing
|
from ..db import db, Thing
|
||||||
@ -27,6 +44,10 @@ def update_thing(
|
|||||||
type: str | None = None,
|
type: str | None = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> Thing:
|
) -> Thing:
|
||||||
|
"""
|
||||||
|
Updates a thing in database, treating deleted & type as special params
|
||||||
|
and everything else goes into the data JSON.
|
||||||
|
"""
|
||||||
with db.atomic():
|
with db.atomic():
|
||||||
thing = Thing.get_by_id(item_id)
|
thing = Thing.get_by_id(item_id)
|
||||||
updates = {"data": thing.data | kwargs}
|
updates = {"data": thing.data | kwargs}
|
||||||
@ -80,6 +101,13 @@ def get_things(
|
|||||||
filters: dict[str, str] | None = None,
|
filters: dict[str, str] | None = None,
|
||||||
sort: str = "",
|
sort: str = "",
|
||||||
) -> list[Thing]:
|
) -> list[Thing]:
|
||||||
|
"""
|
||||||
|
Main query function.
|
||||||
|
|
||||||
|
- search_text: fuzzy match strings against text field
|
||||||
|
- filters: exact field lookups (will use attribs then data elements)
|
||||||
|
- sort: comma-separated sort string specifying sort parameters
|
||||||
|
"""
|
||||||
query = Thing.select().where(~Thing.deleted)
|
query = Thing.select().where(~Thing.deleted)
|
||||||
|
|
||||||
if search_text:
|
if search_text:
|
||||||
|
54
src/tt/db.py
54
src/tt/db.py
@ -1,3 +1,7 @@
|
|||||||
|
"""
|
||||||
|
Defines the core data models in peewee ORM
|
||||||
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from datetime import date, timedelta, datetime
|
from datetime import date, timedelta, datetime
|
||||||
from xdg_base_dirs import xdg_data_home
|
from xdg_base_dirs import xdg_data_home
|
||||||
@ -11,9 +15,7 @@ from peewee import (
|
|||||||
)
|
)
|
||||||
from playhouse.sqlite_ext import JSONField
|
from playhouse.sqlite_ext import JSONField
|
||||||
|
|
||||||
# This module defines the core data types.
|
# Global DB connection
|
||||||
#
|
|
||||||
|
|
||||||
db = SqliteDatabase(
|
db = SqliteDatabase(
|
||||||
xdg_data_home() / "tt/tt.db",
|
xdg_data_home() / "tt/tt.db",
|
||||||
pragmas={
|
pragmas={
|
||||||
@ -30,6 +32,26 @@ class BaseModel(Model):
|
|||||||
|
|
||||||
|
|
||||||
class Thing(BaseModel):
|
class Thing(BaseModel):
|
||||||
|
"""
|
||||||
|
Thing uses sqlite as a sort of schema-less database.
|
||||||
|
|
||||||
|
- type corresponds to a table type in config.toml
|
||||||
|
- data is the JSON data, which adheres (loosely) to the schema defined for the given type
|
||||||
|
- also contains additional system-wide metadata fields tracked by app
|
||||||
|
|
||||||
|
Note: There are a few places in the code where an attribute of Thing is
|
||||||
|
accessed where the caller can't be sure if a field name is part of the
|
||||||
|
model or somewhere within data.
|
||||||
|
|
||||||
|
Therefore the lookup logic is almost always:
|
||||||
|
|
||||||
|
- check for attribute first, e.g. getattr(obj, "field")
|
||||||
|
- if the attribute does not exist, check for data["field"]
|
||||||
|
- if neither is present, raise an error
|
||||||
|
|
||||||
|
Therefore, `data` cannot contain properties with names used by attributes here.
|
||||||
|
"""
|
||||||
|
|
||||||
type = CharField()
|
type = CharField()
|
||||||
data = JSONField()
|
data = JSONField()
|
||||||
created_at = DateTimeField(default=datetime.now)
|
created_at = DateTimeField(default=datetime.now)
|
||||||
@ -37,15 +59,26 @@ class Thing(BaseModel):
|
|||||||
deleted = BooleanField(default=False)
|
deleted = BooleanField(default=False)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
|
# override of save method to ensure updated_at timestamp is always set
|
||||||
self.updated_at = datetime.now()
|
self.updated_at = datetime.now()
|
||||||
|
# schema enforcement could happen here as well, if/when needed
|
||||||
return super(Thing, self).save(*args, **kwargs)
|
return super(Thing, self).save(*args, **kwargs)
|
||||||
|
|
||||||
# @property
|
@property
|
||||||
# def due_week(self):
|
def due_week(self):
|
||||||
# return self.due.isocalendar()[1] - 12
|
# FIXME: this is an experimental hack for spring quarter
|
||||||
|
# ideally will have a way to define logic about this complex within config
|
||||||
|
return self.due.isocalendar()[1] - 12
|
||||||
|
|
||||||
|
|
||||||
class SavedSearch(BaseModel):
|
class SavedSearch(BaseModel):
|
||||||
|
"""
|
||||||
|
Saved search parameters.
|
||||||
|
|
||||||
|
Possibly to be removed in favor of config.toml saved searches?
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
name = CharField(unique=True)
|
name = CharField(unique=True)
|
||||||
filters = CharField()
|
filters = CharField()
|
||||||
sort_string = CharField()
|
sort_string = CharField()
|
||||||
@ -55,9 +88,14 @@ class SavedSearch(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class ThingGenerator(BaseModel):
|
class ThingGenerator(BaseModel):
|
||||||
|
"""
|
||||||
|
Abstract "thing generator" class, allows creation of recurring tasks
|
||||||
|
and other "things that make other things".
|
||||||
|
"""
|
||||||
|
|
||||||
template = CharField()
|
template = CharField()
|
||||||
type = CharField()
|
type = CharField()
|
||||||
config = TextField() # JSON
|
config = TextField() # TODO: JSON
|
||||||
deleted = BooleanField(default=False)
|
deleted = BooleanField(default=False)
|
||||||
last_generated_at = DateTimeField(null=True)
|
last_generated_at = DateTimeField(null=True)
|
||||||
created_at = DateTimeField(default=datetime.now)
|
created_at = DateTimeField(default=datetime.now)
|
||||||
@ -93,7 +131,7 @@ class ThingGenerator(BaseModel):
|
|||||||
if not self.last_generated_at or self.last_generated_at < maybe_next:
|
if not self.last_generated_at or self.last_generated_at < maybe_next:
|
||||||
return maybe_next
|
return maybe_next
|
||||||
|
|
||||||
# TODO: this doesn't handle if a month was missed somehow, just advances one
|
# FIXME: this doesn't handle if a month was missed somehow, just advances one
|
||||||
# same logic as above, if we're stepping another month forward
|
# same logic as above, if we're stepping another month forward
|
||||||
month += 1
|
month += 1
|
||||||
if month == 13:
|
if month == 13:
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
"""
|
||||||
|
Experimental sync code.
|
||||||
|
"""
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from .controller.things import get_things
|
from .controller.things import get_things
|
||||||
|
@ -1,136 +0,0 @@
|
|||||||
from textual.app import App, ComposeResult
|
|
||||||
from textual.containers import ScrollableContainer, Horizontal
|
|
||||||
from textual.widgets import DataTable, Label
|
|
||||||
from textual.binding import Binding
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from ..controller.summaries import (
|
|
||||||
get_category_summary,
|
|
||||||
get_recently_active,
|
|
||||||
get_due_soon,
|
|
||||||
)
|
|
||||||
|
|
||||||
####
|
|
||||||
# Due Soon # Task Summary
|
|
||||||
# #
|
|
||||||
# #
|
|
||||||
##################################
|
|
||||||
# WIP # Recently active
|
|
||||||
# #
|
|
||||||
# #
|
|
||||||
# #
|
|
||||||
|
|
||||||
|
|
||||||
class CategoryTable(DataTable):
|
|
||||||
"""Table showing category summaries"""
|
|
||||||
|
|
||||||
def on_mount(self):
|
|
||||||
self.add_columns(
|
|
||||||
"Category", "Zero", "WIP", "Blocked", "Done", "Overdue", "Due Soon"
|
|
||||||
)
|
|
||||||
self.refresh_data()
|
|
||||||
|
|
||||||
def refresh_data(self):
|
|
||||||
self.clear()
|
|
||||||
summaries = get_category_summary(10) # Show top 10 categories
|
|
||||||
for summary in summaries:
|
|
||||||
self.add_row(
|
|
||||||
summary["category"],
|
|
||||||
str(summary["tasks"]["zero"]),
|
|
||||||
str(summary["tasks"]["wip"]),
|
|
||||||
str(summary["tasks"]["blocked"]),
|
|
||||||
str(summary["tasks"]["done"]),
|
|
||||||
str(summary["tasks"]["overdue"]),
|
|
||||||
str(summary["tasks"]["due_soon"]),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def format_due_date(due_date: datetime | None) -> str:
|
|
||||||
if not due_date:
|
|
||||||
return "No due date"
|
|
||||||
return due_date.strftime("%Y-%m-%d")
|
|
||||||
|
|
||||||
|
|
||||||
class DueTaskList(DataTable):
|
|
||||||
"""Table showing upcoming and overdue tasks"""
|
|
||||||
|
|
||||||
def on_mount(self):
|
|
||||||
self.add_columns("Task", "Category", "Due Date")
|
|
||||||
self.refresh_data()
|
|
||||||
|
|
||||||
def refresh_data(self):
|
|
||||||
self.clear()
|
|
||||||
tasks = get_due_soon(10, all_overdue=True)
|
|
||||||
for task in tasks:
|
|
||||||
self.add_row(
|
|
||||||
task["text"],
|
|
||||||
task["category"],
|
|
||||||
format_due_date(task["due"]),
|
|
||||||
key=str(task["id"]),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class RecentTaskList(DataTable):
|
|
||||||
"""Table showing recently active tasks"""
|
|
||||||
|
|
||||||
def on_mount(self):
|
|
||||||
self.add_columns("Task", "Category", "Due Date")
|
|
||||||
self.refresh_data()
|
|
||||||
|
|
||||||
def refresh_data(self):
|
|
||||||
self.clear()
|
|
||||||
tasks = get_recently_active(10)
|
|
||||||
for task in tasks:
|
|
||||||
self.add_row(
|
|
||||||
task["text"],
|
|
||||||
task["category"] or "-",
|
|
||||||
format_due_date(task["due"]),
|
|
||||||
key=str(task["id"]),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Overview(App):
|
|
||||||
"""Task overview application"""
|
|
||||||
|
|
||||||
CSS = """
|
|
||||||
CategoryTable {
|
|
||||||
height: 40%;
|
|
||||||
margin: 1 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
Label {
|
|
||||||
align: center middle;
|
|
||||||
background: purple;
|
|
||||||
}
|
|
||||||
|
|
||||||
#lists {
|
|
||||||
height: 60%;
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
BINDINGS = [
|
|
||||||
Binding("q", "quit", "Quit"),
|
|
||||||
Binding("r", "refresh", "Refresh Data"),
|
|
||||||
]
|
|
||||||
|
|
||||||
def compose(self) -> ComposeResult:
|
|
||||||
with Horizontal(id="top"):
|
|
||||||
yield CategoryTable()
|
|
||||||
with Horizontal(id="lists"):
|
|
||||||
with ScrollableContainer():
|
|
||||||
yield Label("Due Soon")
|
|
||||||
yield DueTaskList()
|
|
||||||
with ScrollableContainer():
|
|
||||||
yield Label("Activity")
|
|
||||||
yield RecentTaskList()
|
|
||||||
|
|
||||||
def action_refresh(self):
|
|
||||||
"""Refresh all data in the tables"""
|
|
||||||
self.query_one(CategoryTable).refresh_data()
|
|
||||||
self.query_one(DueTaskList).refresh_data()
|
|
||||||
self.query_one(RecentTaskList).refresh_data()
|
|
||||||
|
|
||||||
|
|
||||||
def run():
|
|
||||||
app = Overview()
|
|
||||||
app.run()
|
|
@ -1,3 +1,10 @@
|
|||||||
|
"""
|
||||||
|
General purpose utilities.
|
||||||
|
|
||||||
|
Most of these utilities have to do with TUI. Perhaps this becomes tui/utils
|
||||||
|
once webui is ready.
|
||||||
|
"""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import os
|
import os
|
||||||
import datetime
|
import datetime
|
||||||
@ -7,6 +14,9 @@ from .constants import SPECIAL_DATES_DISPLAY
|
|||||||
|
|
||||||
|
|
||||||
def filter_to_string(filters, search_query):
|
def filter_to_string(filters, search_query):
|
||||||
|
"""
|
||||||
|
Format/summarize search fields for display.
|
||||||
|
"""
|
||||||
pieces = []
|
pieces = []
|
||||||
project = filters.get("project")
|
project = filters.get("project")
|
||||||
status = filters.get("status")
|
status = filters.get("status")
|
||||||
@ -19,14 +29,16 @@ def filter_to_string(filters, search_query):
|
|||||||
return "".join(pieces)
|
return "".join(pieces)
|
||||||
|
|
||||||
|
|
||||||
def remove_rich_tag(text):
|
def remove_rich_tag(text: str) -> str:
|
||||||
"""remove rich styling from a string"""
|
"""remove rich styling from a string"""
|
||||||
pattern = r"\[[^\]]*\](.*?)\[/\]"
|
pattern = r"\[[^\]]*\](.*?)\[/\]"
|
||||||
return re.sub(pattern, r"\1", text)
|
return re.sub(pattern, r"\1", text)
|
||||||
|
|
||||||
|
|
||||||
def advance_enum_val(enum_type, cur_val):
|
def advance_enum_val(enum_type, cur_val):
|
||||||
"""advance a value in an enum by one, wrapping around"""
|
"""
|
||||||
|
Advance a value in an enum by one position, wrap around if end is reached.
|
||||||
|
"""
|
||||||
members = [str(e.value) for e in enum_type]
|
members = [str(e.value) for e in enum_type]
|
||||||
cur_idx = members.index(remove_rich_tag(cur_val))
|
cur_idx = members.index(remove_rich_tag(cur_val))
|
||||||
next_idx = (cur_idx + 1) % len(members)
|
next_idx = (cur_idx + 1) % len(members)
|
||||||
@ -34,11 +46,20 @@ def advance_enum_val(enum_type, cur_val):
|
|||||||
|
|
||||||
|
|
||||||
def get_color_enum(value: str, enum: dict[str, dict]) -> str:
|
def get_color_enum(value: str, enum: dict[str, dict]) -> str:
|
||||||
|
"""
|
||||||
|
Use settings to color enumeration fields in UI.
|
||||||
|
"""
|
||||||
color = enum.get(value, {"color": "#ff0000"})["color"]
|
color = enum.get(value, {"color": "#ff0000"})["color"]
|
||||||
return f"[{color}]{value}[/]"
|
return f"[{color}]{value}[/]"
|
||||||
|
|
||||||
|
|
||||||
def get_colored_date(date: datetime.date) -> str:
|
def get_colored_date(date: datetime.date | str) -> str:
|
||||||
|
"""
|
||||||
|
Date coloring function -- heat approach.
|
||||||
|
|
||||||
|
TODO: refactor this into a set of rules that can be defined in TOML config
|
||||||
|
"""
|
||||||
|
# incoming date can be date or str, we need both
|
||||||
if isinstance(date, datetime.date):
|
if isinstance(date, datetime.date):
|
||||||
as_date = date
|
as_date = date
|
||||||
as_str = date.strftime("%Y-%m-%d")
|
as_str = date.strftime("%Y-%m-%d")
|
||||||
@ -46,7 +67,7 @@ def get_colored_date(date: datetime.date) -> str:
|
|||||||
as_date = datetime.datetime.strptime(date, "%Y-%m-%d").date()
|
as_date = datetime.datetime.strptime(date, "%Y-%m-%d").date()
|
||||||
as_str = as_date.strftime("%Y-%m-%d")
|
as_str = as_date.strftime("%Y-%m-%d")
|
||||||
else:
|
else:
|
||||||
return "~bad~"
|
return "~bad~" # so *something* will show up in UI & can be edited
|
||||||
|
|
||||||
if as_str in SPECIAL_DATES_DISPLAY:
|
if as_str in SPECIAL_DATES_DISPLAY:
|
||||||
return SPECIAL_DATES_DISPLAY[as_str]
|
return SPECIAL_DATES_DISPLAY[as_str]
|
||||||
@ -58,6 +79,7 @@ def get_colored_date(date: datetime.date) -> str:
|
|||||||
delta = as_date - today
|
delta = as_date - today
|
||||||
weeks = delta.days // 7
|
weeks = delta.days // 7
|
||||||
|
|
||||||
|
# red,orange,yellow,white gradient
|
||||||
colors = [
|
colors = [
|
||||||
# "#FF4000",
|
# "#FF4000",
|
||||||
"#FF8000",
|
"#FF8000",
|
||||||
@ -77,6 +99,11 @@ def get_colored_date(date: datetime.date) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def get_text_from_editor(initial_text: str = "") -> str | None:
|
def get_text_from_editor(initial_text: str = "") -> str | None:
|
||||||
|
"""
|
||||||
|
Function that launches $EDITOR with a field's text.
|
||||||
|
|
||||||
|
When editor is closed, will read file (if modified) and ingest edited text.
|
||||||
|
"""
|
||||||
editor = os.environ.get("EDITOR", "vim")
|
editor = os.environ.get("EDITOR", "vim")
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(suffix=".txt", mode="w+", delete=True) as tf:
|
with tempfile.NamedTemporaryFile(suffix=".txt", mode="w+", delete=True) as tf:
|
||||||
|
Loading…
Reference in New Issue
Block a user