Compare commits
No commits in common. "ccd0178de1e9c8188ee7b0745a29e1fc7ab8fb2d" and "4980b2e7e522cdca56564d2545604687df5dd9d2" have entirely different histories.
ccd0178de1
...
4980b2e7e5
@ -1,7 +1,5 @@
|
|||||||
import json
|
|
||||||
from datetime import date, timedelta
|
|
||||||
from ..db import db, TaskGenerator, GeneratorType
|
from ..db import db, TaskGenerator, GeneratorType
|
||||||
from .tasks import add_task
|
import json
|
||||||
|
|
||||||
|
|
||||||
def get_generator(item_id: int) -> TaskGenerator:
|
def get_generator(item_id: int) -> TaskGenerator:
|
||||||
@ -29,36 +27,12 @@ def add_generator(
|
|||||||
return task
|
return task
|
||||||
|
|
||||||
|
|
||||||
def generate_needed_tasks():
|
|
||||||
to_create = []
|
|
||||||
for g in get_generators():
|
|
||||||
next = g.next_at()
|
|
||||||
if not next:
|
|
||||||
continue
|
|
||||||
# TODO: make configurable
|
|
||||||
if date.today() - next > timedelta(days=14):
|
|
||||||
to_create.append(
|
|
||||||
{
|
|
||||||
"text": g.template.format(next=next),
|
|
||||||
"category": "recurring",
|
|
||||||
"due": next,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
for c in to_create:
|
|
||||||
add_task(**c)
|
|
||||||
|
|
||||||
return to_create
|
|
||||||
|
|
||||||
|
|
||||||
def update_generator(
|
def update_generator(
|
||||||
item_id: int,
|
item_id: int,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> TaskGenerator:
|
) -> TaskGenerator:
|
||||||
# replace "val" with JSON
|
config = {"val": kwargs.pop("val")}
|
||||||
if "val" in kwargs:
|
kwargs["config"] = json.dumps(config)
|
||||||
config = {"val": kwargs.pop("val")}
|
|
||||||
kwargs["config"] = json.dumps(config)
|
|
||||||
with db.atomic():
|
with db.atomic():
|
||||||
query = TaskGenerator.update(kwargs).where(TaskGenerator.id == item_id)
|
query = TaskGenerator.update(kwargs).where(TaskGenerator.id == item_id)
|
||||||
query.execute()
|
query.execute()
|
||||||
|
64
src/tt/db.py
64
src/tt/db.py
@ -1,5 +1,4 @@
|
|||||||
import json
|
from datetime import datetime, timedelta
|
||||||
from datetime import date, timedelta, datetime
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from peewee import (
|
from peewee import (
|
||||||
BooleanField,
|
BooleanField,
|
||||||
@ -47,6 +46,7 @@ class Category(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class Task(BaseModel):
|
class Task(BaseModel):
|
||||||
|
# TODO: rename name
|
||||||
text = TextField()
|
text = TextField()
|
||||||
status = CharField(
|
status = CharField(
|
||||||
choices=[(status.value, status.name) for status in TaskStatus],
|
choices=[(status.value, status.name) for status in TaskStatus],
|
||||||
@ -81,48 +81,32 @@ class TaskGenerator(BaseModel):
|
|||||||
last_generated_at = DateTimeField(null=True)
|
last_generated_at = DateTimeField(null=True)
|
||||||
created_at = DateTimeField(default=datetime.now)
|
created_at = DateTimeField(default=datetime.now)
|
||||||
|
|
||||||
def next_at(self) -> datetime.date:
|
def should_generate(self) -> bool:
|
||||||
if self.deleted:
|
if self.deleted:
|
||||||
return None
|
return False
|
||||||
|
if not self.last_generated_at:
|
||||||
|
return True
|
||||||
|
|
||||||
today = date.today()
|
now = datetime.now()
|
||||||
val = int(json.loads(self.config)["val"])
|
if self.type == GeneratorType.DAYS_BETWEEN:
|
||||||
|
days_between = self.config["val"]
|
||||||
|
days_since = (now - self.last_generated_at).days
|
||||||
|
return days_since >= days_between
|
||||||
|
|
||||||
if self.type == GeneratorType.DAYS_BETWEEN.value:
|
elif self.type == GeneratorType.MONTHLY:
|
||||||
if not self.last_generated_at:
|
day_of_month = self.config["val"]
|
||||||
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 we need to go forward a month
|
# check each day until now to see if target day occurred
|
||||||
if day_of_month < day:
|
one_day = timedelta(days=1)
|
||||||
month += 1
|
check_date = self.last_generated_at + one_day
|
||||||
if month == 13:
|
while check_date <= now:
|
||||||
month = 1
|
if check_date.day == day_of_month:
|
||||||
year += 1
|
return True
|
||||||
|
check_date += one_day
|
||||||
|
|
||||||
# recurring tasks on 29-31 in Feb will just happen on 28th
|
return False
|
||||||
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 False
|
||||||
return maybe_next
|
|
||||||
|
|
||||||
# TODO: this doesn't handle if a month was missed somehow, just advances one
|
|
||||||
# same logic as above, if we're stepping another month forward
|
|
||||||
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 initialize_db():
|
def initialize_db():
|
||||||
@ -131,3 +115,7 @@ def initialize_db():
|
|||||||
if not Category.select().exists():
|
if not Category.select().exists():
|
||||||
Category.create(name="default")
|
Category.create(name="default")
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
initialize_db()
|
||||||
|
@ -42,19 +42,17 @@ class TableColumnConfig:
|
|||||||
display_name: str,
|
display_name: str,
|
||||||
*,
|
*,
|
||||||
default=None,
|
default=None,
|
||||||
enum=None,
|
|
||||||
preprocessor=None,
|
|
||||||
enable_editor=False,
|
enable_editor=False,
|
||||||
|
enum=None,
|
||||||
filterable=True,
|
filterable=True,
|
||||||
read_only=False,
|
preprocessor=None,
|
||||||
):
|
):
|
||||||
self.field = field
|
self.field = field
|
||||||
self.display_name = display_name
|
self.display_name = display_name
|
||||||
self.default = default
|
self.default = default
|
||||||
self.enum = enum
|
|
||||||
self.enable_editor = enable_editor
|
self.enable_editor = enable_editor
|
||||||
|
self.enum = enum
|
||||||
self.filterable = filterable
|
self.filterable = filterable
|
||||||
self.read_only = read_only
|
|
||||||
if preprocessor:
|
if preprocessor:
|
||||||
self.preprocessor = preprocessor
|
self.preprocessor = preprocessor
|
||||||
elif self.enum:
|
elif self.enum:
|
||||||
@ -225,8 +223,6 @@ class TableEditor(App):
|
|||||||
# prepopulate with either the field default or the current filter
|
# prepopulate with either the field default or the current filter
|
||||||
prepopulated = {}
|
prepopulated = {}
|
||||||
for fc in self.TABLE_CONFIG:
|
for fc in self.TABLE_CONFIG:
|
||||||
if fc.read_only:
|
|
||||||
continue
|
|
||||||
val = self.filters.get(fc.field, fc.default)
|
val = self.filters.get(fc.field, fc.default)
|
||||||
if val is not None:
|
if val is not None:
|
||||||
# enums use comma separated filters
|
# enums use comma separated filters
|
||||||
@ -324,21 +320,16 @@ class TableEditor(App):
|
|||||||
self.saved_cursor_pos = (self.table.cursor_row, self.table.cursor_column)
|
self.saved_cursor_pos = (self.table.cursor_row, self.table.cursor_column)
|
||||||
|
|
||||||
def action_start_change(self):
|
def action_start_change(self):
|
||||||
# invalid position
|
|
||||||
if self.table.cursor_row is None or self.table.cursor_column == 0:
|
if self.table.cursor_row is None or self.table.cursor_column == 0:
|
||||||
return
|
return
|
||||||
|
|
||||||
# check if editable
|
|
||||||
cconf = self._active_column_config()
|
|
||||||
if cconf.read_only:
|
|
||||||
return
|
|
||||||
|
|
||||||
self._save_cursor()
|
self._save_cursor()
|
||||||
current_value = self.table.get_cell_at(
|
current_value = self.table.get_cell_at(
|
||||||
(self.table.cursor_row, self.table.cursor_column)
|
(self.table.cursor_row, self.table.cursor_column)
|
||||||
)
|
)
|
||||||
if current_value.endswith(ELLIPSIS):
|
if current_value.endswith(ELLIPSIS):
|
||||||
self.notify("multi-line text, use (e)dit")
|
# TODO: flash message?
|
||||||
|
# need to edit with e
|
||||||
return
|
return
|
||||||
current_value = remove_rich_tag(current_value)
|
current_value = remove_rich_tag(current_value)
|
||||||
self._show_input("edit", current_value)
|
self._show_input("edit", current_value)
|
||||||
|
@ -1,19 +1,27 @@
|
|||||||
import json
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from ..controller.generators import (
|
from ..controller.generators import (
|
||||||
get_generator,
|
get_generator,
|
||||||
get_generators,
|
get_generators,
|
||||||
add_generator,
|
add_generator,
|
||||||
update_generator,
|
update_generator,
|
||||||
generate_needed_tasks,
|
|
||||||
)
|
)
|
||||||
from ..db import GeneratorType
|
from ..db import GeneratorType
|
||||||
from .editor import (
|
from .editor import (
|
||||||
TableEditor,
|
TableEditor,
|
||||||
TableColumnConfig,
|
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):
|
class TaskGenEditor(TableEditor):
|
||||||
TABLE_CONFIG = (
|
TABLE_CONFIG = (
|
||||||
TableColumnConfig("id", "ID"),
|
TableColumnConfig("id", "ID"),
|
||||||
@ -24,8 +32,7 @@ class TaskGenEditor(TableEditor):
|
|||||||
default=GeneratorType.DAYS_BETWEEN.value,
|
default=GeneratorType.DAYS_BETWEEN.value,
|
||||||
enum=GeneratorType,
|
enum=GeneratorType,
|
||||||
),
|
),
|
||||||
TableColumnConfig("val", "Value", default="1"),
|
TableColumnConfig("val", "Value", default=""),
|
||||||
TableColumnConfig("next_at", "Next @", default="", read_only=True),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@ -35,20 +42,17 @@ class TaskGenEditor(TableEditor):
|
|||||||
self.get_item_callback = get_generator
|
self.get_item_callback = get_generator
|
||||||
|
|
||||||
def refresh_items(self):
|
def refresh_items(self):
|
||||||
generated = generate_needed_tasks()
|
|
||||||
if num := len(generated):
|
|
||||||
self.notify(f"created {num} tasks")
|
|
||||||
items = get_generators()
|
items = get_generators()
|
||||||
for item in items:
|
for item in items:
|
||||||
self.table.add_row(
|
self.table.add_row(
|
||||||
str(item.id),
|
str(item.id), item.template, item.type, json.loads(item.config)["val"]
|
||||||
item.template,
|
|
||||||
item.type,
|
|
||||||
json.loads(item.config)["val"],
|
|
||||||
str(item.next_at()),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def run():
|
def run():
|
||||||
app = TaskGenEditor()
|
app = TaskGenEditor()
|
||||||
app.run()
|
app.run()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
run()
|
||||||
|
Loading…
Reference in New Issue
Block a user