Add Todo

We just hooked up a route to view a single todo. It’s natural now to hook up adding todos, meaning an HTML form element that posts to a view then re-displays the list of todos.

In this step we introduce running under the debugger:

  • Launch a debug session
  • Speedup the debugger if not on Windows
  • Set a breakpoint to stop execution, resume, and remove the breakpoint

Source for this step

Steps

  1. In models.py, instead of random ids, let’s increment the id by changing the Todo constructor (__init__) to:

    self.id = max([todo.id for todo in todos], default=0) + 1
    
  2. We’d like the models module to be self-contained. Let the Todo class handle adding a todo by adding a static method to Todo:

    @staticmethod
    def add(title):
        todo = Todo(title)
        todos.append(todo)
        return todo
    
  3. Change the populate_todos function to use the class method when adding:

    def populate_todos():
        Todo.add('First')
        Todo.add('Second')
        Todo.add('Third')
    
  4. Confirm this works by re-running the models.py run configuration.

  5. We now import randint but don’t use it. Let’s PyCharm fix this by clicking the menu item Code -> Optimize Imports.

  6. Let’s now run our web app under the debugger. Click on app in the 4. Run tool window at the bottom, then click the red square to stop the regular run.

  7. In the editor for app.py, right-click and choose Debug models.py.

    Note

    If you are on Mac or Linux, the Debugger window’s Console tab will say something like:

    warning: Debugger speedups using cython not found. Run '"/Users/pauleveritt/projects/pauleveritt/pauleveritt.github.io/env35/bin/python3.5" "/Applications/PyCharm.app/Contents/helpers/pydev/setup_cython.py" build_ext --inplace' to build.
    

    If so, click the hyperlink after the Run to install the Cython compiled extensions that speed up the debugger. You only need to do that once per installed Python on your system.

  8. Reload in the browser. Everything should work normally.

  9. In app.py, let’s add a route:

    @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/')
    
  10. request and redirect need to be imported. Click on each and do Alt-Enter to let PyCharm help you generate the import.

  11. We need an HTML form that collects and submits the todo title. Change def list_todos() to add this to the HTML:

    @app.route('/todo/')
    def list_todos():
        todos = Todo.list()
        div = '<div><a href="/todo/{id}">{title}</a></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)
    
  12. Let’s use the debugger, setting a breakpoint to pause execution. In add_todo, on the line that performs Todo.add, click in the left margin beside the line to create a big red circle, aka a breakpoint.

  13. In your browser, reload the Todo Listing. Type something in the input box and press enter.

  14. PyCharm appears, with the debugger stopped on the line of the breakpoint. Notice also that the browser thinks it is still loading the page, waiting for the server response.

  15. Continue execution by clicking, in the Debug tool window, the green Resume button (it looks like a right arrow.)

  16. Let’s break on the next line. Click on the red circle to remove the first breakpoint, then click in the left margin beside the next line (where we return the value) to add a new breakpoint.

  17. In the browser, type a new value into the input and press enter.

  18. PyCharm appears, stopped on the next line.

  19. Note that you did not have to restart PyCharm when changing debugger information such as breakpoints.

  20. Remove this second breakpoint by clicking on the red circle.

  21. Continue execution by clicking the green Resume button resume.

  22. Your models.py should match the following:

    models.py in Add Todo
    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 todos
    
        @staticmethod
        def add(title):
            todo = Todo(title)
            todos.append(todo)
            return todo
    
        @staticmethod
        def get_id(todo_id):
            return [todo for todo in todos if todo.id == todo_id][0]
    
    
    def populate_todos():
        Todo.add('First')
        Todo.add('Second')
        Todo.add('Third')
    
    
    if __name__ == '__main__':
        populate_todos()
        first_todo = Todo.list()[0]
        first_id = first_todo.id
        print(Todo.get_id(first_id))
    
  23. Your app.py should match the following:

    app.py in Add 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}">{title}</a></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/')
    
    
    if __name__ == '__main__':
        populate_todos()
        app.run(debug=True)
    

Extra Credit

  1. Should we move the todos array into the Todo class as a class attribute?