Scryfall API docs - https://scryfall.com/docs/api
Final App Deployed at https://kaitlinsmagicdash.herokuapp.com/
- Create a new virtual environment in my directory
python3 -m venv env
- Activate the virtual environment
source env/bin/activate
- Install Flask
pip3 install Flask
- Create
flask/
folder andapp.py
in there - Add boilerplate code
"""test Flask with this"""
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello():
return 'Hello World!'
cd flask
flask run
to run this locally on localhost:5000
git init
git status
Create a .gitignore
and include __pycache__
git add .
git commit -m "Initial commit"
Create a new repo on GitHub
git remote add origin https://github.com/k-berryman/Magic-Dashboard.git
git remote -v
git push origin master
(with help from these instructions)
pip3 install Flask-WTF
pip3 install flask-wtf
just to double check- Create
forms.py
- Add the following imports in
forms.py
from flask_wtf import FlaskForm
from wtforms import FloatField, StringField
- Configuring our form with the following
class AddForm(FlaskForm):
"""Form"""
name = StringField("Snack Name")
price = FloatField("Price in USD")
- In
app.py
,from forms import AddForm
- Start by rendering the form to the user which can be submitted via POST req
- In
app.py
@app.route('/')
def home():
form = AddForm()
return render_template("index.html", form=form)
- Create
templates/index.html
- In
index.html
,
<body>
<h1>My Form</h1>
<form action="" method="POST">
{% for field in form
if field.widget.input_type != 'hidden' %}
<p>
{{ field.label }}
{{ field }}
</p>
{% endfor %}
<button>Submit</button>
</form>
</body>
- Update the route
methods=["GET", "POST"]
inapp.py
Now let's handle CSRF security
This part if field.widget.input_type != 'hidden'
filters out CSRF Token in display
In index.html
At the top of the form add {{ form.hidden_tag() }} <!-- add type=hidden form fields -->
Make sure it's part of the form because we want it to be included in our POST req
Now we need to validate that token on the serverside. In app.py
,
@app.route('/form', methods=["GET", "POST"])
def form():
form = AddForm()
# if it's a post request with a valid CSRF Token
if form.validate_on_submit():
return redirect('/answer')
else:
return render_template("index.html", form=form)
Make sure to test with data for it to work properly
validate_on_submit
takes an empty form and fills it with data from the request
# if it's a post request with a valid CSRF Token
if form.validate_on_submit():
name = form.name.data
price = form.price.data
print(name, price)
return redirect('/answer')
Now we have the data!
Time to validate the data -- Throw friendly errors if it doesn't match ideal data format In forms.py
, from wtforms.validators import InputRequired, Optional, Email
class AddForm(FlaskForm):
"""Form"""
name = StringField(
"Snack Name",
validators=[InputRequired()])
price = FloatField(
"Price in USD",
validators=[InputRequired()])
quantity = FloatField(
"Amount of Snack",
validators=[InputRequired()])
validate _on_submit
in app.py
handles validating this
We want some error messages to render
In index.html
, in form,
<p>
{{ field.label }}
{{ field }}
{{% for err in field.errors %}}
{{err}}
{{% endfor %}}
</p>
A secret key is required to use CSRF. In app.py
,
# Flask-WTF requires an encryption key - the string can be anything
app.config['SECRET_KEY'] = 'TESTINGGG'
This is what index.html
should contain
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Form</title>
</head>
<body>
<h1>My Form</h1>
<form method="POST">
{{ form.hidden_tag() }} <!-- add type=hidden form fields -->
{{ form.csrf_token }}
{% for field in form
if field.widget.input_type != 'hidden' %}
<p>
{{ field.label }}
{{ field }}
{% for err in field.errors %}
{{ err }}
{% endfor %}
</p>
{% endfor %}
<button>Submit</button>
</form>
</body>
</html>
Yay! WTForms is set up. Right now the values are from my last project, so we'll just update based on what input we need.
pip3 install requests
Add jsonify
to imports
@app.route('/req')
def req():
resp = requests.get("https://api.scryfall.com/cards/random")
data = resp.json()
# using the APIs JSON data, return that to browser
return jsonify(data)
- Make
templates
folder andbase.html
in there and add this boilerplate
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootswatch/4.1.3/lumen/bootstrap.css">
</head>
<body>
<div class="container">
{% block content %}
{% endblock %}
</div>
</body>
</html>
Create home.html
with this boilerplate
{% extends 'base.html' %}
{% block content %}
<h1>Home Page</h1>
<h2>Hello, user!</h2>
{% endblock %}
In app.py
, import render_template
and return render_template("home.html")
In forms.py
,
class RegisterForm(FlaskForm):
"""Register Form"""
name = StringField("Name",
validators=[
InputRequired("Name can't be blank"),
Length(min=1, max=50, message="Name must be 50 characters or less")])
email = StringField("Email",
validators=[
InputRequired("Email can't be blank"),
Email("Please enter a valid email"),
Length(min=1, max=50, message="Email must be 50 characters or less")])
username = StringField("Username",
validators=[
InputRequired("Username can't be blank"),
Length(min=1, max=25, message="Username must be 25 characters or less")])
password = PasswordField("Password",
validators=[
InputRequired("Password can't be blank")])
Create view function in app.py
@app.route('/register', methods=["GET", "POST"])
def register():
form = RegisterForm()
# if it's a request with a valid CSRF Token
if form.validate_on_submit():
# retrieve data from form
name = form.name.data
email = form.email.data
username = form.username.data
password = form.password.data
# add to SQLAlchemy
#user = User(username=username, password=password, email=email, first_name=first_name, last_name=last_name)
#db.session.add(user)
#db.session.commit()
# redirect
return redirect('/success')
return render_template("register.html", form=form)
In forms.py
,
class LoginForm(FlaskForm):
"""Login Form"""
username = StringField("Username",
validators=[
InputRequired("Username can't be blank"),
Length(min=1, max=25, message="Username must be 25 characters or less")])
password = PasswordField("Password",
validators=[
InputRequired("Password can't be blank")])
Create view function in app.py
@app.route('/login', methods=["GET", "POST"])
def login():
form = LoginForm()
# if it's a request with a valid CSRF Token
if form.validate_on_submit():
# retrieve data from form
username = form.username.data
password = form.password.data
# verification...?
# redirect
return redirect('/secret')
else:
return render_template("login.html", form=form)
- Create
models.py
and add some boilerplate code
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
def connect_db(app):
"""Connect to database."""
db.app = app db.init_app(app)
class User(db.Model):
"""User."""
__tablename__ = "users"
id = db.Column(db.Integer,
primary_key=True,
autoincrement=True)
name = db.Column(db.String(150),
nullable=False)
description = db.Column(db.String(300),
nullable=False)
- Update the data to match the given schema
pip3 install flask_sqlalchemy
pip3 install flask_bcrypt
In models.py
, from flask_bcrypt import Bcrypt
In app.py
,
from models import connect_db, db, User
...
app.config['SQLALCHEMY_DATABASE_URI'] = 'postgresql:///magicDB'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SQLALCHEMY_ECHO'] = True
connect_db(app)
In terminal, createdb magicDB
ipython3
%run app.py
db.create_all()
psql
\c magicDB
SELECT * FROM users;
SELECT * FROM cards;
Add a register
class method to User model in models.py
@classmethod
def register(cls, name, email, username, password):
"""Register user w/ hashed password & return user."""
hashed = bcrypt.generate_password_hash(password)
# turn bytestring into normal unicode utf8 string
hashed_utf8 = hashed.decode("utf8")
# return instance of user w/ username and hashed password
return cls(name=name, email=email, username=username, password=hashed_utf8)
ipython3
, %run app.py
, user1 = User.register(sample data)
, user1.username
, user1.password
It works!
Authenticate class method on User model
@classmethod
def authenticate(cls, username, password):
"""Validate that user exists and password is correct. Return user if valid; else, return False """
u = User.query.filter_by(username=username).first()
if u and bcrypt.check_password_hash(u.password, password):
return u
else:
return False
pip3 install psycopg2
View function for login
@app.route('/login', methods=["GET", "POST"])
def login():
form = LoginForm()
# if it's a request with a valid CSRF Token
if form.validate_on_submit():
# retrieve data from form
username = form.username.data
password = form.password.data
# verification
user = User.authenticate(username, password)
if user:
return redirect('/secret')
else:
form.username.errors = ['Invalid username/password']
else:
return render_template("login.html", form=form)
View function for register
@app.route('/register', methods=["GET", "POST"])
def register():
form = RegisterForm()
# if it's a request with a valid CSRF Token
if form.validate_on_submit():
# retrieve data from form
name = form.name.data
email = form.email.data
username = form.username.data
password = form.password.data
# add to SQLAlchemy
newUser = User.register(name, email, username, password)
db.session.add(newUser)
db.session.commit()
# redirect
flash('Welcome! Successfully logged in')
return redirect('/secret')
return render_template("register.html", form=form)
Now we have password hashing, but anyone can navigate to /secret
. Let’s protect this route and make sure that only users who have logged in can access this route.
After login or registering, store their username in the session.
import session
In register
view func before redirecting,
# add new user's username to session
session['sessionUsername'] = newUser.username
In login
view func before redirecting,
# add user_id to session
session['sessionUsername'] = user.username
Make the /secret
route
@app.route('/secret')
def secret():
if "sessionUsername" not in session:
flash('Please login first!')
return redirect('/')
return "You made it!"
Make a new route. Clear any information from the session and redirect to /
@app.route('/logout')
def logout():
session.pop('sessionUsername')
flash('Goodbye! Logging out now..')
return redirect('/')
Could make this a post request by styling an empty form on top of UI Logout. Post req is best practice, but this is the Springboard taught us.
Let’s add some authorization! When a user logs in, take them to GET /users/<username>
Display a template the shows information about that user (everything except for their password). You should ensure that only logged in users can access this page.
Change secrets route name
Make dashboard.html
Pass user into dashboard.html Change references from '/secret'
Let's add some styling
Many-to-Many
class Deck(db.Model):
"""Deck."""
__tablename__ = "decks"
id = db.Column(db.Integer,
primary_key=True,
autoincrement=True)
name = db.Column(db.String(150),
nullable=False)
user_id = db.Column(db.Integer,
db.ForeginKey('users.id'))
card_id = db.Column(db.Integer,
db.ForeginKey('cards.id'))
ipython3
, %run app.py
, db.drop_all()
, db.create_all()
Still in ipython3
,
user = User(name="Kaitlin Berryman", email="[email protected]", username="GolgariGal", password="Simic123")
db.session.add(user)
db.session.commit()
card = Card(name="Terastodon", picture="https://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=197137&type=card", cmc="8", price=3.13, colors="g")
db.session.add(card)
db.session.commit()
deck_card = Deck(name="Aesi", user_id=1, card_id=1)
db.session.add(deck_card)
db.session.commit()
Tailwind CSS
Make an account on Heroku
Install CLI brew install heroku/brew/heroku
Configure production ready server on heroku called gunicorn
pip3 install gunicorn
Prepping requirements.txt
pip3 freeze > requirements.txt
and make sure venv/ is in .gitignore
Adding a Procfile (tells Heroku what command to run to start server)
echo "web: gunicorn app:app" > Procfile
in main dir
Adding runtime.txt (tell Heroku python version)
I'm using Python 3.9.7
echo "python-3.9.7" > runtime.txt
Login to heroku account via CLI
heroku login
Create an app
Make sure everything is committed via git
heroku create kaitlinsmagicdash
git remote -v
heroku is added to remotes
push to heroku
git push heroku master
(this outputs https://git.heroku.com/kaitlinsmagicdash.git)
heroku open
opens it up in the web at https://kaitlinsmagicdash.herokuapp.com/login
However, internal server errors because Postgres isn't set up yet and so our models don't work. Don't panic!!
View errors with heroku logs --tail
Since we're on a different server, we need different environment variable values
dev vs. prod
heroku config:set FLASK_ENV=production
heroku config
to see env vars
heroku config:set SECRET_KEY=actualsecret
In app.py
,
import os
# use secret key in production or default to our dev one
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'shh')
git add .
git commit -m "Add OS for environ var"
git push heroku master
--
Could configure custom domain via heroku dashboard, but we're not going to
heroku addons:create heroku-postgresql:hobby-dev
Install an add-on to our heroku app which is for our db
hobby-dev is the tier/plan on heroku (the free one)
heroku config
We see DATABASE_URL
In app.py
,
import os
# production or dev DB
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DATABASE_URL', 'postgresql:///magicDB')
Yes, it still works locally
git add .
git commit -m "Add DB to env var for prod"
git push heroku master
--
Create tables on Heroku
Connect to postgres directly heroku pg:psql
This connects us to the heroku shell with our db
You could do postgres commands here, but we're not gonna
We're going to make a seed.py
file
from models import db
from app import app
db.drop_all()
db.create_all()
(commit this code)
We need to run this seed file on heroku
To run this on our prod server, heroku run python seed.py
https://stackoverflow.com/questions/62688256/sqlalchemy-exc-nosuchmoduleerror-cant-load-plugin-sqlalchemy-dialectspostgre https://help.heroku.com/ZKNTJQSK/why-is-sqlalchemy-1-4-x-not-connecting-to-heroku-postgres I followed heroku's instructions, but that didn't work I just tried and tried until it worked
# production or dev DB
#app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get(prodURI, 'postgresql:///magicDB')
try:
prodURI = os.getenv('DATABASE_URL')
prodURI = prodURI.replace("postgres://", "postgresql://")
app.config['SQLALCHEMY_DATABASE_URI'] = prodURI
print(prodURI)
except:
app.config['SQLALCHEMY_DATABASE_URI'] = 'postgresql:///magicDB'
Try this again
heroku run python seed.py
Bug fixed :-)
Tables made
heroku open
Register a user -- app works! Everything works!
Any changes? Just push it to heroku