cli-todo-jd 0.2.1__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (23) hide show
  1. cli_todo_jd-0.3.0/MANIFEST.in +1 -0
  2. {cli_todo_jd-0.2.1 → cli_todo_jd-0.3.0}/PKG-INFO +16 -9
  3. {cli_todo_jd-0.2.1 → cli_todo_jd-0.3.0}/README.md +14 -8
  4. cli_todo_jd-0.3.0/cli_todo_jd/cli/cli_entry.py +243 -0
  5. cli_todo_jd-0.3.0/cli_todo_jd/cli/cli_menu.py +129 -0
  6. cli_todo_jd-0.3.0/cli_todo_jd/helpers.py +109 -0
  7. {cli_todo_jd-0.2.1 → cli_todo_jd-0.3.0}/cli_todo_jd/main.py +140 -189
  8. cli_todo_jd-0.3.0/cli_todo_jd/web/app.py +106 -0
  9. cli_todo_jd-0.3.0/cli_todo_jd/web/templates/index.html +162 -0
  10. {cli_todo_jd-0.2.1 → cli_todo_jd-0.3.0}/cli_todo_jd.egg-info/PKG-INFO +16 -9
  11. {cli_todo_jd-0.2.1 → cli_todo_jd-0.3.0}/cli_todo_jd.egg-info/SOURCES.txt +7 -2
  12. cli_todo_jd-0.3.0/cli_todo_jd.egg-info/entry_points.txt +4 -0
  13. {cli_todo_jd-0.2.1 → cli_todo_jd-0.3.0}/cli_todo_jd.egg-info/requires.txt +1 -0
  14. {cli_todo_jd-0.2.1 → cli_todo_jd-0.3.0}/pyproject.toml +6 -4
  15. cli_todo_jd-0.2.1/cli_todo_jd/cli_entry.py +0 -148
  16. cli_todo_jd-0.2.1/cli_todo_jd.egg-info/entry_points.txt +0 -3
  17. {cli_todo_jd-0.2.1 → cli_todo_jd-0.3.0}/cli_todo_jd/__init__.py +0 -0
  18. {cli_todo_jd-0.2.1 → cli_todo_jd-0.3.0}/cli_todo_jd/storage/__init__.py +0 -0
  19. {cli_todo_jd-0.2.1 → cli_todo_jd-0.3.0}/cli_todo_jd/storage/migrate.py +0 -0
  20. {cli_todo_jd-0.2.1 → cli_todo_jd-0.3.0}/cli_todo_jd/storage/schema.py +0 -0
  21. {cli_todo_jd-0.2.1 → cli_todo_jd-0.3.0}/cli_todo_jd.egg-info/dependency_links.txt +0 -0
  22. {cli_todo_jd-0.2.1 → cli_todo_jd-0.3.0}/cli_todo_jd.egg-info/top_level.txt +0 -0
  23. {cli_todo_jd-0.2.1 → cli_todo_jd-0.3.0}/setup.cfg +0 -0
@@ -0,0 +1 @@
1
+ include cli_todo_jd/web/templates/index.html
@@ -1,12 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cli-todo-jd
3
- Version: 0.2.1
3
+ Version: 0.3.0
4
4
  Summary: Add your description here
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
7
7
  Requires-Dist: questionary>=2.1.1
8
8
  Requires-Dist: rich>=14.2.0
9
9
  Requires-Dist: typer>=0.21.1
10
+ Requires-Dist: flask>=3.0.0
10
11
  Provides-Extra: dev
11
12
  Requires-Dist: pre-commit; extra == "dev"
12
13
  Requires-Dist: pytest; extra == "dev"
@@ -19,23 +20,29 @@ A command line to do list with interactive menu
19
20
  ## What is`cli-todo-jd`?
20
21
 
21
22
  This is a command line interface todo list. Once installed, there are two ways to interact
22
- with the list.
23
+ with the list stored as a sqlite database
23
24
 
24
- ### `todo_menu`
25
+ ### `todo menu`
25
26
 
26
- Once installed use `todo_menu` to launch into the interactive menu. From here you can add,
27
+ Once installed use `todo menu` to launch into the interactive menu. From here you can add,
27
28
  remove, list, or clear your todo list. Items in your list are stored (by default) as
28
- `.todo_list.json`. The menu does also support optional filepaths using `-f` or `--filepath`.
29
+ `.todo_list.db`. The menu does also support optional filepaths using `-f` or `--filepath`.
30
+
31
+ ### `todo web`
32
+
33
+ Once installed use `todo web` to launch into the interactive web UI. From here you can add,
34
+ remove, list, or clear your todo list. Items in your list are stored (by default) as
35
+ `.todo_list.db`. The menu does also support optional filepaths using `-f` or `--filepath`.
29
36
 
30
37
 
31
38
  ### interacting with todo list without menu
32
39
 
33
40
  Alternately you can interact directly using the following commands (`--filepath can be substituted for -f`)
34
41
 
35
- - `todo_add text --filepath optional_path_to_json` used to add an item to your list
36
- - `todo_remove index --filepath optional_path_to_json` used to remove item number `index`
37
- - `todo_list --filepath optional_path_to_json` used to view list
38
- - `todo_clear --filepath optional_path_to_json` used to clear list (prompts y/n to confirm)
42
+ - `todo add text --filepath optional_path_to_json` used to add an item to your list
43
+ - `todo remove index --filepath optional_path_to_json` used to remove item number `index`
44
+ - `todo list --filepath optional_path_to_json` used to view list
45
+ - `todo clear --filepath optional_path_to_json` used to clear list (prompts y/n to confirm)
39
46
 
40
47
  ## Getting started
41
48
 
@@ -5,23 +5,29 @@ A command line to do list with interactive menu
5
5
  ## What is`cli-todo-jd`?
6
6
 
7
7
  This is a command line interface todo list. Once installed, there are two ways to interact
8
- with the list.
8
+ with the list stored as a sqlite database
9
9
 
10
- ### `todo_menu`
10
+ ### `todo menu`
11
11
 
12
- Once installed use `todo_menu` to launch into the interactive menu. From here you can add,
12
+ Once installed use `todo menu` to launch into the interactive menu. From here you can add,
13
13
  remove, list, or clear your todo list. Items in your list are stored (by default) as
14
- `.todo_list.json`. The menu does also support optional filepaths using `-f` or `--filepath`.
14
+ `.todo_list.db`. The menu does also support optional filepaths using `-f` or `--filepath`.
15
+
16
+ ### `todo web`
17
+
18
+ Once installed use `todo web` to launch into the interactive web UI. From here you can add,
19
+ remove, list, or clear your todo list. Items in your list are stored (by default) as
20
+ `.todo_list.db`. The menu does also support optional filepaths using `-f` or `--filepath`.
15
21
 
16
22
 
17
23
  ### interacting with todo list without menu
18
24
 
19
25
  Alternately you can interact directly using the following commands (`--filepath can be substituted for -f`)
20
26
 
21
- - `todo_add text --filepath optional_path_to_json` used to add an item to your list
22
- - `todo_remove index --filepath optional_path_to_json` used to remove item number `index`
23
- - `todo_list --filepath optional_path_to_json` used to view list
24
- - `todo_clear --filepath optional_path_to_json` used to clear list (prompts y/n to confirm)
27
+ - `todo add text --filepath optional_path_to_json` used to add an item to your list
28
+ - `todo remove index --filepath optional_path_to_json` used to remove item number `index`
29
+ - `todo list --filepath optional_path_to_json` used to view list
30
+ - `todo clear --filepath optional_path_to_json` used to clear list (prompts y/n to confirm)
25
31
 
26
32
  ## Getting started
27
33
 
@@ -0,0 +1,243 @@
1
+ from __future__ import annotations
2
+
3
+ from argparse import ArgumentParser
4
+ from cli_todo_jd.helpers import (
5
+ add_item_to_list,
6
+ remove_item_from_list,
7
+ remove_item_from_list_by_id,
8
+ list_items_on_list,
9
+ clear_list_of_items,
10
+ mark_item_as_done,
11
+ mark_item_as_not_done,
12
+ mark_item_as_done_by_id,
13
+ mark_item_as_not_done_by_id,
14
+ edit_item_in_list_by_id,
15
+ )
16
+ from cli_todo_jd.cli.cli_menu import cli_menu
17
+ from cli_todo_jd.web.app import run_web
18
+ from pathlib import Path
19
+ import typer
20
+
21
+ app = typer.Typer(help="A tiny todo CLI built with Typer.")
22
+
23
+
24
+ @app.command()
25
+ def add(
26
+ text: list[str] = typer.Argument(..., help="Todo item text (no quotes needed)."),
27
+ filepath: Path = typer.Option(
28
+ Path(".todo_list.db"),
29
+ "--filepath",
30
+ "-f",
31
+ help="Path to the JSON file used for storage.",
32
+ ),
33
+ ) -> None:
34
+ full_text = " ".join(text).strip()
35
+ if not full_text:
36
+ raise typer.BadParameter("Todo item text cannot be empty.")
37
+
38
+ add_item_to_list(full_text, filepath)
39
+ typer.echo(f"Added: {full_text}")
40
+
41
+
42
+ @app.command(name="list")
43
+ def list_(
44
+ filepath: Path = typer.Option(Path(".todo_list.db"), "--filepath", "-f"),
45
+ show_all: bool = typer.Option(
46
+ False, "--all", "-a", help="Show all todos (open + done)."
47
+ ),
48
+ show_done: bool = typer.Option(
49
+ False, "--done", "-d", help="Show only completed todos."
50
+ ),
51
+ show_open: bool = typer.Option(
52
+ False, "--open", "-o", help="Show only open todos (default)."
53
+ ),
54
+ ) -> None:
55
+ """List todos.
56
+
57
+ Examples
58
+ --------
59
+ - todo list
60
+ - todo list --done
61
+ - todo list --all
62
+ - todo list -a
63
+ """
64
+
65
+ # Choose filter. If nothing specified, default to open.
66
+ # If the user specifies multiple flags, error out.
67
+ flags = [show_all, show_done, show_open]
68
+ if sum(1 for f in flags if f) > 1:
69
+ raise typer.BadParameter(
70
+ "Use only one of: --all / -a, --done / -d, --open / -o"
71
+ )
72
+
73
+ if show_all:
74
+ show = "all"
75
+ elif show_done:
76
+ show = "done"
77
+ else:
78
+ # default is open (or explicit --open)
79
+ show = "open"
80
+
81
+ list_items_on_list(filepath, show=show)
82
+
83
+
84
+ @app.command()
85
+ def remove(
86
+ todo_id: int | None = typer.Argument(None, help="Todo ID to remove (preferred)."),
87
+ index: int | None = typer.Option(
88
+ None,
89
+ "--index",
90
+ "-i",
91
+ help="1-based display index (legacy; use ID instead).",
92
+ ),
93
+ filepath: Path = typer.Option(Path(".todo_list.db"), "--filepath", "-f"),
94
+ ) -> None:
95
+ if todo_id is None and index is None:
96
+ raise typer.BadParameter("Provide either TODO_ID argument or --index/-i")
97
+ if todo_id is not None and index is not None:
98
+ raise typer.BadParameter("Provide either TODO_ID or --index/-i, not both")
99
+
100
+ if todo_id is not None:
101
+ remove_item_from_list_by_id(todo_id, filepath)
102
+ else:
103
+ remove_item_from_list(index, filepath)
104
+
105
+
106
+ @app.command()
107
+ def clear(
108
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt."),
109
+ filepath: Path = typer.Option(Path(".todo_list.db"), "--filepath", "-f"),
110
+ ) -> None:
111
+ if not yes and not typer.confirm(f"Clear all todos in {filepath}?"):
112
+ typer.echo("Cancelled.")
113
+ raise typer.Exit(code=1)
114
+
115
+ clear_list_of_items(filepath)
116
+
117
+
118
+ @app.command()
119
+ def edit(
120
+ todo_id: int = typer.Argument(..., help="Todo ID to edit."),
121
+ new_text: list[str] = typer.Argument(..., help="New text for the todo item."),
122
+ filepath: Path = typer.Option(Path(".todo_list.db"), "--filepath", "-f"),
123
+ ) -> None:
124
+ new_text_stripped = " ".join(new_text).strip()
125
+ if not new_text_stripped:
126
+ raise typer.BadParameter("New todo item text cannot be empty.")
127
+
128
+ edit_item_in_list_by_id(todo_id, new_text_stripped, filepath)
129
+ typer.echo(f'Edited todo ID {todo_id} to: "{new_text_stripped}"')
130
+
131
+
132
+ @app.command(name="menu")
133
+ def menu_(
134
+ filepath: Path = typer.Option(
135
+ Path(".todo_list.db"),
136
+ "--filepath",
137
+ "-f",
138
+ help="Path to the JSON file used for storage.",
139
+ ),
140
+ ) -> None:
141
+ cli_menu(filepath)
142
+ typer.echo("Exited menu.")
143
+
144
+
145
+ @app.command()
146
+ def done(
147
+ todo_id: int | None = typer.Argument(
148
+ None, help="Todo ID to mark as done (preferred)."
149
+ ),
150
+ index: int | None = typer.Option(
151
+ None,
152
+ "--index",
153
+ "-i",
154
+ help="1-based display index (legacy; use ID instead).",
155
+ ),
156
+ filepath: Path = typer.Option(Path(".todo_list.db"), "--filepath", "-f"),
157
+ ) -> None:
158
+ if todo_id is None and index is None:
159
+ raise typer.BadParameter("Provide either TODO_ID argument or --index/-i")
160
+ if todo_id is not None and index is not None:
161
+ raise typer.BadParameter("Provide either TODO_ID or --index/-i, not both")
162
+
163
+ if todo_id is not None:
164
+ mark_item_as_done_by_id(todo_id, filepath)
165
+ else:
166
+ mark_item_as_done(index, filepath)
167
+
168
+ list_items_on_list(filepath=filepath, show="all")
169
+
170
+
171
+ @app.command(name="not-done")
172
+ def not_done(
173
+ todo_id: int | None = typer.Argument(
174
+ None, help="Todo ID to mark as not done (preferred)."
175
+ ),
176
+ index: int | None = typer.Option(
177
+ None,
178
+ "--index",
179
+ "-i",
180
+ help="1-based display index (legacy; use ID instead).",
181
+ ),
182
+ filepath: Path = typer.Option(Path(".todo_list.db"), "--filepath", "-f"),
183
+ ) -> None:
184
+ if todo_id is None and index is None:
185
+ raise typer.BadParameter("Provide either TODO_ID argument or --index/-i")
186
+ if todo_id is not None and index is not None:
187
+ raise typer.BadParameter("Provide either TODO_ID or --index/-i, not both")
188
+
189
+ if todo_id is not None:
190
+ mark_item_as_not_done_by_id(todo_id, filepath)
191
+ else:
192
+ mark_item_as_not_done(index, filepath)
193
+
194
+ list_items_on_list(filepath=filepath, show="all")
195
+
196
+
197
+ @app.command()
198
+ def web(
199
+ filepath: Path = typer.Option(Path(".todo_list.db"), "--filepath", "-f"),
200
+ host: str = typer.Option(
201
+ "127.0.0.1", help="Host interface to bind the web server."
202
+ ),
203
+ port: int = typer.Option(8000, help="Port to run the web server on."),
204
+ debug: bool = typer.Option(False, help="Run Flask in debug mode."),
205
+ ) -> None:
206
+ """Run a local web UI for your todo list."""
207
+ run_web(filepath, host=host, port=port, debug=debug)
208
+
209
+
210
+ def parser_optional_args(parser: ArgumentParser):
211
+ parser.add_argument(
212
+ "-f",
213
+ "--filepath",
214
+ help="Path to the file to process",
215
+ default="./.todo_list.db",
216
+ )
217
+
218
+
219
+ def todo_menu():
220
+ parser = ArgumentParser(description="Todo List CLI Menu")
221
+ parser_optional_args(parser)
222
+ args = parser.parse_args()
223
+
224
+ cli_menu(filepath=args.filepath)
225
+
226
+
227
+ def todo_web():
228
+ parser = ArgumentParser(description="Todo List Web Server")
229
+ parser_optional_args(parser)
230
+ parser.add_argument(
231
+ "--host", help="Host interface to bind the web server.", default="127.0.0.1"
232
+ )
233
+ parser.add_argument(
234
+ "--port", help="Port to run the web server on.", default=8000, type=int
235
+ )
236
+ parser.add_argument("--debug", help="Run Flask in debug mode.", action="store_true")
237
+ args = parser.parse_args()
238
+
239
+ run_web(db_path=args.filepath, host=args.host, port=args.port, debug=args.debug)
240
+
241
+
242
+ if __name__ == "__main__":
243
+ app()
@@ -0,0 +1,129 @@
1
+ from questionary import Style
2
+ import questionary
3
+ from cli_todo_jd.helpers import create_list
4
+
5
+ custom_style = Style(
6
+ [
7
+ ("qmark", "fg:#ff9d00 bold"),
8
+ ("question", "bold"),
9
+ ("answer", "fg:#ff9d00 bold"),
10
+ ("pointer", "fg:#ff9d00 bold"),
11
+ ("highlighted", "fg:#ff9d00 bold"),
12
+ ("selected", "fg:#ff9d00 bold"),
13
+ ("separator", "fg:#ff9d00 bold"),
14
+ ("instruction", ""), # default
15
+ ("text", ""),
16
+ ("disabled", "fg:#ff9d00 italic"),
17
+ ]
18
+ )
19
+
20
+
21
+ def cli_menu(filepath="./.todo_list.db"):
22
+ """
23
+ Display the command-line interface menu for the todo list.
24
+
25
+ Parameters
26
+ ----------
27
+ filepath : str, optional
28
+ The file path to the JSON file for storing todos, by default "./.todo_list.db"
29
+ """
30
+ app = create_list(file_path_to_db=filepath)
31
+ while True:
32
+ action = questionary.select(
33
+ "What would you like to do?",
34
+ choices=[
35
+ "Add todo",
36
+ "List todos",
37
+ "Update todo status",
38
+ "Remove todo",
39
+ "Edit todo",
40
+ "Clear all todos",
41
+ "Exit",
42
+ ],
43
+ style=custom_style,
44
+ ).ask()
45
+
46
+ if action == "Add todo":
47
+ item = questionary.text("Enter the todo item:", style=custom_style).ask()
48
+ app.add_todo(item)
49
+ elif action == "List todos":
50
+ app.list_todos(show="all")
51
+ elif action == "Update todo status":
52
+ app.reload_todos()
53
+ if not app.todos:
54
+ print("No todos to update.")
55
+ continue
56
+ todo_choice = questionary.select(
57
+ "Select the todo to update:",
58
+ choices=["<Back>"] + app.todos,
59
+ style=custom_style,
60
+ ).ask()
61
+
62
+ if todo_choice == "<Back>" or todo_choice is None:
63
+ continue
64
+
65
+ todo_index = app.todos.index(todo_choice) + 1
66
+ status_choice = questionary.select(
67
+ "Mark as:",
68
+ choices=["Done", "Not Done", "<Back>"],
69
+ style=custom_style,
70
+ ).ask()
71
+
72
+ if status_choice == "<Back>" or status_choice is None:
73
+ continue
74
+ elif status_choice == "Done":
75
+ app.mark_as_done(todo_index)
76
+ elif status_choice == "Not Done":
77
+ app.mark_as_not_done(todo_index)
78
+ app.list_todos(show="all")
79
+ elif action == "Remove todo":
80
+ app.reload_todos()
81
+ if not app.todos:
82
+ print("No todos to remove.")
83
+ continue
84
+ todo_choice = questionary.select(
85
+ "Select the todo to remove:",
86
+ choices=["<Back>"] + app.todos,
87
+ style=custom_style,
88
+ ).ask()
89
+
90
+ if todo_choice == "<Back>" or todo_choice is None:
91
+ continue
92
+
93
+ todo_to_remove = app.todos.index(todo_choice) + 1
94
+ app.remove_todo(todo_to_remove)
95
+ elif action == "Edit todo":
96
+ app.reload_todos()
97
+ if not app.todos:
98
+ print("No todos to edit.")
99
+ continue
100
+ todo_choice = questionary.select(
101
+ "Select the todo to edit:",
102
+ choices=["<Back>"] + app.todos,
103
+ style=custom_style,
104
+ ).ask()
105
+
106
+ if todo_choice == "<Back>" or todo_choice is None:
107
+ continue
108
+
109
+ todo_index = app.todos.index(todo_choice) + 1
110
+ new_text = questionary.text(
111
+ "Enter the new text for the todo:",
112
+ default=todo_choice,
113
+ style=custom_style,
114
+ ).ask()
115
+
116
+ if new_text is None:
117
+ continue
118
+ app.edit_entry(todo_index, new_text)
119
+
120
+ elif action == "Clear all todos":
121
+ confirm = questionary.confirm(
122
+ "Are you sure you want to clear all todos?", style=custom_style
123
+ ).ask()
124
+ if confirm:
125
+ app.clear_all()
126
+ elif action == "Exit":
127
+ break
128
+ else:
129
+ break
@@ -0,0 +1,109 @@
1
+ from cli_todo_jd.main import TodoApp
2
+
3
+
4
+ def create_list(file_path_to_db: str = "./.todo_list.db"):
5
+ """
6
+ Create a new todo list.
7
+
8
+ Parameters
9
+ ----------
10
+ file_path_to_db : str, optional
11
+ The file path to the JSON file for storing todos, by default "./.todo_list.db"
12
+
13
+ Returns
14
+ -------
15
+ TodoApp
16
+ An instance of the TodoApp class.
17
+ """
18
+ app = TodoApp(file_path_to_db=file_path_to_db)
19
+ return app
20
+
21
+
22
+ def add_item_to_list(item: str, filepath: str):
23
+ """
24
+ Add a new item to the todo list.
25
+
26
+ Parameters
27
+ ----------
28
+ item : str
29
+ The todo item to add.
30
+ filepath : str
31
+ The file path to the JSON file for storing todos.
32
+ """
33
+ app = create_list(file_path_to_db=filepath)
34
+ app.add_todo(item)
35
+ app.list_todos()
36
+
37
+
38
+ def list_items_on_list(filepath: str, show: str = "open"):
39
+ """List items in the todo list.
40
+
41
+ Parameters
42
+ ----------
43
+ filepath:
44
+ The SQLite database path.
45
+ show:
46
+ "open" (default), "done", or "all".
47
+ """
48
+ app = create_list(file_path_to_db=filepath)
49
+ app.list_todos(show=show)
50
+
51
+
52
+ def remove_item_from_list(index: int, filepath: str):
53
+ """
54
+ remove an item from the todo list using index
55
+
56
+ Parameters
57
+ ----------
58
+ index : int
59
+ The index of the todo item to remove.
60
+ filepath : str
61
+ The file path to the JSON file for storing todos.
62
+ """
63
+ app = create_list(file_path_to_db=filepath)
64
+ app.remove_todo(index)
65
+ app.list_todos()
66
+
67
+
68
+ def clear_list_of_items(filepath: str):
69
+ """
70
+ Clear all items from the todo list.
71
+
72
+ Parameters
73
+ ----------
74
+ filepath : str
75
+ The file path to the JSON file for storing todos.
76
+ """
77
+ app = create_list(file_path_to_db=filepath)
78
+ app.clear_all()
79
+
80
+
81
+ def mark_item_as_done(index: int, filepath: str):
82
+ app = create_list(file_path_to_db=filepath)
83
+ app.mark_as_done(index)
84
+
85
+
86
+ def mark_item_as_not_done(index: int, filepath: str):
87
+ app = create_list(file_path_to_db=filepath)
88
+ app.mark_as_not_done(index)
89
+
90
+
91
+ def remove_item_from_list_by_id(todo_id: int, filepath: str):
92
+ app = create_list(file_path_to_db=filepath)
93
+ app.remove_by_id(todo_id)
94
+ app.list_todos(show="all")
95
+
96
+
97
+ def mark_item_as_done_by_id(todo_id: int, filepath: str):
98
+ app = create_list(file_path_to_db=filepath)
99
+ app.mark_done_by_id(todo_id)
100
+
101
+
102
+ def mark_item_as_not_done_by_id(todo_id: int, filepath: str):
103
+ app = create_list(file_path_to_db=filepath)
104
+ app.mark_not_done_by_id(todo_id)
105
+
106
+
107
+ def edit_item_in_list_by_id(todo_id: int, new_text: str, filepath: str):
108
+ app = create_list(file_path_to_db=filepath)
109
+ app.edit_by_id(todo_id, new_text)