User login and storage of computed results

We now want to make an app where the computed results can be stored in a database. To this end, each user must create an account and login to this account for archiving results and for browsing previous runs of the application. More files are needed for this purpose, compared to the previous apps, and the files are located in the login directory.

Required additional software

Three more packages are needed for this more advanced application:

Installation is done by

sudo pip install --upgrade flask-login
sudo pip install --upgrade flask-sqlalchemy
sudo pip install --upgrade flask-mail

The compute part

The compute.py file contains the compute function from the vib2 app in the section Avoiding plot files.

There is quite much Flask code required for user accounts and login. The files here were generated by the Parampool package and then edited and specialized to the present application. In general, it is not recommended to hack further on the example given here, but rather utilize Parampool to generate web apps with user accounts and login.

The interfaces of the app

The first page after starting the app (python controller.py) shows input fields for the numerical parameters, but there are also links to the right for registering a new user or logging in to an existing account:

Clicking on Register brings up a registration page:

Already registered users can just log in:

Then the fields with input parameters are shown again. After pressing Compute we are left with a combination of input and results, plus a field where the user can write a comment about this simulation:

All simulations (input data, results, and comment) are stored in a database. Clicking on Previous simulation brings us to an overview of what is in the database:

Here, one can browse previous results, remove entries, or erase the whole database.

The source code

Rather than explaining everything about the source code in detail, we primarily list the various code files below. A starting point is the central controller.py file, which is now quite lengthy and involves four different URLs (the input page, the login page, the registration page, and the previous results page).

import os
from compute import compute as compute_function

from flask import Flask, render_template, request, redirect, url_for
from forms import ComputeForm
from db_models import db, User, Compute
from flask.ext.login import LoginManager, current_user, \
     login_user, logout_user, login_required
from app import app

login_manager = LoginManager()
login_manager.init_app(app)

@login_manager.user_loader
def load_user(user_id):
    return db.session.query(User).get(user_id)

# Path to the web application
@app.route('/', methods=['GET', 'POST'])
def index():
    result = None
    user = current_user
    form = ComputeForm(request.form)
    if request.method == "POST":
        if form.validate():

            result = compute_function(form.A.data, form.b.data,
                                      form.w.data, form.T.data)
            if user.is_authenticated():
                object = Compute()
                form.populate_obj(object)
                object.result = result
                object.user = user
                db.session.add(object)
                db.session.commit()

                # Send email notification
                if user.notify and user.email:
                    send_email(user)
    else:
        if user.is_authenticated():
            if user.Compute.count() > 0:
                instance = user.Compute.order_by('-id').first()
                result = instance.result
                form = populate_form_from_instance(instance)

    return render_template("view.html", form=form,
                           result=result, user=user)


def populate_form_from_instance(instance):
    """Repopulate form with previous values"""
    form = ComputeForm()
    for field in form:
        field.data = getattr(instance, field.name)
    return form

def send_email(user):
    from flask.ext.mail import Mail, Message
    mail = Mail(app)
    msg = Message("Compute Computations Complete",
                  recipients=[user.email])
    msg.body = """
A simulation has been completed by the Flask Compute app.
Please log in at

http://localhost:5000/login

to see the results.

---
If you don't want email notifications when a result is found,
please register a new user and leave the 'notify' field
unchecked.
"""
    mail.send(msg)

@app.route('/reg', methods=['GET', 'POST'])
def create_login():
    from forms import register_form
    form = register_form(request.form)
    if request.method == 'POST' and form.validate():
        user = User()
        form.populate_obj(user)
        user.set_password(form.password.data)

        db.session.add(user)
        db.session.commit()

        login_user(user)
        return redirect(url_for('index'))
    return render_template("reg.html", form=form)

@app.route('/login', methods=['GET', 'POST'])
def login():
    from forms import login_form
    form = login_form(request.form)
    if request.method == 'POST' and form.validate():
        user = form.get_user()
        login_user(user)
        return redirect(url_for('index'))
    return render_template("login.html", form=form)

@app.route('/logout')
@login_required
def logout():
    logout_user()
    return redirect(url_for('index'))

@app.route('/old')
@login_required
def old():
    data = []
    user = current_user
    if user.is_authenticated():
        instances = user.Compute.order_by('-id').all()
        for instance in instances:
            form = populate_form_from_instance(instance)

            result = instance.result
            if instance.comments:
                comments = "<h3>Comments</h3>" + instance.comments
            else:
                comments = ''
            data.append(
                {'form':form, 'result':result,
                 'id':instance.id, 'comments': comments})

    return render_template("old.html", data=data)

@app.route('/add_comment', methods=['GET', 'POST'])
@login_required
def add_comment():
    user = current_user
    if request.method == 'POST' and user.is_authenticated():
        instance = user.Compute.order_by('-id').first()
        instance.comments = request.form.get("comments", None)
        db.session.commit()
    return redirect(url_for('index'))

@app.route('/delete/<id>', methods=['GET', 'POST'])
@login_required
def delete_post(id):
    id = int(id)
    user = current_user
    if user.is_authenticated():
        if id == -1:
            instances = user.Compute.delete()
        else:
            try:
                instance = user.Compute.filter_by(id=id).first()
                db.session.delete(instance)
            except:
                pass

        db.session.commit()
    return redirect(url_for('old'))


if __name__ == '__main__':
    if not os.path.isfile(os.path.join(
        os.path.dirname(__file__), 'sqlite.db')):
        db.create_all()
    app.run(debug=True)

Creation of the app object is put in a separate file app.py:

import os
from flask import Flask

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///sqlite.db'
app.secret_key = os.urandom(24)

# Email settings
import base64
app.config.update(
        MAIL_SERVER='smtp.gmail.com',
        MAIL_PORT=587,
        MAIL_USE_TLS=True,
        MAIL_USERNAME = 'cbcwebsolvermail@gmail.com',
        MAIL_PASSWORD = base64.decodestring('WGlmZmljdox0UFch'),
        MAIL_DEFAULT_SENDER = 'Some name <name@gmail.com>'
        )

The forms for the compute function and for the login is stored in a file called forms.py:

import wtforms as wtf
from math import pi

class ComputeForm(wtf.Form):
    A = wtf.FloatField(label='\( A \)', default=1.0,
        validators=[wtf.validators.InputRequired()])
    b = wtf.FloatField(label='\( b \)', default=0.0,
        validators=[wtf.validators.InputRequired()])
    w = wtf.FloatField(label='\( w \)', default=pi,
        validators=[wtf.validators.InputRequired()])
    T = wtf.FloatField(label='\( T \)', default=18,
        validators=[wtf.validators.InputRequired()])
    resolution = wtf.IntegerField(label='resolution', default=500,
        validators=[wtf.validators.InputRequired()])

from db_models import db, User
import flask.ext.wtf.html5 as html5

# Standard Forms
class register_form(wtf.Form):
    username = wtf.TextField(
        label='Username', validators=[wtf.validators.Required()])
    password = wtf.PasswordField(
        label='Password', validators=[
            wtf.validators.Required(),
            wtf.validators.EqualTo(
                'confirm', message='Passwords must match')])
    confirm  = wtf.PasswordField(
        label='Confirm Password',
        validators=[wtf.validators.Required()])
    email    = html5.EmailField(label='Email')
    notify   = wtf.BooleanField(label='Email notifications')

    def validate(self):
        if not wtf.Form.validate(self):
            return False

        if self.notify.data and not self.email.data:
            self.notify.errors.append(
        'Cannot send notifications without a valid email address')
            return False

        if db.session.query(User).filter_by(
            username=self.username.data).count() > 0:
            self.username.errors.append('User already exists')
            return False

        return True

class login_form(wtf.Form):
    username = wtf.TextField(
        label='Username', validators=[wtf.validators.Required()])
    password = wtf.PasswordField(
        label='Password', validators=[wtf.validators.Required()])

    def validate(self):
        if not wtf.Form.validate(self):
            return False

        user = self.get_user()

        if user is None:
            self.username.errors.append('Unknown username')
            return False

        if not user.check_password(self.password.data):
            self.password.errors.append('Invalid password')
            return False

        return True

    def get_user(self):
        return db.session.query(User).filter_by(
            username=self.username.data).first()

Database-related code for the SQLAlchemy database is collected in db_models.py:

from flask.ext.sqlalchemy import SQLAlchemy
from werkzeug import generate_password_hash, check_password_hash
from app import app

db = SQLAlchemy(app)

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(50), unique=True)
    pwhash = db.Column(db.String())
    email = db.Column(db.String(120), nullable=True)
    notify = db.Column(db.Boolean())

    def __repr__(self):
        return '<User %r>' % (self.username)

    def check_password(self, pw):
        return check_password_hash(self.pwhash, pw)

    def set_password(self, pw):
        self.pwhash = generate_password_hash(pw)

    def is_authenticated(self):
        return True

    def is_active(self):
        return True

    def is_anonymous(self):
        return False

    def get_id(self):
        return self.id

class Compute(db.Model):
    id = db.Column(db.Integer, primary_key=True)

    A = db.Column(db.String())
    b = db.Column(db.String())
    w = db.Column(db.String())
    T = db.Column(db.String())
    resolution = db.Column(db.Integer)

    result = db.Column(db.String())
    comments = db.Column(db.String(), nullable=True)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
    user = db.relationship('User',
           backref=db.backref('Compute', lazy='dynamic'))

Finally, we need views. For the results of the computation we have a view.html file that is very similar to view_table.html in the vib1 app:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Flask Vib app</title>
  </head>
  <body>

<script type="text/x-mathjax-config">
MathJax.Hub.Config({
  TeX: {
     equationNumbers: {  autoNumber: "AMS"  },
     extensions: ["AMSmath.js", "AMSsymbols.js", "autobold.js", "color.js"]
  }
});
</script>
<script type="text/javascript" src=
"http://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML">
</script>

{% if user.is_anonymous() %}
  <p align="right"><a href="/login">Login</a>
  / <a href="/reg">Register</a></p>
{% else %}
  <p align="right">Logged in as {{user.username}}<br>
    <a href="/old">Previous simulations<a/><br>
    <a href="/logout">Logout</a></p>
{% endif %}

This web page visualizes the function \(
u(t) = Ae^{-bt}\sin (w t), \hbox{ for } t\in [0,T]
\).

<p>
<!-- Input and Results are typeset as a two-column table -->
<table>
<tr>
<td valign="top">
<h2>Input:</h2>

<form method=post action="" enctype=multipart/form-data>
<table>
  {% for field in form %}
    <tr>
      <td>{{ field.label }}</td>
      <td>{{ field(size=20) }}</td>
      <td>
      {% if field.errors %}
        <ul class=errors>
        {% for error in field.errors %}
          <li>{{ error }}</li>
        {% endfor %}</ul>
      {% endif %}
      </td></tr>
  {% endfor %}
</table>
<p>
<input type="submit" value="Compute">
</p>
</form>
</td>

<td valign="top">
  {% if result != None %}
    <h2>Results:</h2>
    <img src="data:image/png;base64,{{ result|safe }}" width="400">
    {% if not user.is_anonymous() %}
      <h3>Comments:</h3>
      <form method="post" action="/add_comment">
          <textarea name="comments" rows="4" cols="40"></textarea>
          <p><input type="submit" value="Add">
      </form>
      {% endif %}

  {% endif %}
</td></tr>
</table>
</body>
</html>

The login.html template for the login page takes the form

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <title>Flask Vib app</title>
</head>
<body>
<h2>Login:</h2>
<form method=post action="">
  <table>
    {% for field in form %}
      <tr><td>{{ field.label }}</td>
          <td>{{ field(size=20) }}</td>
          <td>
            {% if field.errors %}
              <ul class=errors>
              {% for error in field.errors %}
                <li>{{ error }}</li>
              {% endfor %}</ul>
            {% endif %}</td></tr>
    {% endfor %}
  </table>
  <p><input type="submit" value="Login">
</form>
</body>
</html>

The page for registering a new user has a simple template reg.html:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <title>Flask Vib app</title>
</head>
<body>
<h2>Register:</h2>
<form method=post action="">
<table>
   {% for field in form %}
     <tr><td>{{ field.label }}</td>
         <td>{{ field(size=20) }}</td>
          <td>
            {% if field.errors %}
              <ul class=errors>
              {% for error in field.errors %}
                <li>{{ error }}</li>
              {% endfor %}</ul>
            {% endif %}</td></tr>
   {% endfor %}
</table>
<p><input type=submit value=Register>
</form>
</body>
</html>

The final file is old.html for retrieving old simulations:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <title>Flask Vib app</title>
</head>
<body>

<script type="text/x-mathjax-config">
MathJax.Hub.Config({
  TeX: {
     equationNumbers: {  autoNumber: "AMS"  },
     extensions: ["AMSmath.js", "AMSsymbols.js", "autobold.js", "color.js"]
  }
});
</script>
<script type="text/javascript"
 src="http://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML">
</script>

<h2>Previous simulations</h2>
<p align="right"><a href="/">Back to index</a></p>
{% if data %}
  {% for post in data %}
    <hr>
    <table>
    <tr>
    <td valign="top" width="30%">
    <h3>Input</h3>
      <table>
      {% for field in post.form %}
        <tr><td>{{ field.label }}:&nbsp;</td>
            <td>{{ field.data }}</td></tr>
      {% endfor %}
      </table>
    </td><td valign="top" width="60%">
    <h3>Results</h3>
    <img src="data:image/png;base64,{{ post.result|safe }}" width="400">
    {% if True %}
      <p>
      {{ comments }}
    {% endif %}
    </td><td valign="top" width="60%">
    <p>
    <form method="POST" action="/delete/{{ post.id }}">
      <input type=submit value="Delete"
       title="Delete this post from database">
    </form>
    </td></tr>
    </table>
   {% endfor %}
   <hr>
   <center>
   <form method="POST" action="/delete/-1">
     <input type=submit value="Delete all">
   </form>
   </center>
{% else %}
  No previous simulations
{% endif %}
</body>
</html>

Warning.

(hpl 1: Check if an autogenerated app from _generate_app.py can send mail and store comments. Need a compute function that returns full HTML text then.)

Resources

Working with a database in Flask is described here: