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.
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.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 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.
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 }}: </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>
(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.)
Working with a database in Flask is described here: