Delete Todo

Deleting a todo is an important part of our application. Let’s add that feature while showing more about debugging:

  • Run to Cursor
  • Evaluate Expression
  • Console
  • Moving backwards in frames

Source for this step

Steps

This time we’re going to reverse our mode of implementation and start instead with the UI side in app.py.

  1. In app.py, change list_todos to have a delete button:

    @app.route('/todo/')
    def list_todos():
        todos = Todo.list()
        div = '''<div><a href="/todo/{id}/delete">{title}</a>
        <form method="POST" action="/todo/{id}/delete" style="display: inline">
            <input type="submit" value="x"/>
        </form>
        </div>'''
        form = '''<form method="POST" action="add">
            <input name="todo_id" placeholder="Add todo..."/>
            </form>
        '''
        items = [div.format(id=t.id, title=t.title) for t in todos]
        items.append(form)
        return '\n'.join(items)
    
  2. This form posts to a /delete route on a todo, so let’s add that route:

    @app.route('/todo/<int:todo_id>/delete', methods=['POST'])
    def delete_todo(todo_id):
        todo = Todo.get_id(todo_id)
        return redirect('/todo/')
    
  3. Put a breakpoint on the first line inside this delete_todo route. Reload your browser (restarting your app in the debugger if necessary) and click on the x for one of the todos.

  4. When execution stops in the route, confirm that todo_id is an integer.

  5. Click Resume resume to continue execution.

  6. In models.py, add a method for deletion:

    def delete(self):
        todos.remove(self)
    
  7. Back in app.py, add a line to delete_todo that calls this new method:

    @app.route('/todo/<int:todo_id>/delete', methods=['POST'])
    def delete_todo(todo_id):
        todo = Todo.get_id(todo_id)
        todo.delete()
        return redirect('/todo/')
    
  8. Reload browser and click X to delete the second todo. Execution stops at your breakpoint.

  9. Put your cursor in the delete method in models.py.

  10. In the debugger, click Run to Cursor runtocursor.

  11. Let’s make sure this is the correct todo. Click the Evaluate Expression button evaluate.

  12. In the popup window, enter self.id == 2 to confirm that this is the correct todo. (Note: it might not be 2 based on the sorting and random title generation.)

  13. Click the Debug Tool Window’s Console tab (beside the Debugger tab), then click the Show Python Prompt button prompt.

  14. Let’s see what todo_int was defined as in the earlier stack frame by clicking delete_todo in the Frames window. Variables updates to show us the new scope.

  15. Click the Resume button resume.

  16. Your models.py should match the following:

    models.py in Delete Todo
    from random import choice
    
    todos = []
    
    
    class Todo:
        def __init__(self, title):
            self.title = title
            self.display_fmt = 'Todo {todo_id}'
            self.id = max([todo.id for todo in todos], default=0) + 1
    
        def __repr__(self):
            return self.display
    
        @property
        def display(self):
            return self.display_fmt.format(todo_id=self.id)
    
        @staticmethod
        def list():
            return sorted(todos, key=lambda todo: todo.title)
    
        @staticmethod
        def add(title):
            todo = Todo(title)
            todos.append(todo)
            return todo
    
        def delete(self):
            todos.remove(self)
    
        @staticmethod
        def get_id(todo_id):
            return [todo for todo in todos if todo.id == todo_id][0]
    
    
    def populate_todos():
        random_verbs = ['Make', 'Buy', 'Organize', 'Finish']
        random_nouns = ['Red', 'Apple', 'Blue', 'Tomato', 'Green', 'Pear',
                        'Black', 'Bean', 'Yellow', 'Banana', 'Brown', 'Raisin']
        for i in range(5):
            v = choice(random_verbs)
            w1 = choice(random_nouns)
            w2 = choice(random_nouns)
            title = '{v} {w1} {w2}'.format(v=v, w1=w1, w2=w2)
            Todo.add(title)
    
    
    if __name__ == '__main__':
        populate_todos()
        first_todo = Todo.list()[0]
        first_id = first_todo.id
        print(Todo.get_id(first_id))
    
  17. Your app.py should match the following:

    app.py in Delete Todo
    from flask import Flask, request
    from flask import redirect
    
    from models import populate_todos, Todo
    
    app = Flask(__name__)
    
    
    @app.route('/')
    def home_page():
        return 'Hello World! <a href="/todo/">Todos</a>'
    
    
    @app.route('/todo/')
    def list_todos():
        todos = Todo.list()
        div = '''<div><a href="/todo/{id}/delete">{title}</a>
        <form method="POST" action="/todo/{id}/delete" style="display: inline">
            <input type="submit" value="x"/>
        </form>
        </div>'''
        form = '''<form method="POST" action="add">
            <input name="todo_id" placeholder="Add todo..."/>
            </form>
        '''
        items = [div.format(id=t.id, title=t.title) for t in todos]
        items.append(form)
        return '\n'.join(items)
    
    
    @app.route('/todo/<int:todo_id>')
    def show_todo(todo_id):
        todo = Todo.get_id(todo_id)
        fmt = '<h1>Todo {todo_id}</h1><p>{title}</p>'
        return fmt.format(todo_id=todo.id, title=todo.title)
    
    
    @app.route('/todo/add', methods=['POST'])
    def add_todo():
        todo_id = request.form['todo_id']
        if todo_id:
            Todo.add(todo_id)
        return redirect('/todo/')
    
    
    @app.route('/todo/<int:todo_id>/delete', methods=['POST'])
    def delete_todo(todo_id):
        todo = Todo.get_id(todo_id)
        todo.delete()
        return redirect('/todo/')
    
    
    if __name__ == '__main__':
        populate_todos()
        app.run(debug=True)