Commit 326cb853 by Trevor Austin

Add week 9 notes and examples

parent b361df96
<html lang="en" dir="ltr">
<head>
<title>Enter Key to Post</title>
<link rel="stylesheet" type="text/css" href="style.css">
<script src="https://unpkg.com/react@16/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script>
<script src="https://unpkg.com/prop-types@15.6/prop-types.js" crossorigin></script>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel" src="script.js"></script>
</body>
</html>
class Posts extends React.Component {
render() {
const posts = this.props.posts.map((post, index) => {
const lines = post.split(/\r\n|\r|\n/);
return (<div key={index} id={"post_" + index}>
{lines.map((line, lineIndex) => <div key={lineIndex}>{line}</div>)}
</div>);
});
return (
<div className="posts" id="posts">
<h2>Posts</h2>
{posts}
</div>
);
}
}
Posts.propTypes = {
posts: window.PropTypes.array
}
class Journal extends React.Component {
constructor(props) {
super(props);
this.state = {
compose: "",
composeHeight: 50,
composeScroll: 0,
posts: []
};
}
composeChangeHandler = (event) => {
console.log(event);
const height = event.target.scrollHeight;
this.setState({
compose: event.target.value,
composeHeight: height,
composeScroll: height
});
}
postListener = (event) => {
// TODO: Listen for the Enter Key
console.log("KeyCode: " +event.key);
}
postHandler = (event) => {
this.setState((state) => ({
posts: [...state.posts, state.compose],
compose: "",
composeHeight: 50,
composeScroll: 0
}));
}
render() {
const composeStyle = {
height: this.state.composeHeight + 'px',
scrollTop: this.state.composeScroll
};
const postsStyle = {
paddingBottom: (this.state.composeHeight + 50) + 'px',
}
console.log("render Journal");
return (
<div className="weblog">
<div className="title_bar">
<h1>Using the Enter Key to Post</h1>
</div>
<div style={postsStyle}>
<Posts posts={this.state.posts} />
</div>
<div className="compose" id="compose">
<div className="post_form">
<textarea
value={this.state.compose}
onChange={this.composeChangeHandler}
onKeyPress={this.postListener}
style={composeStyle}
></textarea>
<button onClick={this.postHandler}>Post</button>
</div>
</div>
</div>
);
}
}
// ========================================
ReactDOM.render(
React.createElement(Journal),
document.getElementById('root')
);
body {
margin: 0
}
.weblog {
width: 960px;
margin: auto;
font-family: sans-serif;
}
.title_bar {
background-color: #CCFFEE
}
h1 {
margin: 0;
padding-top: 10px;
padding-bottom: 5px;
}
.compose {
position: fixed;
bottom: 0;
}
.post_form {
background-color: white;
width: 400px;
border: 2px solid #AADDCC;
border-radius: 4px;
display: grid;
grid-template-columns: auto auto auto;
grid-gap: 10px 10px;
padding: 20px 40px 20px 40px;
}
.post_form textarea {
min-height: 50px;
height: auto;
grid-column: 1 / 3;
border: 2px solid #AADDCC;
border-radius: 4px;
}
.post_form button {
grid-column: 3;
width: 100%;
height: 50px;
margin-top: 0;
font-size: 18px;
border: 2px solid #AADDCC;
border-radius: 4px;
}
from flask import Flask, render_template, request, jsonify
from functools import wraps
import mysql.connector
import bcrypt
import configparser
import io
app = Flask(__name__)
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 0
config = configparser.ConfigParser()
config.read('secrets.cfg')
# PEPPER = config['secrets']['PEPPER']
DB_NAME = 'passwords'
DB_USERNAME = config['secrets']['DB_USERNAME']
DB_PASSWORD = config['secrets']['DB_PASSWORD']
@app.route('/')
def index():
# print(PEPPER)
return app.send_static_file('index.html')
# -------------------------------- API ROUTES ----------------------------------
@app.route('/api/signup', methods=['POST'])
def signup ():
print(request.data)
body = request.get_json()
print(body)
username = body['username']
password = body['password']
salt = bcrypt.gensalt()
hashed = bcrypt.hashpw(password.encode('utf-8'), salt)
connection = mysql.connector.connect(user=DB_USERNAME, database=DB_NAME, password=DB_PASSWORD)
cursor = connection.cursor()
query = "INSERT into users (username, password) VALUES (%s, %s)"
try:
cursor.execute(query, (username, hashed))
connection.commit()
return {}
except Exception as e:
print(e)
return {"username": username}, 302
finally:
cursor.close()
connection.close()
@app.route('/api/login', methods=['POST'])
def login ():
print(request.data)
body = request.get_json()
print(body)
username = body['username']
password = body['password']
connection = mysql.connector.connect(user=DB_USERNAME, database=DB_NAME, password=DB_PASSWORD)
cursor = connection.cursor()
query = "SELECT password FROM users WHERE username=%s"
try:
cursor.execute(query, (username,))
hashed = cursor.fetchone()[0]
print(hashed)
success = bcrypt.checkpw(password.encode('utf-8'), hashed.encode('utf-8'))
print(success)
if success:
return {}
return {}, 404
except Exception as e:
print(e)
return {}, 404
finally:
cursor.close()
connection.close()
-- sudo mysql -u root < 20200304T184500-create_database.sql
create database passwords CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- sudo mysql -u root weblog < 20200304T184700-create_tables.sql
create table users (
username VARCHAR(40) PRIMARY KEY,
password BINARY(60) NOT NULL
);
[secrets]
PEPPER = some really long string that I keep secret
DB_USERNAME = trevor
DB_PASSWORD = password
<html lang="en" dir="ltr">
<head>
<title>Single-Page Login and Post</title>
<link rel="stylesheet" type="text/css" href="static/style.css">
<script src="https://unpkg.com/react@16/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script>
<script src="https://unpkg.com/prop-types@15.6/prop-types.js" crossorigin></script>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel" src="static/script.js"></script>
</body>
</html>
class SignupAndLogin extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
signup = () => {
const username = document.getElementById("username").value;
const password = document.getElementById("password").value;
fetch("http://127.0.0.1:5000/api/signup", {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({username: username, password: password})
}).then((response) => {
if(response.status == 200) {
alert("Created user "+username);
} else {
alert("A user "+username+" already exists");
}
});
}
login = () => {
const username = document.getElementById("username").value;
const password = document.getElementById("password").value;
fetch("http://127.0.0.1:5000/api/login", {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({username: username, password: password})
}).then((response) => {
if(response.status == 200) {
alert("Logged in as "+username);
} else {
alert("Incorrect username and password");
}
});
}
render() {
return (
<div className="signup">
<h1>Signup and Login</h1>
<div className="signup_form">
<label htmlFor="username">Username</label>
<input id="username"></input>
<label htmlFor="password">Password</label>
<input id="password" type="password"></input>
<button className="form_button" onClick={this.signup}>
Signup
</button>
<button className="form_button" onClick={this.login}>
Login
</button>
</div>
</div>
);
}
}
// ========================================
ReactDOM.render(
React.createElement(SignupAndLogin),
document.getElementById('root')
);
body {
margin: 0
}
.signup {
width: 960px;
margin: auto;
font-family: sans-serif;
}
h1 {
margin: 0;
padding-top: 10px;
padding-bottom: 5px;
}
.signup_form {
width: 400px;
border: 2px solid #AADDCC;
border-radius: 4px;
display: grid;
grid-template-columns: auto auto auto;
grid-gap: 10px 10px;
padding: 20px 40px 20px 40px;
}
.signup_form label {
grid-column: 1 / 2;
text-align: right;
margin-top: auto;
margin-bottom: auto;
}
.signup_form input {
grid-column: 2 / 4;
font-size: 24px;
border: 2px solid #AADDCC;
border-radius: 4px;
}
.signup_form button {
grid-column: 2;
}
button.form_button {
width: 100%;
font-size: 18px;
border: 2px solid #AADDCC;
border-radius: 4px;
}
image: "python:3.8"
before_script:
- pip install -r requirements.txt
pytest:
script:
- pytest -v
from flask import Flask, render_template, request, jsonify
from functools import wraps
import mysql.connector
import bcrypt
import configparser
import io
app = Flask(__name__)
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 0
config = configparser.ConfigParser()
config.read('secrets.cfg')
PEPPER = config['secrets']['PEPPER']
DB_NAME = 'passwords'
DB_USERNAME = config['secrets']['DB_USERNAME']
DB_PASSWORD = config['secrets']['DB_PASSWORD']
@app.route('/')
def index():
# print(PEPPER)
return app.send_static_file('index.html')
# -------------------------------- API ROUTES ----------------------------------
@app.route('/api/signup', methods=['POST'])
def signup ():
print(request.data)
body = request.get_json()
print(body)
username = body['username']
password = body['password'] + PEPPER
salt = bcrypt.gensalt()
hashed = bcrypt.hashpw(password.encode('utf-8'), salt)
connection = mysql.connector.connect(user=DB_USERNAME, database=DB_NAME, password=DB_PASSWORD)
cursor = connection.cursor()
query = "INSERT into users (username, password) VALUES (%s, %s)"
try:
cursor.execute(query, (username, hashed))
connection.commit()
return {}
except Exception as e:
print(e)
return {"username": username}, 302
finally:
cursor.close()
connection.close()
@app.route('/api/login', methods=['POST'])
def login ():
print(request.data)
body = request.get_json()
print(body)
username = body['username']
password = body['password'] + PEPPER
connection = mysql.connector.connect(user=DB_USERNAME, database=DB_NAME, password=DB_PASSWORD)
cursor = connection.cursor()
query = "SELECT password FROM users WHERE username=%s"
try:
cursor.execute(query, (username,))
hashed = cursor.fetchone()[0]
print(hashed)
success = bcrypt.checkpw(password.encode('utf-8'), hashed.encode('utf-8'))
print(success)
if success:
return {}
return {}, 404
except Exception as e:
print(e)
return {}, 404
finally:
cursor.close()
connection.close()
-- sudo mysql -u root < 20200304T184500-create_database.sql
create database passwords CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- sudo mysql -u root weblog < 20200304T184700-create_tables.sql
create table users (
username VARCHAR(40) PRIMARY KEY,
password BINARY(60) NOT NULL
);
<html lang="en" dir="ltr">
<head>
<title>Single-Page Login and Post</title>
<link rel="stylesheet" type="text/css" href="static/style.css">
<script src="https://unpkg.com/react@16/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script>
<script src="https://unpkg.com/prop-types@15.6/prop-types.js" crossorigin></script>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel" src="static/script.js"></script>
</body>
</html>
class SignupAndLogin extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
signup = () => {
const username = document.getElementById("username").value;
const password = document.getElementById("password").value;
fetch("http://127.0.0.1:5000/api/signup", {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({username: username, password: password})
}).then((response) => {
if(response.status == 200) {
alert("Created user "+username);
} else {
alert("A user "+username+" already exists");
}
});
}
login = () => {
const username = document.getElementById("username").value;
const password = document.getElementById("password").value;
fetch("http://127.0.0.1:5000/api/login", {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({username: username, password: password})
}).then((response) => {
if(response.status == 200) {
alert("Logged in as "+username);
} else {
alert("Incorrect username and password");
}
});
}
render() {
return (
<div className="signup">
<h1>Signup and Login</h1>
<div className="signup_form">
<label htmlFor="username">Username</label>
<input id="username"></input>
<label htmlFor="password">Password</label>
<input id="password" type="password"></input>
<button className="form_button" onClick={this.signup}>
Signup
</button>
<button className="form_button" onClick={this.login}>
Login
</button>
</div>
</div>
);
}
}
// ========================================
ReactDOM.render(
React.createElement(SignupAndLogin),
document.getElementById('root')
);
body {
margin: 0
}
.signup {
width: 960px;
margin: auto;
font-family: sans-serif;
}
h1 {
margin: 0;
padding-top: 10px;
padding-bottom: 5px;
}
.signup_form {
width: 400px;
border: 2px solid #AADDCC;
border-radius: 4px;
display: grid;
grid-template-columns: auto auto auto;
grid-gap: 10px 10px;
padding: 20px 40px 20px 40px;
}
.signup_form label {
grid-column: 1 / 2;
text-align: right;
margin-top: auto;
margin-bottom: auto;
}
.signup_form input {
grid-column: 2 / 4;
font-size: 24px;
border: 2px solid #AADDCC;
border-radius: 4px;
}
.signup_form button {
grid-column: 2;
}
button.form_button {
width: 100%;
font-size: 18px;
border: 2px solid #AADDCC;
border-radius: 4px;
}
import pytest
import bcrypt_example
@pytest.fixture
def client():
with bcrypt_example.app.test_client() as client:
yield client
def test_static_html(client):
"""Serves the static html page and the root path"""
rv = client.get('/')
assert b'<title>Single-Page Login and Post</title>' in rv.data
def test_signup_and_login(client):
"""Tests that I can log in with Trevor's credentials"""
new_username = "something cool"
new_password = "something weird"
client.post('/api/signup', json = {'username':new_username, 'password':new_password})
response = client.post('/api/login', json = {'username':new_username, 'password':new_password})
assert response.status_code == 200
class: center, middle
# MPCS 52553-2: Web Development
## Week 9: Production
---
class: agenda
# Warm-Up: Handling the Enter Key
- `onKeyPress` and `preventDefault`
# Automated Testing
- Time-consistency vs correctness
- `pytest`
- Lab: Add Tests to Passwords App
# Continuous Integration
- GitLab.com CI
- CircleCI and TravisCI
- Lab: GitLab CI for the Passwords App
# Deployment to Cloud Hosting
- AWS Free Usage Tier
- Heroku Free Resources
# Stretch: Feature Flags
---
# Event Handling: Enter Key
### Lab: Enter Key
https://mit.cs.uchicago.edu/trevoraustin/mpcs-52553-austin/blob/master/week_9/examples/enter_key/script.js
---
class: bigquote
# Automated Testing
![Woman from The Onion](americanvoices6.jpg)
> &quot;If my code is wrong, won't my tests just be wrong in the same way?&quot;
---
# Automated Testing
You do get some amount of correctness validation by writing out your business
logic a second time. It can work like a form of [Rubber Duck Debugging](https://en.wikipedia.org/wiki/Rubber_duck_debugging).
--
More importantly, we use tests for **time consistency**. They future developers,
including future us, make changes and additions to the code with **confidence
that they're not accidentally breaking something**.
---
# Automated Testing
As a bonus, automated tests run much faster than conducting tests by hand.
---
# Automated Testing: `pytest`
There is a testing library called `unittest` built into the Python standard
library, but the most commonly used framework is an external library called
`pytest`
https://docs.pytest.org/en/latest/
---
# Lab: Add Tests to Password App
https://mit.cs.uchicago.edu/trevoraustin/mpcs-52553-austin/tree/master/week_10/examples/passwords
---
# Continuous Integration
Once we have the computer running our tests for us, the next step is to have it
run the tests every time we make a change.
https://en.wikipedia.org/wiki/Continuous_integration
---
# Continuous Integration
Current best practice is to have a cluster of computers automatically run all
the tests every time anyone on the team pushes a new commit to version control.
Several providers offer this as a service:
- https://github.com/features/actions
- https://circleci.com/
- https://travis-ci.com/
- https://docs.gitlab.com/ee/ci/
All of the above offer automated continuous integration for free some some
projects.
---
# Lab: GitLab CI for the Passwords App
First, create a new (free) account at https://gitlab.com
--
Then, check out your own copy of
https://gitlab.com/trevoraustin/uofc-web-development
---
# Deployment to Cloud Hosting
Cloud hosting is an enormous business:
> Of the $3.88 billion in operating income that Amazon captured in the fourth
quarter, $2.60 billion of it, or 67%, was attributable to AWS.
---
# Deployment to Cloud Hosting
https://aws.amazon.com/free/
https://www.heroku.com/free
---
# Stretch: Feature Flags
https://martinfowler.com/articles/feature-toggles.html
https://en.wikipedia.org/wiki/Feature_toggle
https://launchdarkly.com/use-cases/
<!DOCTYPE html>
<html>
<head>
<title>Web Development - Week 3</title>
<meta charset="utf-8">
<style>
@import url(https://fonts.googleapis.com/css?family=Yanone+Kaffeesatz);
@import url(https://fonts.googleapis.com/css?family=Droid+Serif:400,700,400italic);
@import url(https://fonts.googleapis.com/css?family=Ubuntu+Mono:400,700,400italic);
body { font-family: 'Droid Serif'; }
h1, h2, h3 {
font-family: 'Yanone Kaffeesatz';
font-weight: normal;
}
img { width: 100%; }
.remark-code, .remark-inline-code { font-family: 'Ubuntu Mono'; }
.agenda h1 { margin-bottom: 0px; }
.agenda ul { margin-block-start: 0; color: #666 }
.bigquote img { float: left; width: 300px; padding-right: 20px }
.bigquote blockquote { font-size: 40px; }
</style>
</head>
<body>
<textarea id="source">
</textarea>
<script src="https://remarkjs.com/downloads/remark-latest.min.js">
</script>
<script>
var slideshow = remark.create({sourceUrl: 'notes_week_9.md'});
</script>
</body>
</html>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment