diff --git a/src/tt/controller/generators.py b/src/tt/controller/generators.py index 943614a..2bad8b3 100644 --- a/src/tt/controller/generators.py +++ b/src/tt/controller/generators.py @@ -31,8 +31,10 @@ def update_generator( item_id: int, **kwargs, ) -> TaskGenerator: - config = {"val": kwargs.pop("val")} - kwargs["config"] = json.dumps(config) + # replace "val" with JSON + if "val" in kwargs: + config = {"val": kwargs.pop("val")} + kwargs["config"] = json.dumps(config) with db.atomic(): query = TaskGenerator.update(kwargs).where(TaskGenerator.id == item_id) query.execute() diff --git a/src/tt/db.py b/src/tt/db.py index 3407d91..9652700 100644 --- a/src/tt/db.py +++ b/src/tt/db.py @@ -1,4 +1,5 @@ -from datetime import datetime, timedelta +import json +from datetime import date, timedelta, datetime from enum import Enum from peewee import ( BooleanField, @@ -46,7 +47,6 @@ class Category(BaseModel): class Task(BaseModel): - # TODO: rename name text = TextField() status = CharField( choices=[(status.value, status.name) for status in TaskStatus], @@ -81,32 +81,51 @@ class TaskGenerator(BaseModel): last_generated_at = DateTimeField(null=True) created_at = DateTimeField(default=datetime.now) - def should_generate(self) -> bool: + def next_at(self) -> datetime.date: if self.deleted: + return None + + today = date.today() + val = int(json.loads(self.config)["val"]) + + if self.type == GeneratorType.DAYS_BETWEEN.value: + if not self.last_generated_at: + return today + return self.last_generated_at + timedelta(days=val) + elif self.type == GeneratorType.MONTHLY.value: + year, month, day = today.year, today.month, today.day + day_of_month = val + + if day_of_month < day: + month += 1 + if month == 13: + month = 1 + year += 1 + + if day_of_month >= 29 and month == 2: + maybe_next = date(year, month, 28) + else: + maybe_next = date(year, month, day_of_month) + + if not self.last_generated_at or self.last_generated_at < maybe_next: + return maybe_next + + # need to go forward one more month + month += 1 + if month == 13: + month = 1 + year += 1 + if day_of_month >= 29 and month == 2: + maybe_next = date(year, month, 28) + else: + maybe_next = date(year, month, day_of_month) + return maybe_next + + def should_generate(self) -> bool: + next = self.next_at() + if not next: return False - if not self.last_generated_at: - return True - - now = datetime.now() - if self.type == GeneratorType.DAYS_BETWEEN: - days_between = self.config["val"] - days_since = (now - self.last_generated_at).days - return days_since >= days_between - - elif self.type == GeneratorType.MONTHLY: - day_of_month = self.config["val"] - - # check each day until now to see if target day occurred - one_day = timedelta(days=1) - check_date = self.last_generated_at + one_day - while check_date <= now: - if check_date.day == day_of_month: - return True - check_date += one_day - - return False - - return False + return next <= date.today() def initialize_db(): diff --git a/src/tt/tui/editor.py b/src/tt/tui/editor.py index 44dc37f..cf4cba6 100644 --- a/src/tt/tui/editor.py +++ b/src/tt/tui/editor.py @@ -42,17 +42,19 @@ class TableColumnConfig: display_name: str, *, default=None, - enable_editor=False, enum=None, - filterable=True, preprocessor=None, + enable_editor=False, + filterable=True, + read_only=False, ): self.field = field self.display_name = display_name self.default = default - self.enable_editor = enable_editor self.enum = enum + self.enable_editor = enable_editor self.filterable = filterable + self.read_only = read_only if preprocessor: self.preprocessor = preprocessor elif self.enum: @@ -223,6 +225,8 @@ class TableEditor(App): # prepopulate with either the field default or the current filter prepopulated = {} for fc in self.TABLE_CONFIG: + if fc.read_only: + continue val = self.filters.get(fc.field, fc.default) if val is not None: # enums use comma separated filters @@ -320,16 +324,21 @@ class TableEditor(App): self.saved_cursor_pos = (self.table.cursor_row, self.table.cursor_column) def action_start_change(self): + # invalid position if self.table.cursor_row is None or self.table.cursor_column == 0: return + # check if editable + cconf = self._active_column_config() + if cconf.read_only: + return + self._save_cursor() current_value = self.table.get_cell_at( (self.table.cursor_row, self.table.cursor_column) ) if current_value.endswith(ELLIPSIS): - # TODO: flash message? - # need to edit with e + self.notify("multi-line text, use (e)dit") return current_value = remove_rich_tag(current_value) self._show_input("edit", current_value) diff --git a/src/tt/tui/recurring.py b/src/tt/tui/recurring.py index 7135263..0a9c6de 100644 --- a/src/tt/tui/recurring.py +++ b/src/tt/tui/recurring.py @@ -1,5 +1,4 @@ import json -from datetime import datetime from ..controller.generators import ( get_generator, @@ -11,17 +10,9 @@ from ..db import GeneratorType from .editor import ( TableEditor, TableColumnConfig, - NotifyValidationError, ) -def due_preprocessor(val): - try: - return datetime.strptime(val, "%Y-%m-%d") - except ValueError: - raise NotifyValidationError("Invalid date format. Use YYYY-MM-DD") - - class TaskGenEditor(TableEditor): TABLE_CONFIG = ( TableColumnConfig("id", "ID"), @@ -32,7 +23,8 @@ class TaskGenEditor(TableEditor): default=GeneratorType.DAYS_BETWEEN.value, enum=GeneratorType, ), - TableColumnConfig("val", "Value", default=""), + TableColumnConfig("val", "Value", default="1"), + TableColumnConfig("next_at", "Next @", default="", read_only=True), ) def __init__(self): @@ -45,14 +37,14 @@ class TaskGenEditor(TableEditor): items = get_generators() for item in items: self.table.add_row( - str(item.id), item.template, item.type, json.loads(item.config)["val"] + str(item.id), + item.template, + item.type, + json.loads(item.config)["val"], + str(item.next_at()), ) def run(): app = TaskGenEditor() app.run() - - -if __name__ == "__main__": - run()