-
Notifications
You must be signed in to change notification settings - Fork 0
feat(todo): edit in place for todo titles #88
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -4,7 +4,7 @@ | |||||||||||||||||||
| import sqlite3 | ||||||||||||||||||||
| from pathlib import Path | ||||||||||||||||||||
|
|
||||||||||||||||||||
| from flask import Flask, redirect, render_template, request, url_for | ||||||||||||||||||||
| from flask import Flask, jsonify, redirect, render_template, request, url_for | ||||||||||||||||||||
|
|
||||||||||||||||||||
| app = Flask(__name__, template_folder=str(Path(__file__).parent.parent / "templates")) | ||||||||||||||||||||
| DATABASE = os.environ.get("DATABASE_PATH", "todos.db") | ||||||||||||||||||||
|
|
@@ -63,6 +63,21 @@ def toggle(todo_id: int) -> str: | |||||||||||||||||||
| return redirect(url_for("index")) | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| @app.route("/edit/<int:todo_id>", methods=["PATCH"]) | ||||||||||||||||||||
| def edit(todo_id: int) -> str: | ||||||||||||||||||||
| """Edit a todo's title.""" | ||||||||||||||||||||
| title = request.form.get("title", "").strip() | ||||||||||||||||||||
| if not title: | ||||||||||||||||||||
| return jsonify({"error": "Title cannot be empty"}), 400 | ||||||||||||||||||||
| conn = get_db() | ||||||||||||||||||||
| cursor = conn.execute("UPDATE todos SET title = ? WHERE id = ?", (title, todo_id)) | ||||||||||||||||||||
| conn.commit() | ||||||||||||||||||||
| conn.close() | ||||||||||||||||||||
|
Comment on lines
+73
to
+75
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The database is committed before checking Consider checking
Suggested change
Why did I show this?Category: mistake Based on general best practices Tools used:
|
||||||||||||||||||||
| if cursor.rowcount == 0: | ||||||||||||||||||||
| return jsonify({"error": "Todo not found"}), 404 | ||||||||||||||||||||
| return jsonify({"title": title}) | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| @app.route("/delete/<int:todo_id>") | ||||||||||||||||||||
| def delete(todo_id: int) -> str: | ||||||||||||||||||||
| """Delete a todo.""" | ||||||||||||||||||||
|
|
||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| """Shared pytest fixtures.""" | ||
|
|
||
| import pytest | ||
|
|
||
| import todo_app.app as app_module | ||
| from todo_app.app import app, init_db | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def client(tmp_path, monkeypatch): | ||
| """Flask test client with a temporary database.""" | ||
| monkeypatch.setattr(app_module, "DATABASE", str(tmp_path / "test.db")) | ||
| app.config["TESTING"] = True | ||
| init_db() | ||
|
|
||
| with app.test_client() as test_client: | ||
| yield test_client |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| """Tests for the Flask todo application routes.""" | ||
|
|
||
| import pytest | ||
|
|
||
|
|
||
| @pytest.mark.unit | ||
| def test_index_empty(client): | ||
| """Test index page with no todos.""" | ||
| response = client.get("/") | ||
| assert response.status_code == 200 | ||
| assert b"No todos yet." in response.data | ||
|
|
||
|
|
||
| @pytest.mark.unit | ||
| def test_add_todo(client): | ||
| """Test adding a todo.""" | ||
| response = client.post("/add", data={"title": "Buy milk"}, follow_redirects=True) | ||
| assert response.status_code == 200 | ||
| assert b"Buy milk" in response.data | ||
|
|
||
|
|
||
| @pytest.mark.unit | ||
| def test_add_todo_empty_title(client): | ||
| """Test that adding a todo with empty title is ignored.""" | ||
| response = client.post("/add", data={"title": " "}, follow_redirects=True) | ||
| assert response.status_code == 200 | ||
| assert b"No todos yet." in response.data | ||
|
|
||
|
|
||
| @pytest.mark.unit | ||
| def test_toggle_todo(client): | ||
| """Test toggling a todo's completion status.""" | ||
| client.post("/add", data={"title": "Test task"}) | ||
| response = client.get("/toggle/1", follow_redirects=True) | ||
| assert response.status_code == 200 | ||
| assert b"Done" in response.data | ||
|
|
||
|
|
||
| @pytest.mark.unit | ||
| def test_delete_todo(client): | ||
| """Test deleting a todo.""" | ||
| client.post("/add", data={"title": "To delete"}) | ||
| response = client.get("/delete/1", follow_redirects=True) | ||
| assert response.status_code == 200 | ||
| assert b"No todos yet." in response.data | ||
|
|
||
|
|
||
| @pytest.mark.unit | ||
| def test_edit_todo(client): | ||
| """Test editing a todo title.""" | ||
| client.post("/add", data={"title": "Original title"}) | ||
| response = client.patch("/edit/1", data={"title": "Updated title"}) | ||
| assert response.status_code == 200 | ||
| data = response.get_json() | ||
| assert data["title"] == "Updated title" | ||
|
|
||
|
|
||
| @pytest.mark.unit | ||
| def test_edit_todo_empty_title(client): | ||
| """Test that editing with an empty title returns 400.""" | ||
| client.post("/add", data={"title": "Original title"}) | ||
| response = client.patch("/edit/1", data={"title": " "}) | ||
| assert response.status_code == 400 | ||
| data = response.get_json() | ||
| assert "error" in data | ||
|
|
||
|
|
||
| @pytest.mark.unit | ||
| def test_edit_todo_not_found(client): | ||
| """Test that editing a non-existent todo returns 404.""" | ||
| response = client.patch("/edit/999", data={"title": "Ghost edit"}) | ||
| assert response.status_code == 404 | ||
| data = response.get_json() | ||
| assert "error" in data | ||
|
|
||
|
|
||
| @pytest.mark.unit | ||
| def test_edit_todo_persists(client): | ||
| """Test that editing a todo persists the new title.""" | ||
| client.post("/add", data={"title": "Old title"}) | ||
| client.patch("/edit/1", data={"title": "New title"}) | ||
| response = client.get("/") | ||
| assert b"New title" in response.data | ||
| assert b"Old title" not in response.data |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After pressing Enter or Escape, the
blurevent listener is removed viaremoveEventListener, but this only works if the exact same function reference is passed. Heresaveis a named function defined inside the click handler, so the reference is consistent — however, after pressing Escape,cell.textContent = originalTextdestroys the input (removing it from the DOM), which firesblurbefore theremoveEventListenercall on line 110 can take effect (sinceblurfires synchronously on DOM removal in some browsers). This meanssave()can be called on Escape, sending an unwanted PATCH request.Consider setting a flag or removing the blur listener before modifying the DOM:
The fix is to call
input.removeEventListener('blur', save)beforecell.textContent = originalTexton the Escape branch — which is already the order shown, but note that in Firefox and some environmentsblurfires synchronously when the element is removed. A safer approach is to use a boolean guard:Why did I show this?
Category: bug
Comment Quality: high
Based on general best practices
Tools used:
list_changed_files,{'pattern': {'type': 'string', 'value': '**/*.py'}}list_changed_files,{'pattern': '**/*.py'}get_file_lines,{'file_path': 'src/todo_app/app.py', 'start_line': 1, 'end_line': 200}get_file_lines,{'file_path': 'tests/test_app.py', 'start_line': 1, 'end_line': 200}