Compare commits
	
		
			2 commits
		
	
	
		
			525f29aa7a
			...
			4980b2e7e5
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 4980b2e7e5 | |||
| 4110d93b1e | 
					 7 changed files with 293 additions and 37 deletions
				
			
		|  | @ -6,6 +6,7 @@ from .controller.tasks import add_task | ||||||
| from .import_csv import import_tasks_from_csv | from .import_csv import import_tasks_from_csv | ||||||
| from .db import initialize_db | from .db import initialize_db | ||||||
| from .tui import tasks | from .tui import tasks | ||||||
|  | from .tui import recurring | ||||||
| 
 | 
 | ||||||
| app = typer.Typer() | app = typer.Typer() | ||||||
| 
 | 
 | ||||||
|  | @ -48,6 +49,12 @@ def browse( | ||||||
|     tasks.run(view) |     tasks.run(view) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @app.command() | ||||||
|  | def generators(): | ||||||
|  |     initialize_db() | ||||||
|  |     recurring.run() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| @app.command() | @app.command() | ||||||
| def import_csv(filename: str): | def import_csv(filename: str): | ||||||
|     import_tasks_from_csv(filename) |     import_tasks_from_csv(filename) | ||||||
|  |  | ||||||
							
								
								
									
										40
									
								
								src/tt/controller/generators.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/tt/controller/generators.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,40 @@ | ||||||
|  | from ..db import db, TaskGenerator, GeneratorType | ||||||
|  | import json | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def get_generator(item_id: int) -> TaskGenerator: | ||||||
|  |     return TaskGenerator.get_by_id(item_id) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def get_generators() -> list[TaskGenerator]: | ||||||
|  |     query = TaskGenerator.select().where(~TaskGenerator.deleted) | ||||||
|  |     return query.order_by("type", "template") | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def add_generator( | ||||||
|  |     template: str, | ||||||
|  |     type: GeneratorType, | ||||||
|  |     val: str, | ||||||
|  | ) -> TaskGenerator: | ||||||
|  |     # JSON for future expansion | ||||||
|  |     config = json.dumps({"val": val}) | ||||||
|  |     with db.atomic(): | ||||||
|  |         task = TaskGenerator.create( | ||||||
|  |             template=template, | ||||||
|  |             type=type, | ||||||
|  |             config=config, | ||||||
|  |         ) | ||||||
|  |     return task | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def update_generator( | ||||||
|  |     item_id: int, | ||||||
|  |     **kwargs, | ||||||
|  | ) -> TaskGenerator: | ||||||
|  |     config = {"val": kwargs.pop("val")} | ||||||
|  |     kwargs["config"] = json.dumps(config) | ||||||
|  |     with db.atomic(): | ||||||
|  |         query = TaskGenerator.update(kwargs).where(TaskGenerator.id == item_id) | ||||||
|  |         query.execute() | ||||||
|  |         task = TaskGenerator.get_by_id(item_id) | ||||||
|  |     return task | ||||||
							
								
								
									
										31
									
								
								src/tt/db.py
									
									
									
									
									
								
							
							
						
						
									
										31
									
								
								src/tt/db.py
									
									
									
									
									
								
							|  | @ -28,6 +28,11 @@ class TaskStatus(Enum): | ||||||
|     DONE = "done" |     DONE = "done" | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | class GeneratorType(Enum): | ||||||
|  |     DAYS_BETWEEN = "days-btwn" | ||||||
|  |     MONTHLY = "monthly" | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| class BaseModel(Model): | class BaseModel(Model): | ||||||
|     class Meta: |     class Meta: | ||||||
|         database = db |         database = db | ||||||
|  | @ -68,33 +73,28 @@ class SavedSearch(BaseModel): | ||||||
|         return self.name |         return self.name | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class TaskGenerator(Model): | class TaskGenerator(BaseModel): | ||||||
|     template = CharField() |     template = CharField() | ||||||
|     type = CharField() |     type = CharField() | ||||||
|     config = TextField() |     config = TextField()  # 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) | ||||||
| 
 | 
 | ||||||
|     def should_generate(self) -> bool: |     def should_generate(self) -> bool: | ||||||
|         """ |  | ||||||
|         generator types: config keys |  | ||||||
|             recurring: days_between |  | ||||||
|             monthly: day_of_month |  | ||||||
|         """ |  | ||||||
|         if self.deleted: |         if self.deleted: | ||||||
|             return False |             return False | ||||||
|         if not self.last_generated_at: |         if not self.last_generated_at: | ||||||
|             return True |             return True | ||||||
| 
 | 
 | ||||||
|         now = datetime.now() |         now = datetime.now() | ||||||
|         if self.type == "recurring": |         if self.type == GeneratorType.DAYS_BETWEEN: | ||||||
|             days_between = self.config["days_between"] |             days_between = self.config["val"] | ||||||
|             days_since = (now - self.last_generated_at).days |             days_since = (now - self.last_generated_at).days | ||||||
|             return days_since >= days_between |             return days_since >= days_between | ||||||
| 
 | 
 | ||||||
|         elif self.type == "monthly": |         elif self.type == GeneratorType.MONTHLY: | ||||||
|             day_of_month = self.generator_config["day_of_month"] |             day_of_month = self.config["val"] | ||||||
| 
 | 
 | ||||||
|             # check each day until now to see if target day occurred |             # check each day until now to see if target day occurred | ||||||
|             one_day = timedelta(days=1) |             one_day = timedelta(days=1) | ||||||
|  | @ -111,14 +111,7 @@ class TaskGenerator(Model): | ||||||
| 
 | 
 | ||||||
| def initialize_db(): | def initialize_db(): | ||||||
|     db.connect() |     db.connect() | ||||||
|     db.create_tables( |     db.create_tables([Category, Task, SavedSearch, TaskGenerator]) | ||||||
|         [ |  | ||||||
|             Category, |  | ||||||
|             Task, |  | ||||||
|             SavedSearch, |  | ||||||
|             # TaskGenerator |  | ||||||
|         ] |  | ||||||
|     ) |  | ||||||
|     if not Category.select().exists(): |     if not Category.select().exists(): | ||||||
|         Category.create(name="default") |         Category.create(name="default") | ||||||
|     db.close() |     db.close() | ||||||
|  |  | ||||||
|  | @ -15,11 +15,26 @@ from ..utils import ( | ||||||
| ) | ) | ||||||
| from .keymodal import KeyModal | from .keymodal import KeyModal | ||||||
| 
 | 
 | ||||||
|  | ELLIPSIS = "…" | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| class NotifyValidationError(Exception): | class NotifyValidationError(Exception): | ||||||
|     """will notify and continue if raised""" |     """will notify and continue if raised""" | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | def _enum_preprocessor(enumCls): | ||||||
|  |     """generate a default preprocessor to enforce enums""" | ||||||
|  | 
 | ||||||
|  |     def preprocessor(val): | ||||||
|  |         try: | ||||||
|  |             enumCls(val) | ||||||
|  |             return val | ||||||
|  |         except ValueError: | ||||||
|  |             raise NotifyValidationError( | ||||||
|  |                 f"Invalid value. Use: {[s.value for s in enumCls]}" | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| class TableColumnConfig: | class TableColumnConfig: | ||||||
|     def __init__( |     def __init__( | ||||||
|         self, |         self, | ||||||
|  | @ -38,7 +53,12 @@ class TableColumnConfig: | ||||||
|         self.enable_editor = enable_editor |         self.enable_editor = enable_editor | ||||||
|         self.enum = enum |         self.enum = enum | ||||||
|         self.filterable = filterable |         self.filterable = filterable | ||||||
|         self.preprocessor = preprocessor or (lambda x: x) |         if preprocessor: | ||||||
|  |             self.preprocessor = preprocessor | ||||||
|  |         elif self.enum: | ||||||
|  |             self.preprocessor = _enum_preprocessor(self.enum) | ||||||
|  |         else: | ||||||
|  |             self.preprocessor = lambda x: x | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class TableEditor(App): | class TableEditor(App): | ||||||
|  | @ -193,7 +213,7 @@ class TableEditor(App): | ||||||
|         if self.table.cursor_column == 0: |         if self.table.cursor_column == 0: | ||||||
|             cur_row = self.table.cursor_row |             cur_row = self.table.cursor_row | ||||||
|             item_id = int(self.table.get_cell_at((cur_row, 0))) |             item_id = int(self.table.get_cell_at((cur_row, 0))) | ||||||
|             # TODO: deletable items need a delete |             # deletable items need a delete | ||||||
|             self.update_item_callback(item_id, deleted=True) |             self.update_item_callback(item_id, deleted=True) | ||||||
|             self._save_cursor() |             self._save_cursor() | ||||||
|             self.refresh_data() |             self.refresh_data() | ||||||
|  | @ -213,7 +233,7 @@ class TableEditor(App): | ||||||
| 
 | 
 | ||||||
|         new_item = self.add_item_callback(**prepopulated) |         new_item = self.add_item_callback(**prepopulated) | ||||||
|         self.refresh_data(restore_cursor=False) |         self.refresh_data(restore_cursor=False) | ||||||
|         self.move_cursor_to_item(new_item.id) |         self.move_cursor_to_item(new_item.id)  # TODO: check success here | ||||||
|         self.action_start_change() |         self.action_start_change() | ||||||
| 
 | 
 | ||||||
|     def _active_column_config(self): |     def _active_column_config(self): | ||||||
|  | @ -236,8 +256,8 @@ class TableEditor(App): | ||||||
|             # trigger item_id to be saved on the next cursor move |             # trigger item_id to be saved on the next cursor move | ||||||
|             # this avoids filtered columns disappearing right away |             # this avoids filtered columns disappearing right away | ||||||
|             # and tons of DB writes |             # and tons of DB writes | ||||||
|             # TODO: status hard coded here |             update = {cconf.field: next_val} | ||||||
|             self._register_save_on_move(item_id, status=next_val) |             self._register_save_on_move(item_id, **update) | ||||||
| 
 | 
 | ||||||
|     def _register_save_on_move(self, item_id, **kwargs): |     def _register_save_on_move(self, item_id, **kwargs): | ||||||
|         if self.save_on_move and self.save_on_move["item_id"] != item_id: |         if self.save_on_move and self.save_on_move["item_id"] != item_id: | ||||||
|  | @ -307,6 +327,10 @@ class TableEditor(App): | ||||||
|         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): | ||||||
|  |             # TODO: flash message? | ||||||
|  |             # need to edit with e | ||||||
|  |             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) | ||||||
| 
 | 
 | ||||||
|  | @ -363,7 +387,7 @@ class TableEditor(App): | ||||||
|             self.update_item_callback(item_id, **update_data) |             self.update_item_callback(item_id, **update_data) | ||||||
|             self.refresh_data() |             self.refresh_data() | ||||||
|         except NotifyValidationError as e: |         except NotifyValidationError as e: | ||||||
|             self.notify(e.message) |             self.notify(str(e)) | ||||||
|         except Exception as e: |         except Exception as e: | ||||||
|             self.notify(f"Error updating item: {str(e)}") |             self.notify(f"Error updating item: {str(e)}") | ||||||
|         finally: |         finally: | ||||||
|  |  | ||||||
							
								
								
									
										139
									
								
								src/tt/tui/overview.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										139
									
								
								src/tt/tui/overview.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,139 @@ | ||||||
|  | from textual.app import App, ComposeResult | ||||||
|  | from textual.containers import ScrollableContainer, Horizontal | ||||||
|  | from textual.widgets import DataTable, Static | ||||||
|  | from textual.binding import Binding | ||||||
|  | from datetime import datetime | ||||||
|  | 
 | ||||||
|  | from ..controller.summaries import ( | ||||||
|  |     get_category_summary, | ||||||
|  |     get_recently_active, | ||||||
|  |     get_due_soon, | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 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"]), | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class TaskList(DataTable): | ||||||
|  |     """Base class for task list tables""" | ||||||
|  | 
 | ||||||
|  |     def on_mount(self): | ||||||
|  |         self.add_columns("Task", "Status", "Category", "Due Date") | ||||||
|  | 
 | ||||||
|  |     def format_due_date(self, due_date: datetime | None) -> str: | ||||||
|  |         if not due_date: | ||||||
|  |             return "No due date" | ||||||
|  |         return due_date.strftime("%Y-%m-%d") | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class DueTaskList(TaskList): | ||||||
|  |     """Table showing upcoming and overdue tasks""" | ||||||
|  | 
 | ||||||
|  |     def on_mount(self): | ||||||
|  |         super().on_mount() | ||||||
|  |         self.refresh_data() | ||||||
|  | 
 | ||||||
|  |     def refresh_data(self): | ||||||
|  |         self.clear() | ||||||
|  |         tasks = get_due_soon(10, all_overdue=True)  # Show all overdue + next 10 | ||||||
|  |         for task in tasks: | ||||||
|  |             self.add_row( | ||||||
|  |                 task["text"], | ||||||
|  |                 task["status"], | ||||||
|  |                 task["category"] or "No category", | ||||||
|  |                 self.format_due_date(task["due"]), | ||||||
|  |                 key=str(task["id"]), | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class RecentTaskList(TaskList): | ||||||
|  |     """Table showing recently active tasks""" | ||||||
|  | 
 | ||||||
|  |     def on_mount(self): | ||||||
|  |         super().on_mount() | ||||||
|  |         self.refresh_data() | ||||||
|  | 
 | ||||||
|  |     def refresh_data(self): | ||||||
|  |         self.clear() | ||||||
|  |         tasks = get_recently_active(10)  # Show 10 most recent | ||||||
|  |         for task in tasks: | ||||||
|  |             self.add_row( | ||||||
|  |                 task["text"], | ||||||
|  |                 task["status"], | ||||||
|  |                 task["category"] or "No category", | ||||||
|  |                 self.format_due_date(task["due"]), | ||||||
|  |                 key=str(task["id"]), | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class Overview(App): | ||||||
|  |     """Task overview application""" | ||||||
|  | 
 | ||||||
|  |     TITLE = "Task Overview" | ||||||
|  |     CSS = """ | ||||||
|  |     CategoryTable { | ||||||
|  |         height: 40%; | ||||||
|  |         margin: 1 1; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     #lists { | ||||||
|  |         height: 60%; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     TaskList { | ||||||
|  |         width: 50%; | ||||||
|  |         margin: 1 1; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     Static { | ||||||
|  |         content-align: center middle; | ||||||
|  |         background: $panel; | ||||||
|  |         padding: 1; | ||||||
|  |     } | ||||||
|  |     """ | ||||||
|  | 
 | ||||||
|  |     BINDINGS = [ | ||||||
|  |         Binding("q", "quit", "Quit"), | ||||||
|  |         Binding("r", "refresh", "Refresh Data"), | ||||||
|  |     ] | ||||||
|  | 
 | ||||||
|  |     def compose(self) -> ComposeResult: | ||||||
|  |         yield CategoryTable() | ||||||
|  |         yield Static("Upcoming and Recent Tasks") | ||||||
|  |         with Horizontal(id="lists"): | ||||||
|  |             with ScrollableContainer(): | ||||||
|  |                 yield DueTaskList() | ||||||
|  |             with ScrollableContainer(): | ||||||
|  |                 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() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | if __name__ == "__main__": | ||||||
|  |     app = Overview() | ||||||
|  |     app.run() | ||||||
							
								
								
									
										58
									
								
								src/tt/tui/recurring.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								src/tt/tui/recurring.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,58 @@ | ||||||
|  | import json | ||||||
|  | from datetime import datetime | ||||||
|  | 
 | ||||||
|  | from ..controller.generators import ( | ||||||
|  |     get_generator, | ||||||
|  |     get_generators, | ||||||
|  |     add_generator, | ||||||
|  |     update_generator, | ||||||
|  | ) | ||||||
|  | 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"), | ||||||
|  |         TableColumnConfig("template", "Template", default="recur {val}"), | ||||||
|  |         TableColumnConfig( | ||||||
|  |             "type", | ||||||
|  |             "Type", | ||||||
|  |             default=GeneratorType.DAYS_BETWEEN.value, | ||||||
|  |             enum=GeneratorType, | ||||||
|  |         ), | ||||||
|  |         TableColumnConfig("val", "Value", default=""), | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     def __init__(self): | ||||||
|  |         super().__init__() | ||||||
|  |         self.update_item_callback = update_generator | ||||||
|  |         self.add_item_callback = add_generator | ||||||
|  |         self.get_item_callback = get_generator | ||||||
|  | 
 | ||||||
|  |     def refresh_items(self): | ||||||
|  |         items = get_generators() | ||||||
|  |         for item in items: | ||||||
|  |             self.table.add_row( | ||||||
|  |                 str(item.id), item.template, item.type, json.loads(item.config)["val"] | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def run(): | ||||||
|  |     app = TaskGenEditor() | ||||||
|  |     app.run() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | if __name__ == "__main__": | ||||||
|  |     run() | ||||||
|  | @ -20,6 +20,7 @@ from .editor import ( | ||||||
|     TableEditor, |     TableEditor, | ||||||
|     TableColumnConfig, |     TableColumnConfig, | ||||||
|     NotifyValidationError, |     NotifyValidationError, | ||||||
|  |     ELLIPSIS, | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -30,16 +31,6 @@ def due_preprocessor(val): | ||||||
|         raise NotifyValidationError("Invalid date format. Use YYYY-MM-DD") |         raise NotifyValidationError("Invalid date format. Use YYYY-MM-DD") | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def status_preprocessor(val): |  | ||||||
|     try: |  | ||||||
|         TaskStatus(val) |  | ||||||
|         return val |  | ||||||
|     except ValueError: |  | ||||||
|         raise NotifyValidationError( |  | ||||||
|             f"Invalid status. Use: {[s.value for s in TaskStatus]}" |  | ||||||
|         ) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class TT(TableEditor): | class TT(TableEditor): | ||||||
|     TABLE_CONFIG = ( |     TABLE_CONFIG = ( | ||||||
|         TableColumnConfig("id", "ID"), |         TableColumnConfig("id", "ID"), | ||||||
|  | @ -49,7 +40,6 @@ class TT(TableEditor): | ||||||
|             "Status", |             "Status", | ||||||
|             default="zero", |             default="zero", | ||||||
|             enum=TaskStatus, |             enum=TaskStatus, | ||||||
|             preprocessor=status_preprocessor, |  | ||||||
|         ), |         ), | ||||||
|         TableColumnConfig("type", "Type", default=""), |         TableColumnConfig("type", "Type", default=""), | ||||||
|         TableColumnConfig("due", "Due", default="", preprocessor=due_preprocessor), |         TableColumnConfig("due", "Due", default="", preprocessor=due_preprocessor), | ||||||
|  | @ -112,9 +102,14 @@ class TT(TableEditor): | ||||||
|             status = get_colored_status(item.status) |             status = get_colored_status(item.status) | ||||||
|             due = get_colored_date(item.due) |             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), |                 str(item.id), | ||||||
|                 item.text.split("\n")[0],  # first line |                 text, | ||||||
|                 status, |                 status, | ||||||
|                 item.type, |                 item.type, | ||||||
|                 due, |                 due, | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue