Compare commits

..

3 Commits

Author SHA1 Message Date
jpt
855d9ee0f8 remove categories; not sure about summaries rn 2025-04-11 21:58:20 -05:00
jpt
3c422f363a moving from category to project 2025-04-11 21:49:14 -05:00
jpt
fa1534e083 field loading dynamic based on toml too 2025-04-11 20:06:54 -05:00
9 changed files with 50 additions and 115 deletions

View File

@ -40,7 +40,7 @@ def generate_needed_tasks():
to_create.append( to_create.append(
{ {
"text": g.template.format(next=next), "text": g.template.format(next=next),
"category": "recurring", "project": "recurring",
"due": next, "due": next,
} }
) )

View File

@ -6,11 +6,6 @@ from ..db import db, Task, Category, SavedSearch
from .. import config from .. import config
def category_lookup(category):
if category:
category, _ = Category.get_or_create(name=category)
return category.id
def get_task(item_id: int) -> Task: def get_task(item_id: int) -> Task:
return Task.get_by_id(item_id) return Task.get_by_id(item_id)
@ -18,19 +13,18 @@ def get_task(item_id: int) -> Task:
def add_task( def add_task(
text: str, text: str,
category: str,
status: str, status: str,
due: datetime | None = None, due: datetime | None = None,
type: str = "", type: str = "",
project: str = "",
) -> Task: ) -> Task:
""" """
Add a new task to the database. Add a new task to the database.
Returns the created task instance. Returns the created task instance.
""" """
with db.atomic(): with db.atomic():
category_id = category_lookup(category)
task = Task.create( task = Task.create(
text=text, type=type, status=status, due=due, category_id=category_id text=text, type=type, status=status, due=due, project=project
) )
return task return task
@ -40,8 +34,6 @@ def update_task(
**kwargs, **kwargs,
) -> Task: ) -> Task:
with db.atomic(): with db.atomic():
if category := kwargs.pop("category", None):
kwargs["category_id"] = category_lookup(category)
query = Task.update(kwargs).where(Task.id == item_id) query = Task.update(kwargs).where(Task.id == item_id)
query.execute() query.execute()
task = Task.get_by_id(item_id) task = Task.get_by_id(item_id)
@ -82,18 +74,18 @@ def _parse_sort_string(sort_string, status_order):
def get_tasks( def get_tasks(
search_text: str | None = None, search_text: str | None = None,
category: int | None = None,
statuses: tuple[str] | None = None, statuses: tuple[str] | None = None,
projects: tuple[str] | None = None,
sort: str = "", sort: str = "",
) -> list[Task]: ) -> list[Task]:
query = Task.select().where(~Task.deleted) query = Task.select().where(~Task.deleted)
if search_text: if search_text:
query = query.where(fn.Lower(Task.text).contains(search_text.lower())) query = query.where(fn.Lower(Task.text).contains(search_text.lower()))
if category:
query = query.where(Task.category == Category.get(name=category))
if statuses: if statuses:
query = query.where(Task.status.in_(statuses)) query = query.where(Task.status.in_(statuses))
if 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)

View File

@ -26,19 +26,13 @@ class BaseModel(Model):
database = db database = db
class Category(BaseModel):
name = CharField(unique=True)
def __str__(self):
return self.name
class Task(BaseModel): class Task(BaseModel):
text = TextField() text = TextField()
status = CharField() status = CharField()
due = DateTimeField(null=True) due = DateTimeField(null=True)
category = ForeignKeyField(Category, backref="tasks", null=True)
type = CharField() type = CharField()
project = CharField()
created_at = DateTimeField(default=datetime.now) created_at = DateTimeField(default=datetime.now)
updated_at = DateTimeField(default=datetime.now) updated_at = DateTimeField(default=datetime.now)
deleted = BooleanField(default=False) deleted = BooleanField(default=False)
@ -111,7 +105,5 @@ class TaskGenerator(BaseModel):
def initialize_db(): def initialize_db():
db.connect() db.connect()
db.create_tables([Category, Task, SavedSearch, TaskGenerator]) db.create_tables([Task, SavedSearch, TaskGenerator])
if not Category.select().exists():
Category.create(name="default")
db.close() db.close()

View File

@ -1,43 +0,0 @@
import csv
from datetime import datetime
from tt.db import initialize_db, Task, Category
from tt.config import STATUSES
def import_tasks_from_csv(filename: str):
initialize_db()
with open(filename, "r") as f:
reader = csv.DictReader(f)
for row in reader:
# Parse the due date if it exists and isn't empty
due_date = None
if row["due"] and row["due"].strip():
try:
due_date = datetime.strptime(row["due"].strip(), "%Y-%m-%d")
except ValueError:
print(f"Warning: Couldn't parse '{row['due']}', skipping due date")
# Validate status
status = row["status"].lower() if row["status"] else "zero"
if status not in STATUSES:
print(f"Warning: Invalid status '{status}', defaulting to 'zero'")
status = "zero"
category_id = None
if row["category"].strip():
category_name = row["category"].strip()
category, _ = Category.get_or_create(
name=category_name, defaults={"description": None}
)
category_id = category.id
Task.create(
text=row["task"],
type=row["type"],
status=status,
due=due_date,
category_id=category_id,
)
print(f"Imported task: {row['task']}")

View File

@ -13,6 +13,8 @@ from ..utils import (
remove_rich_tag, remove_rich_tag,
filter_to_string, filter_to_string,
get_text_from_editor, get_text_from_editor,
get_color_enum,
get_colored_date,
) )
from .keymodal import KeyModal from .keymodal import KeyModal
from .modals import ChoiceModal, DateModal, ConfirmModal from .modals import ChoiceModal, DateModal, ConfirmModal
@ -50,6 +52,12 @@ class TableColumnConfig:
# default edit mode # default edit mode
app._show_input("edit", current_value) app._show_input("edit", current_value)
def format_for_display(self, val):
val = str(val)
if "\n" in val:
val = val.split("\n")[0] + ELLIPSIS
return val
class EnumColumnConfig(TableColumnConfig): class EnumColumnConfig(TableColumnConfig):
def __init__(self, field: str, display_name: str, enum, **kwargs): def __init__(self, field: str, display_name: str, enum, **kwargs):
@ -62,6 +70,10 @@ class EnumColumnConfig(TableColumnConfig):
else: else:
raise NotifyValidationError(f"Invalid value {val}. Use: {list(self.enum)}") raise NotifyValidationError(f"Invalid value {val}. Use: {list(self.enum)}")
def format_for_display(self, val):
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)
@ -74,6 +86,9 @@ class DateColumnConfig(TableColumnConfig):
except ValueError: except ValueError:
raise NotifyValidationError("Invalid date format. Use YYYY-MM-DD") raise NotifyValidationError("Invalid date format. Use YYYY-MM-DD")
def format_for_display(self, val):
return get_colored_date(val)
def start_change(self, app, current_value): def start_change(self, app, current_value):
app.push_screen(DateModal(current_value), app.apply_change) app.push_screen(DateModal(current_value), app.apply_change)

View File

@ -61,7 +61,7 @@ class ChoiceModal(ModalScreen):
yield RadioSet( yield RadioSet(
*[ *[
RadioButton( RadioButton(
get_color_enum(e.value, config.STATUSES, "red"), value=self.selected == str(e.value) get_color_enum(e.value, config.STATUSES), value=self.selected == str(e.value)
) )
for e in self._enum for e in self._enum
] ]

View File

@ -1,7 +1,6 @@
import json import json
from textual.widgets import Input from textual.widgets import Input
from .. import config
from ..controller.tasks import ( from ..controller.tasks import (
get_task, get_task,
get_tasks, get_tasks,
@ -10,14 +9,7 @@ from ..controller.tasks import (
save_view, save_view,
get_saved_view, get_saved_view,
) )
from ..utils import ( from .editor import TableEditor
get_color_enum,
get_colored_date,
)
from .editor import (
TableEditor,
ELLIPSIS,
)
class TT(TableEditor): class TT(TableEditor):
@ -60,39 +52,34 @@ class TT(TableEditor):
self._load_view(event.value) self._load_view(event.value)
self.refresh_tasks(restore_cursor=False) self.refresh_tasks(restore_cursor=False)
self._hide_input() self._hide_input()
event.prevent_default()
# 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()
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,
category=self.filters.get("category"), projects=self.filters.get("project", "").split(","),
statuses=self.filters.get("status", "").split(",") statuses=self.filters.get("status", "").split(",")
if "status" in self.filters if "status" in self.filters
else None, else None,
sort=self.sort_string, sort=self.sort_string,
) )
for item in items: for item in items:
category = get_color_enum(
item.category.name if item.category else " - ",
config.PROJECTS,
"grey"
)
status = get_color_enum(item.status, config.STATUSES, "red")
due = get_colored_date(item.due)
if "\n" in item.text:
text = item.text.split("\n")[0] + ELLIPSIS
else:
text = item.text
self.table.add_row( self.table.add_row(
str(item.id), *self._db_item_to_row(item),
text,
status,
item.type,
due,
category,
key=str(item.id), key=str(item.id),
) )
@ -100,7 +87,3 @@ class TT(TableEditor):
def run(default_view): def run(default_view):
app = TT(default_view) app = TT(default_view)
app.run() app.run()
if __name__ == "__main__":
run()

View File

@ -7,10 +7,10 @@ import subprocess
def filter_to_string(filters, search_query): def filter_to_string(filters, search_query):
pieces = [] pieces = []
category = filters.get("category") project = filters.get("project")
status = filters.get("status") status = filters.get("status")
if category: if project:
pieces += [f" category:{category}"] pieces += [f" project:{project}"]
if status: if status:
pieces += [f" status:{status}"] pieces += [f" status:{status}"]
if search_query: if search_query:
@ -32,18 +32,11 @@ def advance_enum_val(enum_type, cur_val):
return members[next_idx] return members[next_idx]
def get_color_enum(value: str, enum: dict[str, dict], default: str) -> str: def get_color_enum(value: str, enum: dict[str, dict]) -> str:
color = enum.get(value, {"color": default})["color"] color = enum.get(value, {"color": "#ff0000"})["color"]
return f"[{color}]{value}[/]" return f"[{color}]{value}[/]"
def get_colored_category(category: str) -> str:
hash_val = sum(ord(c) for c in category)
hue = hash_val % 360
color = f"rgb({hue},200,200) on default"
return f"[{color}]{category}[/]"
def get_colored_date(date: datetime.date) -> str: def get_colored_date(date: datetime.date) -> str:
if not isinstance(date, datetime.date): if not isinstance(date, datetime.date):
return "" return ""

View File

@ -55,8 +55,11 @@ field_type = "date"
default = "" default = ""
[[views.columns]] [[views.columns]]
field_name = "category" field_name = "project"
display_name = "Project"
default = "" default = ""
field_type = "enum"
enum = "projects"
[[views]] [[views]]
name = "recurring" name = "recurring"