Compare commits
3 Commits
f8b5399f2f
...
855d9ee0f8
Author | SHA1 | Date | |
---|---|---|---|
855d9ee0f8 | |||
3c422f363a | |||
fa1534e083 |
@ -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,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -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)
|
||||||
|
12
src/tt/db.py
12
src/tt/db.py
@ -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()
|
||||||
|
@ -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']}")
|
|
@ -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)
|
||||||
|
|
||||||
|
@ -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
|
||||||
]
|
]
|
||||||
|
@ -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()
|
|
||||||
|
@ -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 ""
|
||||||
|
5
tt.toml
5
tt.toml
@ -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"
|
||||||
|
Loading…
Reference in New Issue
Block a user