In the intro web course we have relied on Javascript for handling all of our view logic when building out our dynamic web applications. Sites built in that manner are said to be Client Side Rendered (CSR) and they rely on techniques like AJAX to give the user an applike experience.

However, in this lab we will explore how we can build a web application without a single line of javascript. To do this, we would require our application server to handle all of our view logic instead of javascript. Hence an application of this type is said to be as Server Side Rendered.

The differences of SSR and CSR are explored in more depth in this series of articles.

Task 1

Start up the lab 4 workspace.

Jinja Workspace

As we will not be using javascript to render our views, we need another mechanism to write dynamic html. We shall use jinja2 which is a template engine, it allows us to write our html dynamically on the server as opposed to rendering in the client with javascript.

Template Inheritance


Jinja allows us to split up our html code into components called templates and pass data to them. We can also have templates inherit from other templates so that common markup do not have to be repeated.

Task 2

Open templates/layout.html.

<!doctype html>
<html>
  <head>
  
    <!-- <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}"> -->
     <!--Import Google Icon Font-->
    <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
    <!-- Compiled and minified CSS -->
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">
    <!--Let browser know website is optimized for mobile-->
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <title>{% block title %}{% endblock %}</title>
    <style>
      {% block styles %}{% endblock %}

      #alert{
        width: 80vw;
        height: 50px;
        padding: 15px;
        right: 10vw;
        line-height: 20px;
        border-radius: 4px;
        top: 90px;
        position: absolute;
        z-index: 10;
      }

      #close{
        font-size: 25px;
        position: absolute;
        top: 10px;
        right: 10px;
      }
    </style>

  </head>
  <body>
    <nav class="blue">
      <div class="nav-wrapper">
        <a href="#!" class="brand-logo center">{% block page %}{% endblock %}</a>
        {% block link %}{% endblock %}
      </div>
    </nav>

    <div id="content" >
 
      {% block content %}{% endblock %}
    </div>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
  </body>
</html>

This is our base template which means all other templates would inherit the markup inside of it.

The {% block title %}{% endblock %} code is jinja syntax which indicates areas of the template to be replaced by its children. Blocks must be uniquely named and also defined in a child template.


Now open templates/login.html.

{% extends "layout.html" %}
{% block title %}Login{% endblock %}
{% block page %}Login{% endblock %}

{% block link%}
    <ul id="nav-mobile" class="right">
        <li><a href="/signup">Signup</a></li>
    </ul>
{% endblock %}

{% block content %}
{% endblock %}

As this is a child template, an extends block must be given to indicate its parent template. The text/HTML specified in the blocks of a child template would be rendered within the corresponding blocks of its parent template.

Next if you look in app.py you will see that the root route returns a function call to render_template().

@app.route('/', methods=['GET'])
@app.route('/login', methods=['GET'])
def login_page():
  return render_template('login.html')

It will render whatever html file is specified and located in the templates directory. Hence the following result is achieved.

If you view the source of the page you will notice all of the parent's markup is included when the child is rendered.

We shall use the same token based authentication stored in the cookies for this lab. Next we shall implement our authentication UI and routes.

Task 3.1

Update content block in signup.html with the following markup to implement a signup form

<main class="container" style="margin-top: 100px">
   <form class="card col s12" id="loginForm" method="POST" action="/signup" style="padding:1em">
      
      <div class="row">
        <div class="input-field col s12">
          <input placeholder="Placeholder" name="email" type="email" class="validate">
          <label for="first_name">Email</label>
        </div>
      </div>
      <div class="row">
        <div class="input-field col s12">
          <input placeholder="Placeholder" name="username" type="text" class="validate">
          <label for="first_name">Username</label>
        </div>
      </div>
      <div class="row">
        <div class="input-field col s12">
          <input name="password" type="password" class="validate">
          <label for="password">Password</label>
        </div>
      </div>
      <div class="card-action">
        <input type="submit" class="blue text-white btn">
      </div>

  </form>
</main>


Task 3.2

Update content block in login.html with the following to implement a login form.

<main class="container" style="margin-top:100px">
   <form class="card col s12" id="loginForm" method="POST" action="/login" style="padding:1em">

      <div class="row">
        <div class="input-field col s12">
          <input placeholder="Placeholder" name="username" type="text" class="validate">
          <label for="first_name">Username</label>
        </div>
      </div>
      <div class="row">
        <div class="input-field col s12">
          <input name="password" type="password" class="validate">
          <label for="password">Password</label>
        </div>
      </div>
      <div class="card-action">
        <input type="submit" class="blue text-white btn">
      </div>
  </form>
</main>      

Note the action and method attributes on the forms. This tells the user which route to send the form data to when submitted and what http method to use.

That should add a login and signup pages to the site on / and /signup

Task 3.3

Update app.py to include the following routes for handling the submission of the forms on each page. These routes are named as action routes because they handle the submit action on a form, then redirect the user. Both routes will use a predefined login_user() function that works similar to the one in the previous lab.

@app.route('/signup', methods=['POST'])
def signup_action():
  data = request.form  # get data from form submission
  newuser = RegularUser(username=data['username'], email=data['email'], password=data['password'])  # create user object
  response = None
  try:
    db.session.add(newuser)
    db.session.commit()  # save user
    token = login_user(data['username'], data['password'])
    response = redirect(url_for('todos_page'))
    set_access_cookies(response, token)
    flash('Account Created!')  # send message
  except Exception:  # attempted to insert a duplicate user
    db.session.rollback()
    flash("username or email already exists")  # error message
    response = redirect(url_for('login_page'))
  return response

@app.route('/login', methods=['POST'])
def login_action():
  data = request.form
  token = login_user(data['username'], data['password'])
  print(token)
  response = None
  if token:
    flash('Logged in successfully.')  # send message to next page
    response = redirect(
        url_for('todos_page'))  # redirect to main page if login successful
    set_access_cookies(response, token)
  else:
    flash('Invalid username or password')  # send message to next page
    response = redirect(url_for('login_page'))
  return response

Task 3.4

Next we set up the home page of the application. Update app.py after the signup route to add a todos_page() route.

@app.route('/app', methods=['GET'])
@jwt_required()
def todos_page():
  return render_template('todo.html', current_user=current_user)

The route is protected by the @jwt_required decorator to ensure only logged in users can access it. Because the user is logged in.

current_user() imported would work in any route that has @jwt_required. It will resolve to the user object of the currently logged in user.

we also pass current_user to the template todo.html hence we have the following code.

{% block page %} {{ current_user.username }}'s' Todo{% endblock %}

Which prints the logged in user's username in the navbar.

Finally you can try creating a user and logging in.

After logging in we will be directed to the todo page with a flash message and text welcoming the user.

If you reload the page, the flash message would disappear but the user is still logged in.

If you click on the start of the url bar you will see the cookies on the browser, if you click through you can find the values in the cookie. This allows users to remain logged in between page loads.

If you open the chrome debugger, open the network tab and reload the page you can view the details of the request to todos. In the headers tab you can see the value of the cookie automatically sent in the request.

Now that users are logged in we can have them manipulate data in the application. First we create a form to add todos

Task 4.1

Update templates/todo.html

{% extends "layout.html" %}
{% block title %}Todo App{% endblock %}
{% block page %} {{ current_user.username }}'s' Todo{% endblock %}

{% block link%}
    <ul id="nav-mobile" class="right">
        <li><a href="/logout">Logout</a></li>
    </ul>
{% endblock %}

{% block styles %}
  div.card{
    margin:0;
  }

  #result{
    margin-top:10px;
    height: 40vh;
    overflow-y: scroll;
  }

  .card{
    height: 265px;
    padding-top: 50px;
  }

{% endblock %}

{% block content %} 

<main class="container" style="padding-top: 75px">
  <form name="addForm" class="card" method="POST" action="/createTodo">
    <div class="card-content">
      <span class="card-title">Create Todo</span>
        <div class="input-field">
          <input type="text" name="text" placeholder="Enter Todo Text" class="materialize-textarea">
          <label for="text">Enter Todo Text</label>
        </div>
    </div>
    <div class="card-actions">
        <div class="row">
          <div class="col sm12 m4 offset-m8">
            <input class="btn blue right" type="submit" value="SAVE" />
          </div>
        </div>
    </div>
  </form>
</main>
{% endblock %}

We have also moved the username into the Navbar to save space. The page should now look like the following

Task 4.2

Next, we add the following todos_action() route that will handle the submit action on the create todo form and create our todo for the logged in user.

@app.route('/createTodo', methods=['POST'])
@jwt_required()
def create_todo_action():
  data = request.form
  current_user.add_todo(data['text'])
  flash('Created')
  return redirect(url_for('todos_page'))

If we test the form we should see the flash message indicating the creation of the todo.

Next we'd like to render our todos on the page. This is a simple matter of querying the data and passing it to the template.

We make use of the for syntax from jinja2 to render a list item for every todo object in current_user.todos which was passed by the route. Now if you reload the todo page you should see a list of todos.

We also place an if statement in the done checkbox to check off todos which are completed.

Task 4.4

Update todo.html adding the following content under the form

  <ul class="collection " id="result">
      {% for todo in current_user.todos %}
           <li class="collection-item">
              <form class="row" method="POST" action="toggle/{{todo.id}}" >
                <span class="card-title">{{todo.text}}
                  <label class="right">
                    <input type="checkbox" name="done" onchange="this.form.submit()" {%  if todo.done %} checked {% endif %} />
                    <span>Done</span>
                  </label>
                </span>
              </form>
              <div class="row">
                <a href="/editTodo/{{todo.id}}">EDIT</a>
                <a href="/deleteTodo/{{todo.id}}">DELETE</a>
              </div>
          </li>
      {% endfor %}
  </ul>

Now the todos should be rendered on the site.

We shall explore how the inputs work in the next section.

As we won't be using any javascript for this app all interactivity must be done via requests in the browser. This can be achieved by either sending a POST/GET request via a HTML form or a get request via hyperlinks.

Task 5.1

Update app.py to add the following toggle route to update the appropriate todo based on the ID given in the url from the table.

@app.route('/toggle/<id>', methods=['POST'])
@jwt_required()
def toggle_todo_action(id):
  todo = current_user.toggle_todo(id)
  if todo is None:
    flash('Invalid id or unauthorized')
  else:
    flash(f'Todo { "done" if todo.done else "not done" }!')
  return redirect(url_for('todos_page'))

Now when the "toggle done" button is clicked the server updates the appropriate todo and renders its new status on the page.

Next we are going to add functionality to the "edit" button.

Task 5.2

Update app.py and add the following route

@app.route('/editTodo/<id>', methods=["POST"])
@jwt_required()
def edit_todo_action(id):
  data = request.form
  res = current_user.update_todo(id, data["text"])
  if res:
    flash('Todo Updated!')
  else:
    flash('Todo not found or unauthorized')
  return redirect(url_for('todos_page'))

We define a route parameter in the route (id) so that when the page is requested the server knows which todo is to be updated.

Task 5.3

Finally, update templates/edit.html

{% extends "layout.html" %}
{% block title %}Todo App{% endblock %}
{% block page %} Editing Todo{% endblock %}

{% block link%}
    <ul id="nav-mobile" class="right">
        <li><a href="/logout">Logout</a></li>
    </ul>
{% endblock %}

{% block styles %}
  div.card{
    margin:0;
  }

  #result{
    margin-top:10px;
    height: 55vh;
    overflow-y: scroll;
  }

  .card{
    height: 265px;
    padding-top: 50px;
  }

{% endblock %}

{% block content %} 
      
        <form name="editForm" class="card" method="POST" action="/editTodo/{{todo.id}}">
          <div class="card-content">
            <span class="card-title">Editing Todo - {{ todo.text }}</span>
              <div class="input-field">
                <input type="text" name="text" placeholder="Enter new todo text" class="materialize-textarea">
                <label for="text">Enter Todo Text</label>
              </div>
          </div>
          <div class="card-actions">
              <div class="row">
                <div class="col sm12 m4 offset-m8 ">
                  <input class="btn blue right" type="submit" value="Update" />
                  <a class="btn white black-text right" style="margin-right: 5px" href="/app">Cancel</a>
                </div>
              </div>
          </div>
        </form>

       <main class="container">
          <ul class="collection " id="result">
            {% if current_user.is_authenticated %}
                {% for todo in todos %}
                     <li class="collection-item">
                        <form  method="POST" action="toggle/{{todo.id}}" >
                          <span class="card-title">{{todo.text}}
                            <label class="right">
                              <input type="checkbox" name="done" onchange="this.form.submit()" {%  if todo.done %} checked {% endif %} />
                              <span>Done</span>
                            </label>
                          </span>
                        </form>
                        <div class="card-action">
                          <a href="/editTodo/{{todo.id}}">EDIT</a>
                          <a href="/deleteTodo/{{todo.id}}">DELETE</a>
                        </div>
                    </li>
                {% endfor %}
            {% endif %}
          </ul>
    </main>
{% endblock %}

The action attribute receives the id of the todo from the route.

Now clicking the update button should take you to the edit page with the form. On submission you should be redirected to the todos page showing the updated todo. Notice how the edit page also has redundant view logic as the app page. This can give a more seamless experience for the user giving options to still view and operate on other todos.

We can add a link to each todo which will send a get request to delete the todo.

Task 6

Update app.py to include the deletion route

@app.route('/deleteTodo/<id>', methods=["GET"])
@jwt_required()
def delete_todo_action(id):
  res = current_user.delete_todo(id)
  if res == None:
    flash('Invalid id or unauthorized')
  else:
    flash('Todo Deleted')
  return redirect(url_for('todos_page'))

Now users should be able to delete their todos when the corresponding delete button is clicked.

Logging out with flask login is just a matter of calling the logout_user() function. Next we will implement a logout button.

todo.html has a custom block called link that provides a logout link on the todos home page.

In app.py add the following logout route is already provided

@app.route('/logout', methods=['GET'])
@jwt_required()
def logout_action():
  flash('Logged Out')
  response = redirect(url_for('login_page'))
  unset_jwt_cookies(response)
  return response

Task 7

Try clicking the logout button. You should see the following

You can view a completed version of this lab at this point here Completed Workspace

The todo application was updated to work with multiple user classes. Admins can now login to perform actions with higher privileges.

In order to achieve this we would have to make a custom wrapper @login_required() to restrict routes to different types of users.

Task 8.1

Add the following to the top of app.py with the other login functions

def login_required(required_class):
  def wrapper(f):
      @wraps(f)
      @jwt_required()  # Ensure JWT authentication
      def decorated_function(*args, **kwargs):
        user = required_class.query.get(get_jwt_identity())
        if user.__class__ != required_class:  # Check class equality
            return jsonify(message='Invalid user role'), 403
        return f(*args, **kwargs)
      return decorated_function
  return wrapper

This lets us restrict a route to a certain type of user eg @login_required(Admin) to ensure only admins can access.

Now let's create a template for our admin class.

Task 8.2

Create a file named admin.html with the following content.

{% extends "layout.html" %}
{% block title %}Admin View{% endblock %}
{% block page %}Admin View{% endblock %}

{% block link%}
    <ul id="nav-mobile" class="right">
        <li><a href="/todo-stats">Stats</a></li>
        <li><a href="/logout">Logout</a></li>
    </ul>
{% endblock %}

{% block styles %}

{% endblock %}

{% block content %} 
      <main class="container" style="padding-top:100px">

        
        <form  method="GET" action="/admin">
          <div class="row">
            <div class="col m10 input-field">
              <input class="blue-text"  name="q" type="search" placeholder="Search">
            </div>
            <div class="col m2 input-field">
              <div class="row">
                <button type="submit" class="btn blue">
                  <i class="material-icons">search</i>
                </button>
                <button type="button" onclick="q.value=''; this.form.submit()" class="btn blue">
                  <i class="material-icons">close</i>
                </button>
              </div>

            </div>
          </div>


        </form>
        
        <table>
          <thead>
            <tr>
                <th>ID</th>
                <th>User</th>
                <th>Text</th>
                <th>Done</th>
            </tr>
          </thead>
          
          <tbody>
            {% for todo in todos %}
            <tr>
              <td>{{todo.id}}</td>
              <td>{{todo.user.username}}</td>
              <td>{{todo.text}}</td>
              <td>{{todo.done}}</td>
            </tr>
            {% endfor %}
 
          </tbody>
        </table>

        
      </main>
{% endblock %}

Task 8.2

Now let's add a view route for that template update app.py with the following

@app.route('/admin')
@login_required(Admin)
def admin_page():
  todos = Todo.query.all()
  return render_template('admin.html', todos=todos)

Task 8.4

Finally we update our login route in app.py to redirect admins to the admin page.

@app.route('/login', methods=['POST'])
def login_action():
  data = request.form
  token = login_user(data['username'], data['password'])
  print(token)
  response = None
  user = User.query.filter_by(username=data['username']).first()
  if token:
    flash('Logged in successfully.')  # send message to next page
    if user.type == "regular user":
      response = redirect(url_for('todos_page'))
    else :
      response = redirect(url_for('admin_page'))  # redirect to main page if login successful
    set_access_cookies(response, token)
  else:
    flash('Invalid username or password')  # send message to next page
    response = redirect(url_for('login_page'))

When we login with admin credentials pam, pampas we would now see the admin view.

Pagination is a common UI pattern that allows the user to browse a subset of results instead of the entire dataset, this helps with performance and UX.

We use models.query.paginate() to paginate our results by specifying a page number and amount per page parameter.

Task 9.1

Update the following search_todos method to the Admin model in models.py

def search_todos(self, page):
  matching_todos = Todo.query
  return matching_todos.paginate(page=page, per_page=10)


This will then paginate the results based on the page parameter passed to the method.

Task 9.2

Next, we update our admin route to receive a page query parameter and pass it to the search todos method template.

@app.route('/admin')
@login_required(Admin)
def admin_page():
  page = request.args.get('page', 1, type=int)
  todos = current_user.search_todos(page)
  return render_template('admin.html', todos=todos, page=page)

Task 9.3

Finally we update our template with some rather involved templating code to show the pagination controls. Add the following snippet under the table in admin.html

    <!--- Table end -->    
<div class="row">

          <ul class="pagination col s12 center">
            {% if todos.prev_num %}
            <li class="waves-effect">
                <a href="{{ url_for('admin_page', page=todos.prev_num) }}"><i class="material-icons">chevron_left</i></a>
            </li>
            {% else %}
            <li class="disabled">
                <a href="#!"><i class="material-icons">chevron_left</i></a>
            </li>
            {% endif %}
        
            {% for page_num in todos.iter_pages() %}
                {% if page_num %}
                    {% if todos.page == page_num %}
                    <li class="active blue"><a href="#!">{{ page_num }}</a></li>
                    {% else %}
                    <li class="waves-effect"><a href="{{ url_for('admin_page', page=page_num) }}">{{ page_num }}</a></li>
                    {% endif %}
                {% else %}
                    <li class="disabled"><a href="#!">...</a></li>
                {% endif %}
            {% endfor %}
        
            {% if todos.has_next %}
            <li class="waves-effect">
                <a href="{{ url_for('admin_page', page=todos.next_num) }}"><i class="material-icons">chevron_right</i></a>
            </li>
            {% else %}
            <li class="disabled">
                <a href="#!"><i class="material-icons">chevron_right</i></a>
            </li>
            {% endif %}
        </ul>
          
        </div>

You should now have functional pagination controls

Notice how the current page is passed via query parameters in the url.

Next we shall implement the functionality of the search bar. Search bars typically let you search across various fields on a dataset i.e. username and text so some advanced sqlalchemy querying would be needed.

Task 10.1

Update the search_todo() method of the Admin class in models.py as follows:

  def search_todos(self, q, page): 
    matching_todos = None
  
    if q!="" :
      matching_todos = Todo.query.join(RegularUser).filter(
        db.or_(RegularUser.username.ilike(f'%{q}%'), Todo.text.ilike(f'%{q}%'), Todo.id.ilike(f'%{q}%'))
      )
    else:
      matching_todos = Todo.query
      
    return matching_todos.paginate(page=page, per_page=10)


Here we perform a join on Todo and RegularUser so we can search the username and text fields respectively, we use db.or to union the filter results. If the search query is empty it will return all results by default.

Task 10.2

Next we update our admin route to look out for a new query parameter q which will store the search query and pass it to the search method and template.

@app.route('/admin')
@login_required(Admin)
def admin_page():
  page = request.args.get('page', 1, type=int)
  q = request.args.get('q', default='', type=str)
  todos = current_user.search_todos(q, page)
  return render_template('admin.html', todos=todos, page=page, q=q)

Task 10.3

Next update the form in our admin.html template to show the value of the current search query in the search box.

...
<input class="blue-text" value="{{q}}" name="q" type="search" placeholder="Search">
...

Additionally in order for our application to maintain state and context, we need all links to preserve all query parameter values. Hence The links if tha pagination need to be updated to include the parameter q.

Task 10.4

Replace the div.row element under the table of the admin.html as follows:

    <!-- Table End -->
    <div class="row">
        
          <ul class="pagination col s12 center">
              {% if todos.prev_num %}
                <li class="waves-effect">
                  <a href="{{ url_for('admin_page', page=todos.prev_num, q=q, done=done) }}"><i class="material-icons">chevron_left</i></a>
              </li>
              {% else %}
                <li class="disabled">
                  <a href="#!"><i class="material-icons">chevron_left</i></a>
              </li>
              {% endif %}
          
              {% for page_num in todos.iter_pages() %}
                  {% if page_num %}
                      {% if todos.page == page_num %}
                        <li class="active blue"><a href="#!">{{ page_num }}</a></li>
                      {% else %}
                        <li class="waves-effect"><a href="{{ url_for('admin_page', page=page_num, q=q, done=done) }}">{{ page_num }}</a></li>
                      {% endif %}
                  {% else %}
                      <li class="disabled"><a href="#!">...</a></li>
                  {% endif %}
              {% endfor %}
          
              {% if todos.has_next %}
                <li class="waves-effect">
                  <a href="{{ url_for('admin_page', page=todos.next_num, q=q, done=done) }}"><i class="material-icons">chevron_right</i></a>
              </li>
              {% else %}
                <li class="disabled">
                  <a href="#!"><i class="material-icons">chevron_right</i></a>
              </li>
              {% endif %}
          </ul>
          
      </div>
   <!-- Main end —->

This will ensure that the search is preserved even when navigating the pagination by forwarding the page and q parameters in all urls. Note a done parameter is also passed but not yet used.

Now the search bar should be functional with pagination! We can click the X button to submit an empty search query hence returning all results.

Next we want to add some filter controls to filter out done, or not done todos.

Task 11.1

We update our search_todos() method to take a done parameter which can be "true", "false" or "any" and filter the result based on the value.

  def search_todos(self, q, done, page): 
      matching_todos = None
    
      if q!="" and done=="any" :
        #search query and done is any - just do search
        matching_todos = Todo.query.join(RegularUser).filter(
          db.or_(RegularUser.username.ilike(f'%{q}%'), Todo.text.ilike(f'%{q}%'), Todo.id.ilike(f'%{q}%'))
        )
      elif q!="":
        #search query and done is true or false - search then filter by done
        is_done = True if done=="true" else False
        matching_todos = Todo.query.join(RegularUser).filter(
          db.or_(RegularUser.username.ilike(f'%{q}%'), Todo.text.ilike(f'%{q}%'), Todo.id.ilike(f'%{q}%')),
          Todo.done == is_done
        )
      elif done != "any":
        # done is true/false but no search query - filter by done only
        is_done = True if done=="true" else False
        matching_todos = Todo.query.filter_by(
            done= is_done
        )
      else:
        # done is any and no search query - all results
        matching_todos = Todo.query
        
      return matching_todos.paginate(page=page, per_page=10)

Task 11.2

Now our admin route has to handle a new query parameter

@app.route('/admin')
@login_required(Admin)
def admin_page():
  page = request.args.get('page', 1, type=int)
  q = request.args.get('q', default='', type=str)
  done = request.args.get('done', default='any', type=str)
  todos = current_user.search_todos(q, done, page)
  return render_template('admin.html', todos=todos, q=q, page=page, done=done)


We update our template to provide controls for filtering the done status.

Task 11.3

Add the following code within the form element in admin.htm as a third rowl to provide filtering controls for the user.

          <div class="row">
             <label>
              <input class="with-gap blue" name="done" type="radio" value="true" onchange="this.form.submit()" {{ "checked" if done=="true" }} />
              <span>Done</span>
            </label>
            <label>
              <input class="with-gap blue" name="done" type="radio" value="false" onchange="this.form.submit()" {{ "checked" if done=="false" }} />
              <span>Not Done</span>
            </label>
            <label>
              <input class="with-gap blue" name="done" type="radio" value="any" onchange="this.form.submit()" {{ "checked" if done=="any" }} />
              <span>Any</span>
            </label>
          </div>

The radio buttons will be filled based on the value of the done query parameter.

If done correctly you should be able to filter and search with pagination making use of the done parameter we added to our links earlier.

Next we want to add a visualization to our application. We can add a highcharts pie chart to our page.

If you look in stats.html you should find code derived from this highcharts example for rendering a pie chart based on data in the database.

Note the use of JavaScript to fetch data from a route ‘todo-stats", convert it into a relevant format for highcharts then call highcharts to render the chart on the page.

Task 12.1

Create a stats.html template with the following

  {% extends "layout.html" %}
{% block title %}Stats View{% endblock %}
{% block page %}Stats View{% endblock %}

{% block link%}
    <ul id="nav-mobile" class="right">
        <li><a href="/admin">Admin</a></li>
        <li><a href="/logout">Logout</a></li>
    </ul>
{% endblock %}

{% block styles %}
  .highcharts-figure,
  .highcharts-data-table table {
    min-width: 320px;
    max-width: 660px;
    margin: 1em auto;
  }

  .highcharts-data-table table {
    font-family: Verdana, sans-serif;
    border-collapse: collapse;
    border: 1px solid #ebebeb;
    margin: 10px auto;
    text-align: center;
    width: 100%;
    max-width: 500px;
  }

  .highcharts-data-table caption {
    padding: 1em 0;
    font-size: 1.2em;
    color: #555;
  }

  .highcharts-data-table th {
    font-weight: 600;
    padding: 0.5em;
  }

  .highcharts-data-table td,
  .highcharts-data-table th,
  .highcharts-data-table caption {
    padding: 0.5em;
  }

  .highcharts-data-table thead tr,
  .highcharts-data-table tr:nth-child(even) {
    background: #f8f8f8;
  }

  .highcharts-data-table tr:hover {
    background: #f1f7ff;
  }
{% endblock %}

{% block content %}
<script src="https://code.highcharts.com/highcharts.js"></script>
<script src="https://code.highcharts.com/modules/exporting.js"></script>
<script src="https://code.highcharts.com/modules/export-data.js"></script>
<script src="https://code.highcharts.com/modules/accessibility.js"></script>

<main class="container" style="padding-top:100px">
  <secion class="col m12">
    <figure class="highcharts-figure">
      <div id="container"></div>
    </figure>
  </secion>
</main>
<script>

  function drawChart(data){
    Highcharts.chart('container', {
      chart: {
        plotBackgroundColor: null,
        plotBorderWidth: null,
        plotShadow: false,
        type: 'pie'
      },
      title: {
        text: 'Todo share by User',
        align: 'left'
      },
      tooltip: {
        pointFormat: '{series.name}: <b>{point.percentage:.1f}%</b>'
      },
      accessibility: {
        point: {
          valueSuffix: '%'
        }
      },
      plotOptions: {
        pie: {
          allowPointSelect: true,
          cursor: 'pointer',
          dataLabels: {
            enabled: false
          },
          showInLegend: true
        }
      },
      series: [{
        name: 'Users',
        colorByPoint: true,
        data
      }]
    });
  }

  function convertData(data){
    res = []
    for(user in data){
      res.push({
        name:user,
        y: data[user]
      });      
    }

    return res;
  }

  async function getData(){
    let res = await fetch('/todo-stats');
    let data = await res.json();
    data = convertData(data);
    drawChart(data);

  }

  getData();

</script>
{% endblock %}

Task 12.3

Now update Admin, adding a method for returning a frequency count of the todos for all users.

def get_todo_stats(self):
    todos = Todo.query.all()
    res = {}
    for todo in todos:
      if todo.user.username in res:
        res[todo.user.username]+=1
      else:
        res[todo.user.username]=1
    return res

Task 12.4

Finally, add the following routes to app.py to call the method.

@app.route('/todo-stats', methods=["GET"])
@login_required(Admin)
def todo_stats():
  return jsonify(current_user.get_todo_stats())

@app.route('/stats')
@login_required(Admin)
def stats_page():
  return render_template('stats.html')


Now navigating to /todo-stats will render the following chart.

That's the end of the lab. You've just built an entire CRUD application without writing any javascript. This is possible because we shifted all of our view logic to the server side.

Completed Workspace

References & Additional Reading