Build a Simple CRUD App with Python, Flask, and React: In this tutorial you are going to build a JavaScript application using React in the front-end and we are also going to build a ReST API written in Python which is going to persist. Our app will be a Github open source bookmark project (a.k.a kudo).
In this tutorial you are going to build a JavaScript application using React in the front-end and we are also going to build a ReST API written in Python which is going to persist.
Today’s modern web applications are often built with a server-side language serving data via an API and a front-end javascript framework that presents the data in an easy to use manner to the end user. Python is a dynamic language widely adopted by companies and developers. The language states on its core values that software should simple, readable making developers more productive and happier. You’ll also use Flask to help you to quickly put together a ReST API. React is a declarative, efficient, and flexible JavaScript library developed at Facebook for building user interfaces. It facilitates the creation of complex, interactive, and stateful UIs from small and isolated pieces of code called components.
To complete this tutorial, there are few things you will need:
You will start by creating the back-end.
Create a ReST API with PythonMake sure you have Python 3 installed. Check the version of Python installed by running the following command:
python --version
To install Python 3 you can use pyenv.
If you are using macOS, you can install it using Homebrew:
brew update
brew install pyenv
On a Linux system using the bash shell:
curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash
Once installed, you can run the following commands to install Python 3.
pyenv install 3.6.3
pyenv global 3.6.3
Your ReST API will use some third-party code (libraries) to help you (e.g. to connect to a database, to create schemas for your models, and validate whether the incoming requests are authenticated or not). Python has a powerful tool to manage dependencies called pipenv. To install pipenv on your machine follow these steps:
On macOS:
brew install pipenv
pip install --user pipenv
With pipenv installed, create a directory for your backend code:
mkdir kudos_oss && cd kudos_oss
The command above will create a Python 3 virtual environment. Now you can install Flask by running the following command:
pipenv install flask==1.0.2
Python 3 provides some cool features like absolute_import and print_function that you will use in this tutorial. To import them run the following commands:
touch __init__.py
touch __main__.py
And copy and paste the following content into the main.py file:
from __future__ import absolute_import, print_function
Your backend will need to implement the following user stories:
A normal ReST API will expose endpoints so clients can create, update, delete, read and list all resources. By end of this section your back-end application will be capable to handle the following HTTP calls:
# For the authenticated user, fetches all favorited github open source projects
GET /kudos
# Favorite a github open source project for the authenticated user
POST /kudos
# Unfavorite a favorited github open source project
DELETE /kudos/:id
Define the Python Model Schemas
Your ReST API will have two core schemas, they are GithubRepoSchema and KudoSchema. GithubRepoSchema will represent a Github repository sent by the clients whereas KudoSchema will represent the data you are going to persist in the database.
Go ahead and run the following commands:
mkdir -p app/kudo
touch app/kudo/schema.py
touch app/kudo/service.py
touch app/kudo/__init__.py
The above commands will create the app directory with another directory within it called kudo then, the second command will create three files: schema.py, service.py, and init.py.
Copy and paste the content below within the schema.py file.
from marshmallow import Schema, fields
class GithubRepoSchema(Schema):
id = fields.Int(required=True)
repo_name = fields.Str()
full_name = fields.Str()
language = fields.Str()
description = fields.Str()
repo_url = fields.URL()
class KudoSchema(GithubRepoSchema):
user_id = fields.Email(required=True)
As you may have noticed, the schemas are inheriting from Schema a package from the [marshmallow library] (https://marshmallow.readthedocs.io/en/3.0/), marshmallow is an ORM/ODM/framework-agnostic library for serializing/deserializing complex data types, such as objects, to and from native Python data types.
Install the marshmallow library running the following commands:
pipenv install marshmallow==2.16.3
Python ReST API Persistence with MongoDB
Great! You have now your first files in place. The schemas were created to represent the incoming request data as well as the data your application persists in the MongoDB. In order to connect and to execute queries against the database, you are going to use a library created and maintained by MongoDB itself called pymongo.
Install the pymongo library running the following commands:
pipenv install pymongo==3.7.2
You can either use MongoDB installed on your machine or you can use docker to spin up a MongoDB container. This tutorial assumes you have Docker and docker-compose installed.
docker-compose will manage the MongoDB container for you.
Create docker-compose.yml
touch docker-compose.yml
Paste the following content into it:
version: '3'
services:
mongo:
image: mongo
restart: always
ports:
- "27017:27017"
environment:
MONGO_INITDB_ROOT_USERNAME: mongo_user
MONGO_INITDB_ROOT_PASSWORD: mongo_secret
All you have to do now to spin up a MongoDB container is:
docker-compose up
With MongoDB up and running you are ready to work the MongoRepository class, it is always a good idea to have class with just a single responsibility, so the only point in your back-end application MongoDB is going to be explicitly dealt with is in the MongoRepository.
Start by creating a directory where all persistence related files should sit, a suggestion would be: repository.
mkdir -p app/repository
Then, create the file that will hold the MongoRepository class:
touch app/repository/mongo.py
touch app/repository/__init__.py
With pymongo properly installed and MongoDB up and running, paste the following content into the app/repository/mongo.py file.
import os
from pymongo import MongoClient
COLLECTION_NAME = 'kudos'
class MongoRepository(object):
def __init__(self):
mongo_url = os.environ.get('MONGO_URL')
self.db = MongoClient(mongo_url).kudos
def find_all(self, selector):
return self.db.kudos.find(selector)
def find(self, selector):
return self.db.kudos.find_one(selector)
def create(self, kudo):
return self.db.kudos.insert_one(kudo)
def update(self, selector, kudo):
return self.db.kudos.replace_one(selector, kudo).modified_count
def delete(self, selector):
return self.db.kudos.delete_one(selector).deleted_count
As you can see the MongoRepository class is straightforward, it creates a database connection on its initialization then saves it to a instance variable to be use later by the methods: find_all, find, create, update, and delete. Notice that all methods explicitly use the pymongo API.
You might have noticed that the MongoRepository class reads a environment variable MONGO_URL . To export the environment variable, run:
export MONGO_URL=mongodb://mongo_user:[email protected]:27017/
Since you might want to use other database in the future, it is a good idea to decouple your application from MongoDB. For the sake of simplicity you are going to create an abstract class to represent a Repository, this class should be the one used throughout your application.
Paste the following content into the app/repository/init.py file:
class Repository(object):
def __init__(self, adapter=None):
self.client = adapter()
def find_all(self, selector):
return self.client.find_all(selector)
def find(self, selector):
return self.client.find(selector)
def create(self, kudo):
return self.client.create(kudo)
def update(self, selector, kudo):
return self.client.update(selector, kudo)
def delete(self, selector):
return self.client.delete(selector)
You might recall the user story that you’re working on is that ann authenticated user should able to create, delete and list all favorited Github open-source projects. In order to get that done those MongoRepository’s methods will come handy.
You will soon implement the endpoints of your ReST API. First, you need to create a service class that knows how to translate the incoming request payload to our representation KudoSchema defined in the app/kudo/schema.py. The difference between the incoming request payload, represented by GithubSchema, and the object you persist in the database, represented by KudoSchema is: The first has an user_Id which determines who owns the object.
Copy the content below to the app/kudo/service.py file:
from ..repository import Repository
from ..repository.mongo import MongoRepository
from .schema import KudoSchema
class Service(object):
def __init__(self, user_id, repo_client=Repository(adapter=MongoRepository)):
self.repo_client = repo_client
self.user_id = user_id
if not user_id:
raise Exception("user id not provided")
def find_all_kudos(self):
kudos = self.repo_client.find_all({'user_id': self.user_id})
return [self.dump(kudo) for kudo in kudos]
def find_kudo(self, repo_id):
kudo = self.repo_client.find({'user_id': self.user_id, 'repo_id': repo_id})
return self.dump(kudo)
def create_kudo_for(self, githubRepo):
self.repo_client.create(self.prepare_kudo(githubRepo))
return self.dump(githubRepo.data)
def update_kudo_with(self, repo_id, githubRepo):
records_affected = self.repo_client.update({'user_id': self.user_id, 'repo_id': repo_id}, self.prepare_kudo(githubRepo))
return records_affected > 0
def delete_kudo_for(self, repo_id):
records_affected = self.repo_client.delete({'user_id': self.user_id, 'repo_id': repo_id})
return records_affected > 0
def dump(self, data):
return KudoSchema(exclude=['_id']).dump(data).data
def prepare_kudo(self, githubRepo):
data = githubRepo.data
data['user_id'] = self.user_id
return data
Notice that your constructor init receives as parameters the user_id and the repo_client which are used in all operations in this service. That’s the beauty of having a class to represent a repository, As far as the service is concerned, it does not care if the repo_client is persisting the data in a MongoDB, PostgreSQL, or sending the data over the network to a third party service API, all it needs to know is the repo_client is a Repository instance that was configured with an adapter that implements methods like create, delete and find_all.
At this point, you’ve covered 70% of the backend. You are ready to implement the HTTP endpoints and the JWT middleware which will secure your ReST API against unauthenticated requests.
You can start by creating a directory where HTTP related files should be placed.
mkdir -p app/http/api
Within this directory, you will have two files, endpoints.py and middlewares.py. To create them run the following commands:
touch app/http/api/__init__.py
touch app/http/api/endpoints.py
touch app/http/api/middlewares.py
The requests made to your ReST API are JWT-authenticated, which means you need to make sure that every single request carries a valid json web token. pyjwt will take care of the validation for us. To install it run the following command:
pipenv install pyjwt==1.7.1
Now that you understand the role of the JWT middleware, you need to write it. Paste the following content to the middlewares.py file.
from functools import wraps
from flask import request, g, abort
from jwt import decode, exceptions
import json
def login_required(f):
@wraps(f)
def wrap(*args, **kwargs):
authorization = request.headers.get("authorization", None)
if not authorization:
return json.dumps({'error': 'no authorization token provied'}), 403, {'Content-type': 'application/json'}
try:
token = authorization.split(' ')[1]
resp = decode(token, None, verify=False, algorithms=['HS256'])
g.user = resp['sub']
except exceptions.DecodeError as identifier:
return json.dumps({'error': 'invalid authorization token'}), 403, {'Content-type': 'application/json'}
return f(*args, **kwargs)
return wrap
Flask provide a module called g which is a global context shared across the request life cycle. This middleware is checking whether or not the request is valid, if so, the middleware will extract the authenticated user details and persist them in the global context.
The HTTP handlers should be easy now, since you have already done the important pieces, it’s just a matter of putting everything together.
Since your end goal is to create a JavaScript application that will run on web browsers, you need to make sure that web browsers are happy when a preflight is performed, you can learn more about it here. In order to implement CORS our your ReST API, you are going to install flask_cors.
pipenv install flask_cors==3.0.7
Next, implement your endpoints. Go ahead and paste the content above into the app/http/api/endpoints.py file.
from .middlewares import login_required
from flask import Flask, json, g, request
from app.kudo.service import Service as Kudo
from app.kudo.schema import GithubRepoSchema
from flask_cors import CORS
app = Flask(__name__)
CORS(app)
@app.route("/kudos", methods=["GET"])
@login_required
def index():
return json_response(Kudo(g.user).find_all_kudos())
@app.route("/kudos", methods=["POST"])
@login_required
def create():
github_repo = GithubRepoSchema().load(json.loads(request.data))
if github_repo.errors:
return json_response({'error': github_repo.errors}, 422)
kudo = Kudo(g.user).create_kudo_for(github_repo)
return json_response(kudo)
@app.route("/kudo/<int:repo_id>", methods=["GET"])
@login_required
def show(repo_id):
kudo = Kudo(g.user).find_kudo(repo_id)
if kudo:
return json_response(kudo)
else:
return json_response({'error': 'kudo not found'}, 404)
@app.route("/kudo/<int:repo_id>", methods=["PUT"])
@login_required
def update(repo_id):
github_repo = GithubRepoSchema().load(json.loads(request.data))
if github_repo.errors:
return json_response({'error': github_repo.errors}, 422)
kudo_service = Kudo(g.user)
if kudo_service.update_kudo_with(repo_id, github_repo):
return json_response(github_repo.data)
else:
return json_response({'error': 'kudo not found'}, 404)
@app.route("/kudo/<int:repo_id>", methods=["DELETE"])
@login_required
def delete(repo_id):
kudo_service = Kudo(g.user)
if kudo_service.delete_kudo_for(repo_id):
return json_response({})
else:
return json_response({'error': 'kudo not found'}, 404)
def json_response(payload, status=200):
return (json.dumps(payload), status, {'content-type': 'application/json'})
Brilliant! It’s all in place now! You should be able to run your ReST API with the command below:
FLASK_APP=$PWD/app/http/api/endpoints.py FLASK_ENV=development pipenv run python -m flask run --port 4433
Create the React Client-Side App
To create your React Client-Side App, you will use Facebook’s awesome create-react-app tool to bypass all the webpack hassle.
Installing create-react-app is simple. In this tutorial you will use yarn. Make sure you either have it installed or use the dependency manager of your preference.
To install create-react-app, run the command:
yarn global add create-react-app
You will need a directory to place your React application, go ahead and create the web directory within the pkg/http folder.
mkdir -p app/http/web
Now, create a React application:
cd app/http/web
create-react-app app
create-react-app might take a few minutes to generate the boilerplate application. Go to the recently created app directory and run npm start
By default, the React app generated by create-react-app will run listening on port 3000. Let’s change it to listen to the port 8080.
Change the start command on the file app/http/web/app/package.json to use the correct port.
Then, run the React app.
cd app
npm start
Running npm start will start a web server listening to the port 8080. Open http://localhost:8080/ in your browser. Your browser should load React and render the App.js component created automatically by create-react-app.
Your goal now is to use Material Design to create a simple and beautiful UI. Thankfully, the React community has created https://material-ui.com/ which basically are the Material Design concepts translated to React components.
Run the following commands to install what you will need from Material Design.
yarn add @material-ui/core
yarn add @material-ui/icons
Great, now you have components like: Grid, Card, Icon, AppBar and many more ready to be imported and used. You will use them soon. Let’s talk about protected routes.
Writing secure user authentication and building login pages are easy to get wrong and can be the downfall of a new project. Okta makes it simple to implement all the user management functionality quickly and securely. Get started by signing up for a free developer account and creating an OpenID Connect application in Okta.
Once logged in, create a new application by clicking Add Application.
Select the Single-Page App platform option.
The default application settings should be the same as those pictured.
Great! With your OIDC application in place, you can now move forward and secure the routes that requires authentication.
React Router is the most used library for routing URLs to React components. React Router has a collection a components that can be used to help the user to navigate in you application.
Your React application will have two routes:
/ The root route does not require the user to be logged in, it actually is the landing page of your application. A user should be able to access this page in order to log in. You will use the Okta React SDK to integrate react-router with Okta’s OpenID Connect API.
/home The Home route will render most of the React components you application will have. It should implement the following user stories.
An authenticated user should be able to search through the Github API the open source projects of his/her preferences An authenticated user should be able to bookmark open source projects that pleases him/her An authenticated user should be able to see in different tabs his/her previously bookmarked open source projects and the search results
To install react-router run the command:
yarn add react-router-dom
And to install the Okta React SDK run the command:
yarn add @okta/okta-react
Now, go head and create your Main component.
mkdir -p src/Main
Then, within the Main directory create a file named index.js.
touch src/Main/index.js
And paste the following content into the recently created file:
import React, { Component } from 'react';
import { Switch, Route, BrowserRouter as Router } from 'react-router-dom'
import { Security, ImplicitCallback, SecureRoute } from '@okta/okta-react';
import Login from '../Login'
import Home from '../Home'
class Main extends Component {
render() {
return (
<Router>
<Security
issuer={yourOktaDomain}
client_id={yourClientId}
redirect_uri={'http://localhost:8080/implicit/callback'}
scope={['openid', 'profile', 'email']}>
<Switch>
<Route exact path="/" component={Login} />
<Route path="/implicit/callback" component={ImplicitCallback} />
<SecureRoute path="/home" component={Home} />
</Switch>
</Security>
</Router>
);
}
}
export default Main;
Don’t worry for now about the Home and Login components. You will work on them soon. Focus on the Security, SecureRoute, and ImplicitCallback components.
For routes to work properly in React, you need to wrap your whole application in a router. Similarly, to allow access to authentication anywhere in the app, you need to wrap the app in a Security component provided by Okta. Okta also needs access to the router, so the Security component should be nested inside the router.
For routes that require authentication, you will define them using the SecureRoute Okta component. If an unauthenticated user tries to access /home, he/she will be redirect to the / root route.
The ImplicitCallback component is the route/URI destination to which the user will be redirected after Okta finishes the sign in process.
Go ahead and change the src/index.js to mount your Main component.
import React from 'react';
import ReactDOM from 'react-dom';
import { Router } from 'react-router-dom'
import { createBrowserHistory } from 'history'
import Main from './Main';
const history = createBrowserHistory();
ReactDOM.render((
<Router history={history}>
<Main history={history} />
</Router>
), document.getElementById('root'))
Your are now ready to create the Login component. As mentioned previously, this component will be accessible to all users (not only authenticated users). The main goal of the Login component is to authenticate the user.
Inside the directory app, you will find a directory called src which stands for source. Go ahead and create a directory named Login.
mkdir -p src/Login
Then, within the Login directory create a file named index.js.
touch src/Login/index.js
And paste the following content into the file:
import React from 'react'
import Button from '@material-ui/core/Button';
import { Redirect } from 'react-router-dom'
import { withAuth } from '@okta/okta-react';
class Login extends React.Component {
constructor(props) {
super(props);
this.state = { authenticated: null };
this.checkAuthentication = this.checkAuthentication.bind(this);
this.login = this.login.bind(this);
}
async checkAuthentication() {
const authenticated = await this.props.auth.isAuthenticated();
if (authenticated !== this.state.authenticated) {
this.setState({ authenticated });
}
}
async componentDidMount() {
this.checkAuthentication()
}
async login(e) {
this.props.auth.login('/home');
}
render() {
if (this.state.authenticated) {
return <Redirect to='/home' />
} else {
return (
<div style={{height: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center'}}>
<Button variant="contained" color="primary" onClick={this.login}>Login with Okta</Button>
</div>
)
}
}
}
export default withAuth(Login);
In order to see the Login page working, you need to create a placeholder for the Home component.
Go ahead and create a directory called Home.
mkdir -p src/Home
Then, within that directory, create a file named index.js.
touch src/Home/index.js
And paste the following content into it:
import React from 'react'
const home = (props) => {
return (
<div>Home</div>
)
};
export default home;
Now try running npm start and open http://localhost:8080 in your browser. You should see the page below.
In the Login component you are using the Okta React SDK to check whether the user has signed in… If the user has already signed in, they should be redirected to the /home route, otherwise he/she could click Login With Okta to be redirected to Okta, authenticate and be sent to the the home page.
For now, the home page is blank, but eventually here’s what you’ll want the home page to look like:
The Home component is composed of Material Design components like: Tab, AppBar, Button, and Icon as well as a few custom components you will have to create.
For your app, you need to list all the bookmarked open source projects as well as the search results. As you can see in the image above, the Home component is using a tabs to separate bookmarked open source projects from search results, the first tab is listing all the open source projects bookmarked by the user whereas the second tab will list the search results.
You can create a component to represent an open source project in both “Kudos” and “Search Results” lists, that’s the beauty of React components they are highly flexible and reusable.
Go ahead and create a directory called GithubRepo
mkdir -p src/GithubRepo
Then, within that directory, create a file named index.js
touch src/GithubRepo/index.js
And paste the following content into it:
import React from 'react';
import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles';
import Card from '@material-ui/core/Card';
import CardHeader from '@material-ui/core/CardHeader';
import CardContent from '@material-ui/core/CardContent';
import CardActions from '@material-ui/core/CardActions';
import IconButton from '@material-ui/core/IconButton';
import Typography from '@material-ui/core/Typography';
import FavoriteIcon from '@material-ui/icons/Favorite';
const styles = theme => ({
card: {
maxWidth: 400,
},
media: {
height: 0,
paddingTop: '56.25%', // 16:9
},
actions: {
display: 'flex',
}
});
class GithubRepo extends React.Component {
handleClick = (event) => {
this.props.onKudo(this.props.repo)
}
render() {
const { classes } = this.props;
return (
<Card className={classes.card}>
<CardHeader
title={this.props.repo.full_name}
/>
<CardContent>
<Typography component="p" style={{minHeight: '90px', overflow: 'scroll'}}>
{this.props.repo.description}
</Typography>
</CardContent>
<CardActions className={classes.actions} disableActionSpacing>
<IconButton aria-label="Add to favorites" onClick={this.handleClick}>
<FavoriteIcon color={this.props.isKudo ? "secondary" : "primary"} />
</IconButton>
</CardActions>
</Card>
);
}
}
export default withStyles(styles)(GithubRepo);
The GithubRepo is a quite simple component, it receives two props: A repo object which holds a reference to a Github repository and an isKudo boolean flag that indicates whether the repo has been bookmarked or not.
The next component you will need is the SearchBar. It will have two responsibilities: log the user out and call React on every press of the Enter key in the search text field.
Create a directory called SearchBar
mkdir -p src/SearchBar
Then, within the directory, create a file named index.js
touch src/SearchBar/index.js
Paste the following content:
import React from 'react';
import PropTypes from 'prop-types';
import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import InputBase from '@material-ui/core/InputBase';
import Button from '@material-ui/core/Button';
import { fade } from '@material-ui/core/styles/colorManipulator';
import { withStyles } from '@material-ui/core/styles';
import SearchIcon from '@material-ui/icons/Search';
import { withAuth } from '@okta/okta-react';
const styles = theme => ({
root: {
width: '100%',
},
MuiAppBar: {
alignItems: 'center'
},
grow: {
flexGrow: 1,
},
title: {
display: 'none',
[theme.breakpoints.up('sm')]: {
display: 'block',
},
},
search: {
position: 'relative',
borderRadius: theme.shape.borderRadius,
backgroundColor: fade(theme.palette.common.white, 0.15),
'&:hover': {
backgroundColor: fade(theme.palette.common.white, 0.25),
},
marginRight: theme.spacing.unit * 2,
marginLeft: 0,
width: '100%',
[theme.breakpoints.up('sm')]: {
marginLeft: theme.spacing.unit * 3,
width: 'auto',
},
},
searchIcon: {
width: theme.spacing.unit * 9,
height: '100%',
position: 'absolute',
pointerEvents: 'none',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
},
inputRoot: {
color: 'inherit',
width: '100%',
},
inputInput: {
paddingTop: theme.spacing.unit,
paddingRight: theme.spacing.unit,
paddingBottom: theme.spacing.unit,
paddingLeft: theme.spacing.unit * 10,
transition: theme.transitions.create('width'),
width: '100%',
[theme.breakpoints.up('md')]: {
width: 400,
},
},
toolbar: {
alignItems: 'center'
}
});
class SearchBar extends React.Component {
constructor(props) {
super(props);
this.logout = this.logout.bind(this);
}
async logout(e) {
e.preventDefault();
this.props.auth.logout('/');
}
render() {
const { classes } = this.props;
return (
<div className={classes.root}>
<AppBar position="static" style={{alignItems: 'center'}}>
<Toolbar>
<div className={classes.search}>
<div className={classes.searchIcon}>
<SearchIcon />
</div>
<InputBase
placeholder="Search for your OOS project on Github + Press Enter"
onKeyPress={this.props.onSearch}
classes={{
root: classes.inputRoot,
input: classes.inputInput,
}}
/>
</div>
<div className={classes.grow} />
<Button onClick={this.logout} color="inherit">Logout</Button>
</Toolbar>
</AppBar>
</div>
);
}
}
SearchBar.propTypes = {
classes: PropTypes.object.isRequired,
};
export default withStyles(styles)(withAuth(SearchBar));
The SearchBar component receives one prop called onSearch which is the function that should be called in each keyPress event triggered in the search text input.
The SearchBar uses the withAuth helper provided by Okta React SDK which will inject the auth object in the props of the component. The auth object has a method called logout that will wipe out all user related data from the session. This is exactly what you want in order to log the user out.
Now it’s time to work on the Home component. One of the dependencies the component has is the react-swipeable-views library which will add nice animations when the user changes tabs.
To install react-swipeable-views, run the command:
yarn add react-swipeable-views
You will also need to make HTTP calls to your Python ReST API as well as to the Github ReST API. The Github HTTP client will need to have a method or function to make a request to this URL: https://api.github.com/search/repositories?q=USER-QUERY. You are going to use the q query string to pass the term the user wants to query against Github’s repositories.
Create a file named githubClient.js.
touch src/githubClient.js
Paste the following content in it:
export default {
getJSONRepos(query) {
return fetch('https://api.github.com/search/repositories?q=' + query).then(response => response.json());
}
}
Now, you need to create an HTTP client to make HTTP calls to the Python ReST API you implemented in the first section of this tutorial. Since all the requests made to your Python ReST API require the user to be authenticated, you will need to set the Authorization HTTP Header with the accessToken provided by Okta.
Go ahead and create a file named apiClient.js
touch src/apiClient.js
And install axios to help you to perform HTTP calls to your flask API.
yarn add axios
Then, paste the following content:
import axios from 'axios';
const BASE_URI = 'http://localhost:4433';
const client = axios.create({
baseURL: BASE_URI,
json: true
});
class APIClient {
constructor(accessToken) {
this.accessToken = accessToken;
}
createKudo(repo) {
return this.perform('post', '/kudos', repo);
}
deleteKudo(repo) {
return this.perform('delete', `/kudos/${repo.id}`);
}
getKudos() {
return this.perform('get', '/kudos');
}
async perform (method, resource, data) {
return client({
method,
url: resource,
data,
headers: {
Authorization: `Bearer ${this.accessToken}`
}
}).then(resp => {
return resp.data ? resp.data : [];
})
}
}
export default APIClient;
Great! Your APIClient’s method perform is adding the user’s accessToken to the Authorization HTTP header of every request, which means, it’s authenticating every request. When the server receives these HTTP requests your Okta middleware will be able to verify the token and to extract user details from it as well.
Normally, you might create separate components for getting the user’s bookmarks and for searching for github repos. For simplicity’s sake you’ll put them all in the HomeComponent
Paste the following content in the src/Home/index.js file.
import React from 'react';
import { withStyles } from '@material-ui/core/styles';
import SwipeableViews from 'react-swipeable-views';
import Tabs from '@material-ui/core/Tabs';
import Tab from '@material-ui/core/Tab';
import Grid from '@material-ui/core/Grid';
import { withAuth } from '@okta/okta-react';
import GithubRepo from "../GithubRepo"
import SearchBar from "../SearchBar"
import githubClient from '../githubClient'
import APIClient from '../apiClient'
const styles = theme => ({
root: {
flexGrow: 1,
marginTop: 30
},
paper: {
padding: theme.spacing.unit * 2,
textAlign: 'center',
color: theme.palette.text.secondary,
},
});
class Home extends React.Component {
state = {
value: 0,
repos: [],
kudos: []
};
async componentDidMount() {
const accessToken = await this.props.auth.getAccessToken()
this.apiClient = new APIClient(accessToken);
this.apiClient.getKudos().then((data) =>
this.setState({...this.state, kudos: data})
);
}
handleTabChange = (event, value) => {
this.setState({ value });
};
handleTabChangeIndex = index => {
this.setState({ value: index });
};
resetRepos = repos => this.setState({ ...this.state, repos })
isKudo = repo => this.state.kudos.find(r => r.id == repo.id)
onKudo = (repo) => {
this.updateBackend(repo);
}
updateBackend = (repo) => {
if (this.isKudo(repo)) {
this.apiClient.deleteKudo(repo);
} else {
this.apiClient.createKudo(repo);
}
this.updateState(repo);
}
updateState = (repo) => {
if (this.isKudo(repo)) {
this.setState({
...this.state,
kudos: this.state.kudos.filter( r => r.id !== repo.id )
})
} else {
this.setState({
...this.state,
kudos: [repo, ...this.state.kudos]
})
}
}
onSearch = (event) => {
const target = event.target;
if (!target.value || target.length < 3) { return }
if (event.which !== 13) { return }
githubClient
.getJSONRepos(target.value)
.then((response) => {
target.blur();
this.setState({ ...this.state, value: 1 });
this.resetRepos(response.items);
})
}
renderRepos = (repos) => {
if (!repos) { return [] }
return repos.map((repo) => {
return (
<Grid item xs={12} md={3} key={repo.id}>
<GithubRepo onKudo={this.onKudo} isKudo={this.isKudo(repo)} repo={repo} />
</Grid>
);
})
}
render() {
return (
<div className={styles.root}>
<SearchBar auth={this.props.auth} onSearch={this.onSearch} />
<Tabs
value={this.state.value}
onChange={this.handleTabChange}
indicatorColor="primary"
textColor="primary"
fullWidth
>
<Tab label="Kudos" />
<Tab label="Search" />
</Tabs>
<SwipeableViews
axis={'x-reverse'}
index={this.state.value}
onChangeIndex={this.handleTabChangeIndex}
>
<Grid container spacing={16} style={{padding: '20px 0'}}>
{ this.renderRepos(this.state.kudos) }
</Grid>
<Grid container spacing={16} style={{padding: '20px 0'}}>
{ this.renderRepos(this.state.repos) }
</Grid>
</SwipeableViews>
</div>
);
}
}
export default withStyles(styles)(withAuth(Home));
Now run npm start and open http://localhost:8080 in your browser. You should be able to login, search for GitHub repos, and favorite a repo and see it in your Kudos list!
If you want to see what the finished project looks like, you can see the code on GitHub.
*Originally published by Kleber Correia at *developer.okta.com
===================================================================
Thanks for reading :heart: If you liked this post, share it with all of your programming buddies! Follow me on Facebook | Twitter
Learn More☞ Complete Python Bootcamp: Go from zero to hero in Python 3
☞ Python and Django Full Stack Web Developer Bootcamp
☞ Python for Time Series Data Analysis
☞ Python Programming For Beginners From Scratch
☞ Beginner’s guide on Python: Learn python from scratch! (New)
☞ Python for Beginners: Complete Python Programming
☞ Django 2.1 & Python | The Ultimate Web Development Bootcamp
☞ Python eCommerce | Build a Django eCommerce Web Application
In this Python Flask tutorial, you'll learn to build CRUD web applications using Python and Flask. Python and Flask can make building a CRUD app super easy.
In this three-part tutorial, we'll build a CRUD (Create, Read, Update, Delete) employee management web app using Flask, a microframework for Python.
Python Flask for Beginners: Build a CRUD Web App with Python and Flask - Part OneI’ve named the app Project Dream Team, and it will have the following features:
Part One will cover:
Ready? Here we go!
This tutorial builds on my introductory tutorial, Getting Started With Flask, picking up where it left off. It assumes you have, to begin with, the following dependencies installed:
You should have a virtual environment set up and activated. You should also have the following file and directory structure:
├── dream-team
   ├── app
   │   ├── __init__.py
   │   ├── templates
   │   ├── models.py
  │   └── views.py
   ├── config.py
    ├── requirements.txt
   └── run.py
This project structure groups the similar components of the application together. The dream-team
directory houses all the project files. The app
directory is the application package, and houses different but interlinked modules of the application. All templates are stored in the templates
directory, all models are in the models.py
file, and all routes are in the views.py
file. The run.py
file is the application's entry point, the config.py
file contains the application configurations, and the requirements.txt
file contains the software dependencies for the application.
If you don't have these set up, please visit the introductory tutorial and catch up!
Flask has support for several relational database management systems, including SQLite, MySQL, and PostgreSQL. For this tutorial, we will be using MySQL. It’s popular and therefore has a lot of support, in addition to being scalable, secure, and rich in features.
We will install the following (remember to activate your virtual environment):
$ pip install flask-sqlalchemy mysql-python
We'll then create the MySQL database. Ensure you have MySQL installed and running, and then log in as the root user:
$ mysql -u root
mysql> CREATE USER 'dt_admin'@'localhost' IDENTIFIED BY 'dt2016';
Query OK, 0 rows affected (0.00 sec)
mysql> CREATE DATABASE dreamteam_db;
Query OK, 1 row affected (0.00 sec)
mysql> GRANT ALL PRIVILEGES ON dreamteam_db . * TO 'dt_admin'@'localhost';
Query OK, 0 rows affected (0.00 sec)
We have now created a new user dt_admin
with the password dt2016
, created a new database dreamteam_db
, and granted the new user all database privileges.
Next, let's edit the config.py
. Remove any exisiting code and add the following:
# config.py
class Config(object):
"""
Common configurations
"""
# Put any configurations here that are common across all environments
class DevelopmentConfig(Config):
"""
Development configurations
"""
DEBUG = True
SQLALCHEMY_ECHO = True
class ProductionConfig(Config):
"""
Production configurations
"""
DEBUG = False
app_config = {
'development': DevelopmentConfig,
'production': ProductionConfig
}
It is good practice to specify configurations for different environments. In the file above, we have specifed configurations for development, which we will use while building the app and running it locally, as well as production, which we will use when the app is deployed.
Some useful configuration variables are:
You can find more Flask configuration variables here and SQLAlchemy configuration variables here.
Next, create an instance
directory in the dream-team
directory, and then create a config.py
file inside it. We will put configuration variables here that will not be pushed to version control due to their sensitive nature. In this case, we put the secret key as well as the database URI which contains the database user password.
# instance/config.py
SECRET_KEY = 'p9Bv<3Eid9%$i01'
SQLALCHEMY_DATABASE_URI = 'mysql://dt_admin:[email protected]/dreamteam_db'
Now, let's edit the app/__init__.py
file. Remove any existing code and add the following:
# app/__init__.py
# third-party imports
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
# local imports
from config import app_config
# db variable initialization
db = SQLAlchemy()
def create_app(config_name):
app = Flask(__name__, instance_relative_config=True)
app.config.from_object(app_config[config_name])
app.config.from_pyfile('config.py')
db.init_app(app)
return app
We've created a function, create_app
that, given a configuration name, loads the correct configuration from the config.py
file, as well as the configurations from the instance/config.py
file. We have also created a db
object which we will use to interact with the database.
Next, let's edit the run.py
file:
# run.py
import os
from app import create_app
config_name = os.getenv('FLASK_CONFIG')
app = create_app(config_name)
if __name__ == '__main__':
app.run()
We create the app by running the create_app
function and passing in the configuration name. We get this from the OS environment variable FLASK_CONFIG
. Because we are in development, we should set the environment variable to development
.
Let's run the app to ensure everything is working as expected. First, delete the app/views.py
file as well as the app/templates
directory as we will not be needing them going forward. Next, add a temporary route to the app/__init__.py
file as follows:
# app/__init__.py
# existing code remains
def create_app(config_name):
# existing code remains
# temporary route
@app.route('/')
def hello_world():
return 'Hello, World!'
return app
Make sure you set the FLASK_CONFIG
and FLASK_APP
environment variables before running the app:
$ export FLASK_CONFIG=development
$ export FLASK_APP=run.py
$ flask run
* Serving Flask app "run"
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
We can see the “Hello, World” string we set in the route. The app is working well so far.
Now to work on the models. Remember that a model is a representation of a database table in code. We'll need three models: Employee
, Department
, and Role
.
But first, let’s install Flask-Login, which will help us with user management and handle logging in, logging out, and user sessions. The Employee
model will inherit from Flask-Login’s UserMixin
class which will make it easier for us to make use of its properties and methods.
$ pip install flask-login
To use Flask-Login, we need to create a LoginManager object and initialize it in the app/__init__.py
file. First, remove the route we added earlier, and then add the following:
# app/__init__.py
# after existing third-party imports
from flask_login import LoginManager
# after the db variable initialization
login_manager = LoginManager()
def create_app(config_name):
# existing code remains
login_manager.init_app(app)
login_manager.login_message = "You must be logged in to access this page."
login_manager.login_view = "auth.login"
return app
In addition to initializing the LoginManager object, we've also added a login_view
and login_message
to it. This way, if a user tries to access a page that they are not authorized to, it will redirect to the specified view and display the specified message. We haven't created the auth.login
view yet, but we will when we get to authentication.
Now add the following code to the app/models.py
file:
# app/models.py
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
from app import db, login_manager
class Employee(UserMixin, db.Model):
"""
Create an Employee table
"""
# Ensures table will be named in plural and not in singular
# as is the name of the model
__tablename__ = 'employees'
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(60), index=True, unique=True)
username = db.Column(db.String(60), index=True, unique=True)
first_name = db.Column(db.String(60), index=True)
last_name = db.Column(db.String(60), index=True)
password_hash = db.Column(db.String(128))
department_id = db.Column(db.Integer, db.ForeignKey('departments.id'))
role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
is_admin = db.Column(db.Boolean, default=False)
@property
def password(self):
"""
Prevent pasword from being accessed
"""
raise AttributeError('password is not a readable attribute.')
@password.setter
def password(self, password):
"""
Set password to a hashed password
"""
self.password_hash = generate_password_hash(password)
def verify_password(self, password):
"""
Check if hashed password matches actual password
"""
return check_password_hash(self.password_hash, password)
def __repr__(self):
return '<Employee: {}>'.format(self.username)
# Set up user_loader
@login_manager.user_loader
def load_user(user_id):
return Employee.query.get(int(user_id))
class Department(db.Model):
"""
Create a Department table
"""
__tablename__ = 'departments'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(60), unique=True)
description = db.Column(db.String(200))
employees = db.relationship('Employee', backref='department',
lazy='dynamic')
def __repr__(self):
return '<Department: {}>'.format(self.name)
class Role(db.Model):
"""
Create a Role table
"""
__tablename__ = 'roles'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(60), unique=True)
description = db.Column(db.String(200))
employees = db.relationship('Employee', backref='role',
lazy='dynamic')
def __repr__(self):
return '<Role: {}>'.format(self.name)
In the Employee
model, we make use of some of Werkzeug's handy security helper methods, generate_password_hash
, which allows us to hash passwords, and check_password_hash
, which allows us ensure the hashed password matches the password. To enhance security, we have a password
method which ensures that the password can never be accessed; instead an error will be raised. We also have two foreign key fields, department_id
and role_id
, which refer to the ID's of the department and role assigned to the employee.
Note that we have an is_admin
field which is set to False
by default. We will override this when creating the admin user. Just after the Employee
model, we have a user_loader
callback, which Flask-Login uses to reload the user object from the user ID stored in the session.
The Department
and Role
models are quite similar. Both have name
and description
fields. Additionally, both have a one-to-many relationship with the Employee
model (one department or role can have many employees). We define this in both models using the employees
field. backref
allows us to create a new property on the Employee
model such that we can use employee.department
or employee.role
to get the department or role assigned to that employee. lazy
defines how the data will be loaded from the database; in this case it will be loaded dynamically, which is ideal for managing large collections.
Migrations allow us to manage changes we make to the models, and propagate these changes in the database. For example, if later on we make a change to a field in one of the models, all we will need to do is create and apply a migration, and the database will reflect the change.
We’ll begin by installing Flask-Migrate, which will handle the database migrations using Alembic, a lightweight database migration tool. Alembic emits ALTER
statements to a database thus implememting changes made to the models. It also auto-generates minimalistic migration scripts, which may be complex to write.
$ pip install flask-migrate
We'll need to edit the app/__init__.py
file:
# app/__init__.py
# after existing third-party imports
from flask_migrate import Migrate
# existing code remains
def create_app(config_name):
# existing code remains
migrate = Migrate(app, db)
from app import models
return app
We have created a migrate
object which will allow us to run migrations using Flask-Migrate. We have also imported the models from the app
package. Next, we'll run the following command to create a migration repository:
$ flask db init
This creates a migrations
directory in the dream-team
directory:
└── migrations
├── README
├── alembic.ini
├── env.py
├── script.py.mako
└── versions
Next, we will create the first migration:
$ flask db migrate
Finally, we'll apply the migration:
$ flask db upgrade
We've sucessfully created tables based on the models we wrote! Let's check the MySQL database to confirm this:
$ mysql -u root
mysql> use dreamteam_db;
mysql> show tables;
+------------------------+
| Tables_in_dreamteam_db |
+------------------------+
| alembic_version |
| departments |
| employees |
| roles |
+------------------------+
4 rows in set (0.00 sec)
Blueprints are great for organising a flask app into components, each with its own views and forms. I find that blueprints make for a cleaner and more organised project structure because each blueprint is a distinct component that addresses a specific functionality of the app. Each blueprint can even have its own cutsom URL prefix or subdomain. Blueprints are particularly convenient for large applications.
We're going to have three blueprints in this app:
Create the relevant files and directories so that your directory structure resembles this:
└── dream-team
├── app
│   ├── __init__.py
│   ├── admin
│   │   ├── __init__.py
│   │   ├── forms.py
│   │   └── views.py
│   ├── auth
│   │   ├── __init__.py
│   │   ├── forms.py
│   │   └── views.py
│   ├── home
│   │   ├── __init__.py
│   │   └── views.py
│   ├── models.py
│   ├── static
│   └── templates
├── config.py
├── instance
│   └── config.py
├── migrations
│   ├── README
│   ├── alembic.ini
│   ├── env.py
│   ├── script.py.mako
│   └── versions
│   └── a1a1d8b30202_.py
├── requirements.txt
└── run.py
I chose not to have static
and templates
directories for each blueprint, because all the application templates will inherit from the same base template and use the same CSS file. Instead, the templates
directory will have sub-directories for each blueprint so that blueprint templates can be grouped together.
In each blueprint's __init__.py
file, we need to create a Blueprint object and initialize it with a name. We also need to import the views.
# app/admin/__init__.py
from flask import Blueprint
admin = Blueprint('admin', __name__)
from . import views
# app/auth/__init__.py
from flask import Blueprint
auth = Blueprint('auth', __name__)
from . import views
# app/home/__init__.py
from flask import Blueprint
home = Blueprint('home', __name__)
from . import views
Then, we can register the blueprints on the app in the app/__init__.py
file, like so:
# app/__init__.py
# existing code remains
def create_app(config_name):
# existing code remains
from app import models
from .admin import admin as admin_blueprint
app.register_blueprint(admin_blueprint, url_prefix='/admin')
from .auth import auth as auth_blueprint
app.register_blueprint(auth_blueprint)
from .home import home as home_blueprint
app.register_blueprint(home_blueprint)
return app
We have imported each blueprint object and registered it. For the admin
blueprint, we have added a url prefix, /admin
. This means that all the views for this blueprint will be accessed in the browser with the url prefix admin
.
Time to work on fleshing out the blueprints! We'll start with the home
blueprint, which will have the homepage as well as the dashboard.
# app/home/views.py
from flask import render_template
from flask_login import login_required
from . import home
@home.route('/')
def homepage():
"""
Render the homepage template on the / route
"""
return render_template('home/index.html', title="Welcome")
@home.route('/dashboard')
@login_required
def dashboard():
"""
Render the dashboard template on the /dashboard route
"""
return render_template('home/dashboard.html', title="Dashboard")
Each view function has a decorator, home.route
, which has a URL route as a parameter (remember that home
is the name of the blueprint as specified in the app/home/__init__.py
file). Each view handles requests to the specified URL.
The homepage
view renders the home template, while the dashboard
view renders the dashboard template. Note that the dashboard
view has a login_required
decorator, meaning that users must be logged in to access it.
Now to work on the base template, which all other templates will inherit from. Create a base.html
file in the app/templates
directory and add the following code:
<!-- app/templates/base.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<title>{{ title }} | Project Dream Team</title>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
<link href="{{ url_for('static', filename='css/style.css') }}" rel="stylesheet">
<link rel="shortcut icon" href="{{ url_for('static', filename='img/favicon.ico') }}">
</head>
<body>
<nav class="navbar navbar-default navbar-fixed-top topnav" role="navigation">
<div class="container topnav">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand topnav" href="{{ url_for('home.homepage') }}">Project Dream Team</a>
</div>
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav navbar-right">
<li><a href="{{ url_for('home.homepage') }}">Home</a></li>
<li><a href="#">Register</a></li>
<li><a href="#">Login</a></li>
</ul>
</div>
</div>
</nav>
<div class="wrapper">
{% block body %}
{% endblock %}
<div class="push"></div>
</div>
<footer>
<div class="container">
<div class="row">
<div class="col-lg-12">
<ul class="list-inline">
<li><a href="{{ url_for('home.homepage') }}">Home</a></li>
<li class="footer-menu-divider">⋅</li>
<li><a href="#">Register</a></li>
<li class="footer-menu-divider">⋅</li>
<li><a href="#">Login</a></li>
</ul>
<p class="copyright text-muted small">Copyright © 2016. All Rights Reserved</p>
</div>
</div>
</div>
</footer>
</body>
</html>
Note that we use #
for the Register and Login links. We will update this when we are working on the auth
blueprint.
Next, create a home
directory inside the app/templates
directory. The homepage template, index.html
, will go inside it:
<!-- app/templates/home/index.html -->
{% extends "base.html" %}
{% block title %}Home{% endblock %}
{% block body %}
<div class="intro-header">
<div class="container">
<div class="row">
<div class="col-lg-12">
<div class="intro-message">
<h1>Project Dream Team</h1>
<h3>The best company in the world!</h3>
<hr class="intro-divider">
</ul>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
Inside the static
directory, add css
and img
directories. Add the following CSS file, style.css
, to your static/css
directory (note that you will need a background image, intro-bg.jpg
, as well as a favicon in your static/img
directory):
/* app/static/css/style.css */
body, html {
width: 100%;
height: 100%;
}
body, h1, h2, h3 {
font-family: "Lato", "Helvetica Neue", Helvetica, Arial, sans-serif;
font-weight: 700;
}
a, .navbar-default .navbar-brand, .navbar-default .navbar-nav>li>a {
color: #aec251;
}
a:hover, .navbar-default .navbar-brand:hover, .navbar-default .navbar-nav>li>a:hover {
color: #687430;
}
footer {
padding: 50px 0;
background-color: #f8f8f8;
}
p.copyright {
margin: 15px 0 0;
}
.alert-info {
width: 50%;
margin: auto;
color: #687430;
background-color: #e6ecca;
border-color: #aec251;
}
.btn-default {
border-color: #aec251;
color: #aec251;
}
.btn-default:hover {
background-color: #aec251;
}
.center {
margin: auto;
width: 50%;
padding: 10px;
}
.content-section {
padding: 50px 0;
border-top: 1px solid #e7e7e7;
}
.footer, .push {
clear: both;
height: 4em;
}
.intro-divider {
width: 400px;
border-top: 1px solid #f8f8f8;
border-bottom: 1px solid rgba(0,0,0,0.2);
}
.intro-header {
padding-top: 50px;
padding-bottom: 50px;
text-align: center;
color: #f8f8f8;
background: url(../img/intro-bg.jpg) no-repeat center center;
background-size: cover;
height: 100%;
}
.intro-message {
position: relative;
padding-top: 20%;
padding-bottom: 20%;
}
.intro-message > h1 {
margin: 0;
text-shadow: 2px 2px 3px rgba(0,0,0,0.6);
font-size: 5em;
}
.intro-message > h3 {
text-shadow: 2px 2px 3px rgba(0,0,0,0.6);
}
.lead {
font-size: 18px;
font-weight: 400;
}
.topnav {
font-size: 14px;
}
.wrapper {
min-height: 100%;
height: auto !important;
height: 100%;
margin: 0 auto -4em;
}
Run the app; you should be able to see the homepage now.
For the auth
blueprint, we’ll begin by creating the registration and login forms. We’ll use Flask-WTF, which will allow us to create forms that are secure (thanks to CSRF protection and reCAPTCHA support).
pip install Flask-WTF
Now to write the code for the forms:
# app/auth/forms.py
from flask_wtf import FlaskForm
from wtforms import PasswordField, StringField, SubmitField, ValidationError
from wtforms.validators import DataRequired, Email, EqualTo
from ..models import Employee
class RegistrationForm(FlaskForm):
"""
Form for users to create new account
"""
email = StringField('Email', validators=[DataRequired(), Email()])
username = StringField('Username', validators=[DataRequired()])
first_name = StringField('First Name', validators=[DataRequired()])
last_name = StringField('Last Name', validators=[DataRequired()])
password = PasswordField('Password', validators=[
DataRequired(),
EqualTo('confirm_password')
])
confirm_password = PasswordField('Confirm Password')
submit = SubmitField('Register')
def validate_email(self, field):
if Employee.query.filter_by(email=field.data).first():
raise ValidationError('Email is already in use.')
def validate_username(self, field):
if Employee.query.filter_by(username=field.data).first():
raise ValidationError('Username is already in use.')
class LoginForm(FlaskForm):
"""
Form for users to login
"""
email = StringField('Email', validators=[DataRequired(), Email()])
password = PasswordField('Password', validators=[DataRequired()])
submit = SubmitField('Login')
Flask-WTF has a number of validators that make writing forms much easier. All the fields in the models have the DataRequired
validator, which means that users will be required to fill all of them in order to register or login.
For the registration form, we require users to fill in their email address, username, first name, last name, and their password twice. We use the Email
validator to ensure valid email formats are used (e.g [email protected]
.) We use the EqualTo
validator to confirm that the password
and confirm_password
fields in the RegistrationForm
match. We also create methods (validate_email
and validate_username
) to ensure that the email and username entered have not been used before.
The submit
field in both forms will be represented as a button that users will be able to click to register and login respectively.
With the forms in place, we can write the views:
# app/auth/views.py
from flask import flash, redirect, render_template, url_for
from flask_login import login_required, login_user, logout_user
from . import auth
from forms import LoginForm, RegistrationForm
from .. import db
from ..models import Employee
@auth.route('/register', methods=['GET', 'POST'])
def register():
"""
Handle requests to the /register route
Add an employee to the database through the registration form
"""
form = RegistrationForm()
if form.validate_on_submit():
employee = Employee(email=form.email.data,
username=form.username.data,
first_name=form.first_name.data,
last_name=form.last_name.data,
password=form.password.data)
# add employee to the database
db.session.add(employee)
db.session.commit()
flash('You have successfully registered! You may now login.')
# redirect to the login page
return redirect(url_for('auth.login'))
# load registration template
return render_template('auth/register.html', form=form, title='Register')
@auth.route('/login', methods=['GET', 'POST'])
def login():
"""
Handle requests to the /login route
Log an employee in through the login form
"""
form = LoginForm()
if form.validate_on_submit():
# check whether employee exists in the database and whether
# the password entered matches the password in the database
employee = Employee.query.filter_by(email=form.email.data).first()
if employee is not None and employee.verify_password(
form.password.data):
# log employee in
login_user(employee)
# redirect to the dashboard page after login
return redirect(url_for('home.dashboard'))
# when login details are incorrect
else:
flash('Invalid email or password.')
# load login template
return render_template('auth/login.html', form=form, title='Login')
@auth.route('/logout')
@login_required
def logout():
"""
Handle requests to the /logout route
Log an employee out through the logout link
"""
logout_user()
flash('You have successfully been logged out.')
# redirect to the login page
return redirect(url_for('auth.login'))
Just like in the home
blueprint, each view here handles requests to the specified URL. The register
view creates an instance of the Employee
model class using the registration form data to populate the fields, and then adds it to the database. This esentially registers a new employee.
The login
view queries the database to check whether an employee exists with an email address that matches the email provided in the login form data. It then uses the verify_password
method to check that the password in the database for the employee matches the password provided in the login form data. If both of these are true, it proceeds to log the user in using the login_user
method provided by Flask-Login.
The logout
view has the login_required
decorator, which means that a user must be logged in to access it. It calles the logout_user
method provided by Flask-Login to log the user out.
Note the use of flash
method, which allows us to use Flask’s message flashing feature. This allows us to communicate feedback to the user, such as informing them of successful registration or unsuccessful login.
Finally, let’s work on the templates. First, we’ll install Flask-Bootstrap so we can use its wtf
and utils
libraries. The wtf
library will allow us to quickly generate forms in the templates based on the forms in the forms.py
file. The utils
library will allow us to display the flash messages we set earlier to give feedback to the user.
pip install flask-bootstrap
We need to edit the app/__init__.py
file to use Flask-Bootstrap:
# app/__init__.py
# after existing third-party imports
from flask_bootstrap import Bootstrap
# existing code remains
def create_app(config_name):
# existing code remains
Bootstrap(app)
from app import models
# blueprint registration remains here
return app
We've made quite a number of edits to the app/__init__.py
file. This is the final version of the file and how it should look at this point (note that I have re-arranged the imports and variables in alphabetical order):
# app/__init__.py
# third-party imports
from flask import Flask
from flask_bootstrap import Bootstrap
from flask_login import LoginManager
from flask_migrate import Migrate
from flask_sqlalchemy import SQLAlchemy
# local imports
from config import app_config
db = SQLAlchemy()
login_manager = LoginManager()
def create_app(config_name):
app = Flask(__name__, instance_relative_config=True)
app.config.from_object(app_config[config_name])
app.config.from_pyfile('config.py')
Bootstrap(app)
db.init_app(app)
login_manager.init_app(app)
login_manager.login_message = "You must be logged in to access this page."
login_manager.login_view = "auth.login"
migrate = Migrate(app, db)
from app import models
from .admin import admin as admin_blueprint
app.register_blueprint(admin_blueprint, url_prefix='/admin')
from .auth import auth as auth_blueprint
app.register_blueprint(auth_blueprint)
from .home import home as home_blueprint
app.register_blueprint(home_blueprint)
return app
We need two templates for the auth
blueprint: register.html
and login.html
, which we'll create in an auth
directory inside the templates
directory.
<!-- app/templates/auth/register.html -->
{% import "bootstrap/wtf.html" as wtf %}
{% extends "base.html" %}
{% block title %}Register{% endblock %}
{% block body %}
<div class="content-section">
<div class="center">
<h1>Register for an account</h1>
<br/>
{{ wtf.quick_form(form) }}
</div>
</div>
{% endblock %}
<!-- app/templates/auth/login.html -->
{% import "bootstrap/utils.html" as utils %}
{% import "bootstrap/wtf.html" as wtf %}
{% extends "base.html" %}
{% block title %}Login{% endblock %}
{% block body %}
<div class="content-section">
<br/>
{{ utils.flashed_messages() }}
<br/>
<div class="center">
<h1>Login to your account</h1>
<br/>
{{ wtf.quick_form(form) }}
</div>
</div>
{% endblock %}
The forms are loaded from the app/auth/views.py
file, where we specified which template files to display for each view. Remember the Register and Login links in the base template? Let's update them now so we can access the pages from the menus:
<!-- app/templates/base.html -->
<!-- Modify nav bar menu -->
<ul class="nav navbar-nav navbar-right">
<li><a href="{{ url_for('home.homepage') }}">Home</a></li>
<li><a href="{{ url_for('auth.register') }}">Register</a></li>
<li><a href="{{ url_for('auth.login') }}">Login</a></li>
</ul>
<!-- Modify footer menu -->
<ul class="list-inline">
<li><a href="{{ url_for('home.homepage') }}">Home</a></li>
<li class="footer-menu-divider">⋅</li>
<li><a href="{{ url_for('auth.register') }}">Register</a></li>
<li class="footer-menu-divider">⋅</li>
<li><a href="{{ url_for('auth.login') }}">Login</a></li>
</ul>
Run the app again and click on the Register and Login menu links. You should see the templates loaded with the appropriate form.
Try to fill out the registration form; you should be able to register a new employee. After registration, you should be redirected to the login page, where you will see the flash message we configured in the app/auth/views.py
file, inviting you to login.
Logging in should be successful; however you should get a Template Not Found
error after logging in, because the dashboard.html
template has not been created yet. Let's do that now:
<!-- app/templates/home/dashboard.html -->
{% extends "base.html" %}
{% block title %}Dashboard{% endblock %}
{% block body %}
<div class="intro-header">
<div class="container">
<div class="row">
<div class="col-lg-12">
<div class="intro-message">
<h1>The Dashboard</h1>
<h3>We made it here!</h3>
<hr class="intro-divider">
</ul>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
Refresh the page. You'll notice that the navigation menu still has the register and login links, even though we are already logged in. We'll need to modify it to show a logout link when a user is already authenticated. We will also include a Hi, username!
message in the nav bar:
<!-- app/templates/base.html -->
<!-- In the head tag, include link to Font Awesome CSS so we can use icons -->
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
<!-- Modify nav bar menu -->
<ul class="nav navbar-nav navbar-right">
{% if current_user.is_authenticated %}
<li><a href="{{ url_for('home.dashboard') }}">Dashboard</a></li>
<li><a href="{{ url_for('auth.logout') }}">Logout</a></li>
<li><a><i class="fa fa-user"></i> Hi, {{ current_user.username }}!</a></li>
{% else %}
<li><a href="{{ url_for('home.homepage') }}">Home</a></li>
<li><a href="{{ url_for('auth.register') }}">Register</a></li>
<li><a href="{{ url_for('auth.login') }}">Login</a></li>
{% endif %}
</ul>
<!-- Modify footer menu -->
<ul class="list-inline">
<li><a href="{{ url_for('home.homepage') }}">Home</a></li>
<li class="footer-menu-divider">⋅</li>
{% if current_user.is_authenticated %}
<li><a href="{{ url_for('auth.logout') }}">Logout</a></li>
{% else %}
<li><a href="{{ url_for('auth.register') }}">Register</a></li>
<li class="footer-menu-divider">⋅</li>
<li><a href="{{ url_for('auth.login') }}">Login</a></li>
{% endif %}
</ul>
Note how we use if-else
statements in the templates. Also, take note of the current_user
proxy provided by Flask-Login, which allows us to check whether the user is authenticated and to get the user's username.
Logging out will take you back to the login page:
Attempting to access the dashboard page without logging in will redirect you to the login page and display the message we set in the app/__init__.py
file:
Notice that the URL is configured such that once you log in, you will be redirected to the page you initially attempted to access, which in this case is the dashboard.
That's it for Part One! We've covered quite a lot: setting up a MySQL database, creating models, migrating the database, and handling registration, login, and logout. Good job for making it this far!
Watch this space for Part Two, which will cover the CRUD functionality of the app, allowing admin users to add, list, edit, and delete departments and roles, as well as assign them to employees.
Python Flask for Beginners: Build a CRUD Web App with Python and Flask - Part TwoThis is Part Two of a three-part tutorial to build an employee management web app, named Project Dream Team. In Part One) of the tutorial, we set up a MySQL database using MySQL-Python and Flask-SQLAlchemy. We created models, migrated the database, and worked on the home
and auth
blueprints and templates. By the end of Part One, we had a working app that had a homepage, registration page, login page, and dashboard. We could register a new user, login, and logout.
In Part Two, we will work on:
We'll start by creating an admin user through the command line. Flask provides a handy command, flask shell
, that allows us to use an interactive Python shell for use with Flask apps.
$ flask shell
>>> from app.models import Employee
>>> from app import db
>>> admin = Employee(email="[email protected]",username="admin",password="admin2016",is_admin=True)
>>> db.session.add(admin)
>>> db.session.commit()
We've just created a user with a username, admin
, and a password, admin2016
. Recall that we set the is_admin
field to default to False
in the Employee
model. To create the admin user above, we override the default value of is_admin
and set it to True
.
Now that we have an admin user, we need to add a view for an admin dashboard. We also need to ensure that once the admin user logs in, they are redirected to the admin dashboard and not the one for non-admin users. We will do this in the home
blueprint.
# app/home/views.py
# update imports
from flask import abort, render_template
from flask_login import current_user, login_required
# add admin dashboard view
@home.route('/admin/dashboard')
@login_required
def admin_dashboard():
# prevent non-admins from accessing the page
if not current_user.is_admin:
abort(403)
return render_template('home/admin_dashboard.html', title="Dashboard")
# app/auth/views.py
# Edit the login view to redirect to the admin dashboard if employee is an admin
@auth.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
# check whether employee exists in the database and whether
# the password entered matches the password in the database
employee = Employee.query.filter_by(email=form.email.data).first()
if employee is not None and employee.verify_password(
form.password.data):
# log employee in
login_user(employee)
# redirect to the appropriate dashboard page
if employee.is_admin:
return redirect(url_for('home.admin_dashboard'))
else:
return redirect(url_for('home.dashboard'))
# when login details are incorrect
else:
flash('Invalid email or password.')
# load login template
return render_template('auth/login.html', form=form, title='Login')
Next we'll create the admin dashboard template. Create an admin_dashboard.html
file in the templates/home
directory, and then add the following code in it:
<!-- app/templates/home/admin_dashboard.html -->
{% extends "base.html" %}
{% block title %}Admin Dashboard{% endblock %}
{% block body %}
<div class="intro-header">
<div class="container">
<div class="row">
<div class="col-lg-12">
<div class="intro-message">
<h1>Admin Dashboard</h1>
<h3>For administrators only!</h3>
<hr class="intro-divider">
</ul>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
Now we need to edit the base template to show a different menu for the admin user.
<!-- app/templates/base.html -->
<!-- Modify nav bar menu -->
<ul class="nav navbar-nav navbar-right">
{% if current_user.is_authenticated %}
{% if current_user.is_admin %}
<li><a href="{{ url_for('home.admin_dashboard') }}">Dashboard</a></li>
<li><a href="#">Departments</a></li>
<li><a href="#">Roles</a></li>
<li><a href="#">Employees</a></li>
{% else %}
<li><a href="{{ url_for('home.dashboard') }}">Dashboard</a></li>
{% endif %}
<li><a href="{{ url_for('auth.logout') }}">Logout</a></li>
<li><a><i class="fa fa-user"></i> Hi, {{ current_user.username }}!</a></li>
{% else %}
<li><a href="{{ url_for('home.homepage') }}">Home</a></li>
<li><a href="{{ url_for('auth.register') }}">Register</a></li>
<li><a href="{{ url_for('auth.login') }}">Login</a></li>
{% endif %}
</ul>
In the menu above, we make use of the current_user
proxy from Flask-Login to check whether the current user is an admin. If they are, we display the admin menu which will allow them to navigate to the Departments, Roles and Employees pages. Notice that we use #
for the links in the admin menu. We will update this after we have created the respective views.
Now run the app and login as the admin user that we just created. You should see the admin dashboard:
Let's test the error we set in the home/views.py
file to prevent non-admin users from accessing the admin dashboard. Log out and then log in as a regular user. In your browser's address bar, manually enter the following URL: <a href="http://127.0.0.1:5000/admin/dashboard" target="_blank">http://127.0.0.1:5000/admin/dashboard</a>
. You should get a 403 Forbidden
error. It looks pretty boring now, but don't worry, we'll create custom error pages in Part Three!
Now we'll start working on the admin
blueprint, which has the bulk of the functionality in the application. We'll begin by building out CRUD functionality for the departments.
We'll start with the admin/forms.py
file, where we'll create a form to add and edit departments.
# app/admin/forms.py
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired
class DepartmentForm(FlaskForm):
"""
Form for admin to add or edit a department
"""
name = StringField('Name', validators=[DataRequired()])
description = StringField('Description', validators=[DataRequired()])
submit = SubmitField('Submit')
The form is pretty simple and has only two fields, name
and department
, both of which are required. We enforce this using the DataRequired()
validator from WTForms. Note that we will use the same form for adding and editing departments.
Now, let's work on the views:
# app/admin/views.py
from flask import abort, flash, redirect, render_template, url_for
from flask_login import current_user, login_required
from . import admin
from forms import DepartmentForm
from .. import db
from ..models import Department
def check_admin():
"""
Prevent non-admins from accessing the page
"""
if not current_user.is_admin:
abort(403)
# Department Views
@admin.route('/departments', methods=['GET', 'POST'])
@login_required
def list_departments():
"""
List all departments
"""
check_admin()
departments = Department.query.all()
return render_template('admin/departments/departments.html',
departments=departments, title="Departments")
@admin.route('/departments/add', methods=['GET', 'POST'])
@login_required
def add_department():
"""
Add a department to the database
"""
check_admin()
add_department = True
form = DepartmentForm()
if form.validate_on_submit():
department = Department(name=form.name.data,
description=form.description.data)
try:
# add department to the database
db.session.add(department)
db.session.commit()
flash('You have successfully added a new department.')
except:
# in case department name already exists
flash('Error: department name already exists.')
# redirect to departments page
return redirect(url_for('admin.list_departments'))
# load department template
return render_template('admin/departments/department.html', action="Add",
add_department=add_department, form=form,
title="Add Department")
@admin.route('/departments/edit/<int:id>', methods=['GET', 'POST'])
@login_required
def edit_department(id):
"""
Edit a department
"""
check_admin()
add_department = False
department = Department.query.get_or_404(id)
form = DepartmentForm(obj=department)
if form.validate_on_submit():
department.name = form.name.data
department.description = form.description.data
db.session.commit()
flash('You have successfully edited the department.')
# redirect to the departments page
return redirect(url_for('admin.list_departments'))
form.description.data = department.description
form.name.data = department.name
return render_template('admin/departments/department.html', action="Edit",
add_department=add_department, form=form,
department=department, title="Edit Department")
@admin.route('/departments/delete/<int:id>', methods=['GET', 'POST'])
@login_required
def delete_department(id):
"""
Delete a department from the database
"""
check_admin()
department = Department.query.get_or_404(id)
db.session.delete(department)
db.session.commit()
flash('You have successfully deleted the department.')
# redirect to the departments page
return redirect(url_for('admin.list_departments'))
return render_template(title="Delete Department")
We begin by creating a function, check_admin
, which throws a 403 Forbidden
error if a non-admin user attempts to access these views. We will call this function in every admin view.
The list_departments
view queries the database for all departments and assigns them to the variable departments
, which we will use to list them in the template.
The add_department
view creates a new department object using the form data, and adds it to the database. If the department name already exists, an error message is displayed. This view redirects to the list_departments
. This means that once the admin user creates a new department, they will be redirected to the Departments page.
The edit_department
view takes one parameter: id
. This is the department ID, and will be passed to the view in the template. The view queries the database for a department with the ID specified. If the department doesn't exist, a 404 Not Found
error is thrown. If it does, it is updated with the form data.
The delete_department
view is similar to the edit_department
one, in that it takes a department ID as a parameter and throws an error if the specified department doesn't exist. If it does, it is deleted from the database.
Note that we render the same template for adding and editing individual departments: department.html
. This is why we have the add_department
variable in the add_department
view (where it is set to True
), as well as in the edit_department
view (where it is set to False
). We'll use this variable in the department.html
template to determine what wording to use for the title and heading.
Create an templates/admin
directory, and in it, add a departments
directory. Inside it, add the departments.html
and department.html
files:
<!-- app/templates/admin/departments/departments.html -->
{% import "bootstrap/utils.html" as utils %}
{% extends "base.html" %}
{% block title %}Departments{% endblock %}
{% block body %}
<div class="content-section">
<div class="outer">
<div class="middle">
<div class="inner">
<br/>
{{ utils.flashed_messages() }}
<br/>
<h1 style="text-align:center;">Departments</h1>
{% if departments %}
<hr class="intro-divider">
<div class="center">
<table class="table table-striped table-bordered">
<thead>
<tr>
<th width="15%"> Name </th>
<th width="40%"> Description </th>
<th width="15%"> Employee Count </th>
<th width="15%"> Edit </th>
<th width="15%"> Delete </th>
</tr>
</thead>
<tbody>
{% for department in departments %}
<tr>
<td> {{ department.name }} </td>
<td> {{ department.description }} </td>
<td>
{% if department.employees %}
{{ department.employees.count() }}
{% else %}
0
{% endif %}
</td>
<td>
<a href="{{ url_for('admin.edit_department', id=department.id) }}">
<i class="fa fa-pencil"></i> Edit
</a>
</td>
<td>
<a href="{{ url_for('admin.delete_department', id=department.id) }}">
<i class="fa fa-trash"></i> Delete
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div style="text-align: center">
{% else %}
<div style="text-align: center">
<h3> No departments have been added. </h3>
<hr class="intro-divider">
{% endif %}
<a href="{{ url_for('admin.add_department') }}" class="btn btn-default btn-lg">
<i class="fa fa-plus"></i>
Add Department
</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
We've created a table in the template above, where we will display all the departments with their name, description, and number of employees. Take note of the count()
function, which we use in this case to get the number of employees. Each department listed will have an edit and delete link. Notice how we pass the department.id
value to the edit_department
and delete_department
views in the respective links.
If there are no departments, the page will display “No departments have been added”. There is also a button which can be clicked to add a new department.
Now let's work on the template for adding and editing departments:
<!-- app/templates/admin/departments/department.html -->
{% import "bootstrap/wtf.html" as wtf %}
{% extends "base.html" %}
{% block title %}
{% if add_department %}
Add Department
{% else %}
Edit Department
{% endif %}
{% endblock %}
{% block body %}
<div class="content-section">
<div class="outer">
<div class="middle">
<div class="inner">
<div class="center">
{% if add_department %}
<h1>Add Department</h1>
{% else %}
<h1>Edit Department</h1>
{% endif %}
<br/>
{{ wtf.quick_form(form) }}
</div>
</div>
</div>
</div>
</div>
{% endblock %}
Notice that we use the add_department
variable which we initialized in the admin/views.py
file, to determine whether the page title will be “Add Department” or “Edit Department”.
Add the following lines to your style.css
file:
/* app/static/css/style.css */
.outer {
display: table;
position: absolute;
height: 70%;
width: 100%;
}
.middle {
display: table-cell;
vertical-align: middle;
}
.inner {
margin-left: auto;
margin-right: auto;
}
The .middle
, .inner
, and .outer
classes are to center the content in the middle of the page.
Lastly, let's put the correct link to the Departments page in the admin menu:
<!-- app/templates/base.html -->
<!-- Modify nav bar menu -->
<li><a href="{{ url_for('admin.list_departments') }}">Departments</a></li>
Re-start the flask server, and then log back in as the admin user and click on the Departments link. Because we have not added any departments, loading the page will display:
Let's try adding a department:
It worked! We get the success message we configured in the add_department
view, and can now see the department displayed.
Now let's edit it:
Notice that the current department name and description are already pre-loaded in the form. Also, take note of the URL, which has the ID of the department we are editing.
Editing the department is successful as well. Clicking the Delete link deletes the department and redirects to the Departments page, where a confirmation message is displayed:
Now to work on the roles. This will be very similar to the departments code because the functionality for roles and departments is exactly the same.
We'll start by creating the form to add and edit roles. Add the following code to the admin/forms.py
file:
# app/admin/forms.py
# existing code remains
class RoleForm(FlaskForm):
"""
Form for admin to add or edit a role
"""
name = StringField('Name', validators=[DataRequired()])
description = StringField('Description', validators=[DataRequired()])
submit = SubmitField('Submit')
Next we'll write the views to add, list, edit, and delete roles. Add the following code to the admin/views.py file:
# app/admin/views.py
# update imports
from forms import DepartmentForm, RoleForm
from ..models import Department, Role
# existing code remains
# Role Views
@admin.route('/roles')
@login_required
def list_roles():
check_admin()
"""
List all roles
"""
roles = Role.query.all()
return render_template('admin/roles/roles.html',
roles=roles, title='Roles')
@admin.route('/roles/add', methods=['GET', 'POST'])
@login_required
def add_role():
"""
Add a role to the database
"""
check_admin()
add_role = True
form = RoleForm()
if form.validate_on_submit():
role = Role(name=form.name.data,
description=form.description.data)
try:
# add role to the database
db.session.add(role)
db.session.commit()
flash('You have successfully added a new role.')
except:
# in case role name already exists
flash('Error: role name already exists.')
# redirect to the roles page
return redirect(url_for('admin.list_roles'))
# load role template
return render_template('admin/roles/role.html', add_role=add_role,
form=form, title='Add Role')
@admin.route('/roles/edit/<int:id>', methods=['GET', 'POST'])
@login_required
def edit_role(id):
"""
Edit a role
"""
check_admin()
add_role = False
role = Role.query.get_or_404(id)
form = RoleForm(obj=role)
if form.validate_on_submit():
role.name = form.name.data
role.description = form.description.data
db.session.add(role)
db.session.commit()
flash('You have successfully edited the role.')
# redirect to the roles page
return redirect(url_for('admin.list_roles'))
form.description.data = role.description
form.name.data = role.name
return render_template('admin/roles/role.html', add_role=add_role,
form=form, title="Edit Role")
@admin.route('/roles/delete/<int:id>', methods=['GET', 'POST'])
@login_required
def delete_role(id):
"""
Delete a role from the database
"""
check_admin()
role = Role.query.get_or_404(id)
db.session.delete(role)
db.session.commit()
flash('You have successfully deleted the role.')
# redirect to the roles page
return redirect(url_for('admin.list_roles'))
return render_template(title="Delete Role")
These list, add, edit, and delete views are similar to the ones for departments that we created earlier.
Create a roles
directory in the templates/admin
directory. In it, create the roles.html
and role.html
files:
<!-- app/templates/admin/roles/roles.html -->
{% import "bootstrap/utils.html" as utils %}
{% extends "base.html" %}
{% block title %}Roles{% endblock %}
{% block body %}
<div class="content-section">
<div class="outer">
<div class="middle">
<div class="inner">
<br/>
{{ utils.flashed_messages() }}
<br/>
<h1 style="text-align:center;">Roles</h1>
{% if roles %}
<hr class="intro-divider">
<div class="center">
<table class="table table-striped table-bordered">
<thead>
<tr>
<th width="15%"> Name </th>
<th width="40%"> Description </th>
<th width="15%"> Employee Count </th>
<th width="15%"> Edit </th>
<th width="15%"> Delete </th>
</tr>
</thead>
<tbody>
{% for role in roles %}
<tr>
<td> {{ role.name }} </td>
<td> {{ role.description }} </td>
<td>
{% if role.employees %}
{{ role.employees.count() }}
{% else %}
0
{% endif %}
</td>
<td>
<a href="{{ url_for('admin.edit_role', id=role.id) }}">
<i class="fa fa-pencil"></i> Edit
</a>
</td>
<td>
<a href="{{ url_for('admin.delete_role', id=role.id) }}">
<i class="fa fa-trash"></i> Delete
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div style="text-align: center">
{% else %}
<div style="text-align: center">
<h3> No roles have been added. </h3>
<hr class="intro-divider">
{% endif %}
<a href="{{ url_for('admin.add_role') }}" class="btn btn-default btn-lg">
<i class="fa fa-plus"></i>
Add Role
</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
Just like we did for the departments, we have created a table where we will display all the roles with their name, description, and number of employees. Each role listed will also have an edit and delete link. If there are no roles, a message of the same will be displayed. There is also a button which can be clicked to add a new role.
<!-- app/templates/admin/roles/role.html -->
{% import "bootstrap/wtf.html" as wtf %}
{% extends "base.html" %}
{% block title %}
{% if add_department %}
Add Role
{% else %}
Edit Role
{% endif %}
{% endblock %}
{% block body %}
<div class="content-section">
<div class="outer">
<div class="middle">
<div class="inner">
<div class="center">
{% if add_role %}
<h1>Add Role</h1>
{% else %}
<h1>Edit Role</h1>
{% endif %}
<br/>
{{ wtf.quick_form(form) }}
</div>
</div>
</div>
</div>
</div>
{% endblock %}
We use the add_role
variable above the same way we used the add_department
variable for the department.html
template.
Once again, let's update the admin menu with the correct link:
<!-- app/templates/base.html -->
<!-- Modify nav bar menu -->
<li><a href="{{ url_for('admin.list_roles') }}">Roles</a></li>
Re-start the server. You should now be able to access the Roles page, and add, edit and delete roles.
Now to work on listing employees, as well as assigning them departments and roles.
We'll need a form to assign each employee a department and role. Add the following to the admin/forms.py
file:
# app/admin/forms.py
# update imports
from wtforms.ext.sqlalchemy.fields import QuerySelectField
from ..models import Department, Role
# existing code remains
class EmployeeAssignForm(FlaskForm):
"""
Form for admin to assign departments and roles to employees
"""
department = QuerySelectField(query_factory=lambda: Department.query.all(),
get_label="name")
role = QuerySelectField(query_factory=lambda: Role.query.all(),
get_label="name")
submit = SubmitField('Submit')
We have imported a new field type, QuerySelectField
, which we use for both the department and role fields. This will query the database for all departments and roles. The admin user will select one department and one role using the form on the front-end.
Add the following code to the admin/views.py
file:
# app/admin/views.py
# update imports
from forms import DepartmentForm, EmployeeAssignForm, RoleForm
from ..models import Department, Employee, Role
# existing code remains
# Employee Views
@admin.route('/employees')
@login_required
def list_employees():
"""
List all employees
"""
check_admin()
employees = Employee.query.all()
return render_template('admin/employees/employees.html',
employees=employees, title='Employees')
@admin.route('/employees/assign/<int:id>', methods=['GET', 'POST'])
@login_required
def assign_employee(id):
"""
Assign a department and a role to an employee
"""
check_admin()
employee = Employee.query.get_or_404(id)
# prevent admin from being assigned a department or role
if employee.is_admin:
abort(403)
form = EmployeeAssignForm(obj=employee)
if form.validate_on_submit():
employee.department = form.department.data
employee.role = form.role.data
db.session.add(employee)
db.session.commit()
flash('You have successfully assigned a department and role.')
# redirect to the roles page
return redirect(url_for('admin.list_employees'))
return render_template('admin/employees/employee.html',
employee=employee, form=form,
title='Assign Employee')
The list_employees
view queries the database for all employees and assigns them to the variable employees
, which we will use to list them in the template.
The assign_employee
view takes an employee ID. First, it checks whether the employee is an admin user; if it is, a 403 Forbidden
error is thrown. If not, it updates the employee.department
and employee.role
with the selected data from the form, essentially assigning the employee a new department and role.
Create a employees
directory in the templates/admin
directory. In it, create the employees.html
and employee.html
files:
<!-- app/templates/admin/employees/employees.html -->
{% import "bootstrap/utils.html" as utils %}
{% extends "base.html" %}
{% block title %}Employees{% endblock %}
{% block body %}
<div class="content-section">
<div class="outer">
<div class="middle">
<div class="inner">
<br/>
{{ utils.flashed_messages() }}
<br/>
<h1 style="text-align:center;">Employees</h1>
{% if employees %}
<hr class="intro-divider">
<div class="center">
<table class="table table-striped table-bordered">
<thead>
<tr>
<th width="15%"> Name </th>
<th width="30%"> Department </th>
<th width="30%"> Role </th>
<th width="15%"> Assign </th>
</tr>
</thead>
<tbody>
{% for employee in employees %}
{% if employee.is_admin %}
<tr style="background-color: #aec251; color: white;">
<td> <i class="fa fa-key"></i> Admin </td>
<td> N/A </td>
<td> N/A </td>
<td> N/A </td>
</tr>
{% else %}
<tr>
<td> {{ employee.first_name }} {{ employee.last_name }} </td>
<td>
{% if employee.department %}
{{ employee.department.name }}
{% else %}
-
{% endif %}
</td>
<td>
{% if employee.role %}
{{ employee.role.name }}
{% else %}
-
{% endif %}
</td>
<td>
<a href="{{ url_for('admin.assign_employee', id=employee.id) }}">
<i class="fa fa-user-plus"></i> Assign
</a>
</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}
The employees.html
template shows a table of all employees. The table shows their full name, department and role, or displays a -
in case no department and role has been assigned. Each employee has an assign link, which the admin user can click to assign them a department and role.
Because the admin user is an employee as well, they will be displayed in the table. However, we have formatted the table such that admin users stand out with a green background and white text.
<!-- app/templates/admin/employees/employee.html -->
{% import "bootstrap/wtf.html" as wtf %}
{% extends "base.html" %}
{% block title %}Assign Employee{% endblock %}
{% block body %}
<div class="content-section">
<div class="outer">
<div class="middle">
<div class="inner">
<div class="center">
<h1> Assign Departments and Roles </h1>
<br/>
<p>
Select a department and role to assign to
<span style="color: #aec251;">
{{ employee.first_name }} {{ employee.last_name }}
</span>
</p>
<br/>
{{ wtf.quick_form(form) }}
</div>
</div>
</div>
</div>
</div>
{% endblock %}
We need to update the admin menu once more:
<!-- app/templates/base.html -->
<!-- Modify nav bar menu -->
<li><a href="{{ url_for('admin.list_employees') }}">Employees</a></li>
Navigate to the Employees page now. If there are no users other than the admin, this is what you should see:
When there is an employee registered, this is displayed:
Feel free to add a variety of departments and roles so that you can start assigning them to employees.
You can re-assign departments and roles as well.
We now have a completely functional CRUD web app! In Part Two of the tutorial, we've been able to create an admin user and an admin dashboard, as well as customise the menu for different types of users. We've also built out the core functionality of the app, and can now add, list, edit, and delete departments and roles, as well as assign them to employees. We have also taken security into consideration by protecting certain views from unauthorized access.
Python Flask for Beginners: Build a CRUD Web App with Python and Flask - Part ThreeThis is the last part of a three-part tutorial to build an employee management web app, named Project Dream Team. In Part Two of the tutorial, we built out the CRUD functionality of the app.
We created forms, views, and templates to list, add, edit and delete departments and roles. By the end of Part Two, we could assign (and re-assign) departments and roles to employees.
In Part Three, we will cover:
Web applications make use of HTTP errors to let users know that something has gone wrong. Default error pages are usually quite plain, so we will create our own custom ones for the following common HTTP errors:
We'll start by writing the views for the custom error pages. In your app/__init__.py
file, add the following code:
# app/__init__.py
# update imports
from flask import Flask, render_template
# existing code remains
def create_app(config_name):
# existing code remains
@app.errorhandler(403)
def forbidden(error):
return render_template('errors/403.html', title='Forbidden'), 403
@app.errorhandler(404)
def page_not_found(error):
return render_template('errors/404.html', title='Page Not Found'), 404
@app.errorhandler(500)
def internal_server_error(error):
return render_template('errors/500.html', title='Server Error'), 500
return app
We make use of Flask's @app.errorhandler
decorator to define the error page views, where we pass in the status code as a parameter.
Next, we'll create the template files. Create a app/templates/errors
directory, and in it, create 403.html
, 404.html
, and 500.html
.
<!-- app/templates/errors/403.html -->
{% extends "base.html" %}
{% block title %}Forbidden{% endblock %}
{% block body %}
<div class="content-section">
<div class="outer">
<div class="middle">
<div class="inner">
<div style="text-align: center">
<h1> 403 Error </h1>
<h3> You do not have sufficient permissions to access this page. </h3>
<hr class="intro-divider">
<a href="{{ url_for('home.homepage') }}" class="btn btn-default btn-lg">
<i class="fa fa-home"></i>
Home
</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
<!-- app/templates/errors/404.html -->
{% extends "base.html" %}
{% block title %}Page Not Found{% endblock %}
{% block body %}
<div class="content-section">
<div class="outer">
<div class="middle">
<div class="inner">
<div style="text-align: center">
<h1> 404 Error </h1>
<h3> The page you're looking for doesn't exist. </h3>
<hr class="intro-divider">
<a href="{{ url_for('home.homepage') }}" class="btn btn-default btn-lg">
<i class="fa fa-home"></i>
Home
</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
<!-- app/templates/errors/500.html -->
{% extends "base.html" %}
{% block title %}Internal Server Error{% endblock %}
{% block body %}
<div class="content-section">
<div class="outer">
<div class="middle">
<div class="inner">
<div style="text-align: center">
<h1> 500 Error </h1>
<h3> The server encountered an internal error. That's all we know. </h3>
<hr class="intro-divider">
<a href="{{ url_for('home.homepage') }}" class="btn btn-default btn-lg">
<i class="fa fa-home"></i>
Home
</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
All the templates give a brief description of the error, and a button that links to the homepage.
Run the app and log in as a non-admin user, then attempt to access <a href="http://127.0.0.1:5000/admin/departments" target="_blank">http://127.0.0.1:5000/admin/departments</a>
. You should get the following page:
Now attempt to access this non-existent page: <a href="http://127.0.0.1:5000/nothinghere" target="_blank">http://127.0.0.1:5000/nothinghere</a>
. You should see:
To view the internal server error page, we'll create a temporary route where we'll use Flask's abort()
function to raise a 500 error. In the app/__init__.py
file, add the following:
# app/__init__.py
# update imports
from flask import abort, Flask, render_template
# existing code remains
def create_app(config_name):
# existing code remains
@app.route('/500')
def error():
abort(500)
return app
Go to <a href="http://127.0.0.1:5000/500" target="_blank">http://127.0.0.1:5000/500</a>
; you should see the following page:
Now you can remove the temporary route we just created for the internal server error.
Now, let's write some tests for the app. The importance of testing software can't be overstated. Tests help ensure that your app is working as expected, without the need for you to manually test all of your app's functionality.
We’ll begin by creating a test database, and give the database user we created in Part One all privileges on it:
$ mysql -u root
mysql> CREATE DATABASE dreamteam_test;
Query OK, 1 row affected (0.00 sec)
mysql> GRANT ALL PRIVILEGES ON dreamteam_test . * TO 'dt_admin'@'localhost';
Query OK, 0 rows affected (0.00 sec)
Now we need to edit the config.py
file to add configurations for testing. Delete the current contents and replace them with the following code:
# config.py
class Config(object):
"""
Common configurations
"""
DEBUG = True
class DevelopmentConfig(Config):
"""
Development configurations
"""
SQLALCHEMY_ECHO = True
class ProductionConfig(Config):
"""
Production configurations
"""
DEBUG = False
class TestingConfig(Config):
"""
Testing configurations
"""
TESTING = True
app_config = {
'development': DevelopmentConfig,
'production': ProductionConfig,
'testing': TestingConfig
}
We have put DEBUG = True
in the base class, Config
, so that it is the default setting. We override this in the ProductionConfig
class. In the TestingConfig
class, we set the TESTING
configuration variable to True
.
We will be writing unit tests. Unit tests are written to test small, individual, and fairly isolated units of code, such as functions. We will make use of Flask-Testing, an extension that provides unit testing utilities for Flask.
$ pip install Flask-Testing
Next, create a tests.py
file in the root directory of your app. In it, add the following code:
# tests.py
import unittest
from flask_testing import TestCase
from app import create_app, db
from app.models import Employee
class TestBase(TestCase):
def create_app(self):
# pass in test configurations
config_name = 'testing'
app = create_app(config_name)
app.config.update(
SQLALCHEMY_DATABASE_URI='mysql://dt_admin:[email protected]/dreamteam_test'
)
return app
def setUp(self):
"""
Will be called before every test
"""
db.create_all()
# create test admin user
admin = Employee(username="admin", password="admin2016", is_admin=True)
# create test non-admin user
employee = Employee(username="test_user", password="test2016")
# save users to database
db.session.add(admin)
db.session.add(employee)
db.session.commit()
def tearDown(self):
"""
Will be called after every test
"""
db.session.remove()
db.drop_all()
if __name__ == '__main__':
unittest.main()
In the base class above, TestBase
, we have a create_app
method, where we pass in the configurations for testing.
We also have two other methods: setUp
and tearDown
. The setUp
method will be called automatically before every test we run. In it, we create two test users, one admin and one non-admin, and save them to the database. The tearDown
method will be called automatically after every test. In it, we remove the database session and drop all database tables.
To run the tests, we will run the tests.py
file:
$ python tests.py
----------------------------------------------------------------------
Ran 0 tests in 0.000s
OK
The output above lets us know that our test setup is OK. Now let's write some tests.
# tests.py
# update imports
import os
from flask import abort, url_for
from app.models import Department, Employee, Role
# add the following after the TestBase class
class TestModels(TestBase):
def test_employee_model(self):
"""
Test number of records in Employee table
"""
self.assertEqual(Employee.query.count(), 2)
def test_department_model(self):
"""
Test number of records in Department table
"""
# create test department
department = Department(name="IT", description="The IT Department")
# save department to database
db.session.add(department)
db.session.commit()
self.assertEqual(Department.query.count(), 1)
def test_role_model(self):
"""
Test number of records in Role table
"""
# create test role
role = Role(name="CEO", description="Run the whole company")
# save role to database
db.session.add(role)
db.session.commit()
self.assertEqual(Role.query.count(), 1)
class TestViews(TestBase):
def test_homepage_view(self):
"""
Test that homepage is accessible without login
"""
response = self.client.get(url_for('home.homepage'))
self.assertEqual(response.status_code, 200)
def test_login_view(self):
"""
Test that login page is accessible without login
"""
response = self.client.get(url_for('auth.login'))
self.assertEqual(response.status_code, 200)
def test_logout_view(self):
"""
Test that logout link is inaccessible without login
and redirects to login page then to logout
"""
target_url = url_for('auth.logout')
redirect_url = url_for('auth.login', next=target_url)
response = self.client.get(target_url)
self.assertEqual(response.status_code, 302)
self.assertRedirects(response, redirect_url)
def test_dashboard_view(self):
"""
Test that dashboard is inaccessible without login
and redirects to login page then to dashboard
"""
target_url = url_for('home.dashboard')
redirect_url = url_for('auth.login', next=target_url)
response = self.client.get(target_url)
self.assertEqual(response.status_code, 302)
self.assertRedirects(response, redirect_url)
def test_admin_dashboard_view(self):
"""
Test that dashboard is inaccessible without login
and redirects to login page then to dashboard
"""
target_url = url_for('home.admin_dashboard')
redirect_url = url_for('auth.login', next=target_url)
response = self.client.get(target_url)
self.assertEqual(response.status_code, 302)
self.assertRedirects(response, redirect_url)
def test_departments_view(self):
"""
Test that departments page is inaccessible without login
and redirects to login page then to departments page
"""
target_url = url_for('admin.list_departments')
redirect_url = url_for('auth.login', next=target_url)
response = self.client.get(target_url)
self.assertEqual(response.status_code, 302)
self.assertRedirects(response, redirect_url)
def test_roles_view(self):
"""
Test that roles page is inaccessible without login
and redirects to login page then to roles page
"""
target_url = url_for('admin.list_roles')
redirect_url = url_for('auth.login', next=target_url)
response = self.client.get(target_url)
self.assertEqual(response.status_code, 302)
self.assertRedirects(response, redirect_url)
def test_employees_view(self):
"""
Test that employees page is inaccessible without login
and redirects to login page then to employees page
"""
target_url = url_for('admin.list_employees')
redirect_url = url_for('auth.login', next=target_url)
response = self.client.get(target_url)
self.assertEqual(response.status_code, 302)
self.assertRedirects(response, redirect_url)
class TestErrorPages(TestBase):
def test_403_forbidden(self):
# create route to abort the request with the 403 Error
@self.app.route('/403')
def forbidden_error():
abort(403)
response = self.client.get('/403')
self.assertEqual(response.status_code, 403)
self.assertTrue("403 Error" in response.data)
def test_404_not_found(self):
response = self.client.get('/nothinghere')
self.assertEqual(response.status_code, 404)
self.assertTrue("404 Error" in response.data)
def test_500_internal_server_error(self):
# create route to abort the request with the 500 Error
@self.app.route('/500')
def internal_server_error():
abort(500)
response = self.client.get('/500')
self.assertEqual(response.status_code, 500)
self.assertTrue("500 Error" in response.data)
if __name__ == '__main__':
unittest.main()
We've added three classes: TestModels
, TestViews
and TestErrorPages
.
The first class has methods to test that each of the models in the app are working as expected. This is done by querying the database to check that the correct number of records exist in each table.
The second class has methods that test the views in the app to ensure the expected status code is returned. For non-restricted views, such as the homepage and the login page, the 200 OK
code should be returned; this means that everything is OK and the request has succeeded. For restricted views that require authenticated access, a 302 Found
code is returned. This means that the page is redirected to an existing resource, in this case, the login page. We test both that the 302 Found
code is returned and that the page redirects to the login page.
The third class has methods to ensure that the error pages we created earlier are shown when the respective error occurs.
Note that each test method begins with test
. This is deliberate, because unittest
, the Python unit testing framework, uses the test
prefix to automatically identify test methods. Also note that we have not written tests for the front-end to ensure users can register and login, and to ensure administrators can create departments and roles and assign them to employees. This can be done using a tool like Selenium Webdriver; however this is outside the scope of this tutorial.
Run the tests again:
$ python tests.py
..............
----------------------------------------------------------------------
Ran 14 tests in 2.313s
OK
Success! The tests are passing.
Now for the final part of the tutorial: deployment. So far, we’ve been running the app locally. In this stage, we will publish the application on the internet so that other people can use it. We will use PythonAnywhere, a Platform as a Service (PaaS) that is easy to set up, secure, and scalable, not to mention free for basic accounts!
Create a free PythonAnywhere account here if you don’t already have one. Be sure to select your username carefully since the app will be accessible at your-username.pythonanywhere.com
.
Once you've signed up, your-username.pythonanywhere.com
should show this page:
We will use git to upload the app to PythonAnywhere. If you’ve been pushing your code to cloud repository management systems like Bitbucket, Gitlab or Github, that’s great! If not, now’s the time to do it. Remember that we won’t be pushing the instance
directory, so be sure to include it in your .gitignore
file, like so:
# .gitignore
*.pyc
instance/
Also, ensure that your requirements.txt
file is up to date using the pip freeze
command before pushing your code:
$ pip freeze > requirements.txt
Now, log in to your PythonAnywhere account. In your dashboard, there's a Consoles
tab; use it to start a new Bash console.
In the PythonAnywhere Bash console, clone your repository.
$ git clone https://github.com/andela-mnzomo/project-dream-team-three
Next we will create a virtualenv, then install the dependencies from the requirements.txt
file. Because PythonAnywhere installs virtualenvwrapper for all users by default, we can use its commands:
$ mkvirtualenv dream-team
$ cd project-dream-team-three
$ pip install -r requirements.txt
We've created a virtualenv called dream-team
. The virtualenv is automatically activated. We then entered the project directory and installed the dependencies.
Now, in the Web tab on your dashboard, create a new web app.
Select the Manual Configuration option (not the Flask option), and choose Python 2.7 as your Python version. Once the web app is created, its configurations will be loaded. Scroll down to the Virtualenv section, and enter the name of the virtualenv you just created:
Next, we will set up the MySQL production database. In the Databases tab of your PythonAnywhere dashboard, set a new password and then initialize a MySQL server:
The password above will be your database user password. Next, create a new database if you wish. PythonAnywhere already has a default database which you can use.
By default, the database user is your username, and has all privileges granted on any databases created. Now, we need to migrate the database and populate it with the tables. In a Bash console on PythonAnywhere, we will run the flask db upgrade
command, since we already have the migrations directory that we created locally. Before running the commands, ensure you are in your virtualenv as well as in the project directory.
$ export FLASK_CONFIG=production
$ export FLASK_APP=run.py
$ export SQLALCHEMY_DATABASE_URI='mysql://your-username:[email protected]/your-database-name'
$ flask db upgrade
When setting the SQLALCHEMY_DATABASE_URI
environment variable, remember to replace your-username
, your-password
, your-host-address
and your-database-name
with their correct values. The username, host address and database name can be found in the MySQL settings in the Databases tab on your dashboard. For example, using the information below, my database URI is: mysql://projectdreamteam:[email protected]onanywhere-services.com/projectdreamteam$dreamteam_db
Now we will edit the WSGI file, which PythonAnywhere uses to serve the app. Remember that we are not pushing the instance
directory to version control. We therefore need to configure the environment variables for production, which we will do in the WSGI file.
In the Code section of the Web tab on your dashboard, click on the link to the WSGI configuration file.
Delete all the current contents of the file, and replace them with the following:
import os
import sys
path = '/home/your-username/your-project-directory-name'
if path not in sys.path:
sys.path.append(path)
os.environ['FLASK_CONFIG'] = 'production'
os.environ['SECRET_KEY'] = 'p9Bv<3Eid9%$i01'
os.environ['SQLALCHEMY_DATABASE_URI'] = 'mysql://your-username:[email protected]/your-database-name'
from run import app as application
In the file above, we tell PythonAnywhere to get the variable app
from the run.py
file, and serve it as the application. We also set the FLASK_CONFIG
, SECRET_KEY
and SQLALCHEMY_DATABASE_URI
environment variables. Feel free to alter the secret key. Note that the path
variable should contain your username and project directory name, so be sure to replace it with the correct values. The same applies for the database URI environment variable.
We also need to edit our local app/__init__py
file to prevent it from loading the instance/config.py
file in production, as well as to load the configuration variables we've set:
# app/__init__.py
# update imports
import os
# existing code remains
def create_app(config_name):
if os.getenv('FLASK_CONFIG') == "production":
app = Flask(__name__)
app.config.update(
SECRET_KEY=os.getenv('SECRET_KEY'),
SQLALCHEMY_DATABASE_URI=os.getenv('SQLALCHEMY_DATABASE_URI')
)
else:
app = Flask(__name__, instance_relative_config=True)
app.config.from_object(app_config[config_name])
app.config.from_pyfile('config.py')
# existing code remains
Push your changes to version control, and pull them on the PythonAnywhere Bash console:
$ git pull origin master
Now let's try loading the app on PythonAnywhere. First, we need to reload the app on the Web tab in the dashboard:
Now go to your app URL:
Great, it works! Try registering a new user and logging in. This should work just as it did locally.
We will now create an admin user the same way we did locally. Open the Bash console, and run the following commands:
$ flask shell
>>> from app.models import Employee
>>> from app import db
>>> admin = Employee(email="[email protected]",username="admin",password="admin2016",is_admin=True)
>>> db.session.add(admin)
>>> db.session.commit()
Now you can login as an admin user and add departments and roles, and assign them to employees.
Congratulations on successfully deploying your first Flask CRUD web app! From setting up a MySQL database, to creating models, blueprints (with forms and views), templates, custom error pages, tests, and finally deploying the app on PythonAnywhere, you now have a strong foundation in web development with Flask. I hope this has been as fun and educational for you as it has for me! I'm looking forward to hearing about your experiences in the comments below.
In this Python Flask tutorial for beginners, we will see how to get started with Flask the Python Microframwork, its installation and a 'hello world' example and create awesome web apps!
Flask is a simple, easy-to-use microframework for Python that can help build scalable and secure web applications. Here are a few reasons why Flask is great for beginners:
In this tutorial, we'll cover the following:
By the end of this tutorial, we will have built a simple static website using Flask.
Ready? Let's dive in!
InstallationWe'll need the following installed for this tutorial:
You may already have Python installed on your system. You can check by running the python
command in your terminal. If it's installed, you should see the following output:
$ python
Python 2.7.10 (default, Oct 23 2015, 19:19:21)
[GCC 4.2.1 Compatible Apple LLVM 7.0.0 (clang-700.0.59.5)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>>
If you don't have it installed, you can download it here.
We'll start by installing virtualenv, a tool to create isolated Python environments. We need to use virtual environments to keep the dependencies used by different Python projects separate, and to keep our global site-packages directory clean. We'll go one step further and install virtualenvwrapper, a set of extensions that make using virtualenv a whole lot easier by providing simpler commands.
$ pip install virtualenv
$ pip install virtualenvwrapper
$ export WORKON_HOME=~/Envs
$ source /usr/local/bin/virtualenvwrapper.sh
To create and activate a virtualenv, run the following commands:
$ mkvirtualenv my-venv
$ workon my-venv
Told you the commands were simple! We now have a virtualenv called my-venv
, which we have activated and are currently working on. Now, any dependencies we install will be installed here and not globally. Remember to activate the virtualenv whenever you want to use or work on this project!
Next, let's create a directory for our app. This is where all our files will go:
$ mkdir my-project
$ cd my-project
Finally, let's install Flask:
$ pip install Flask
Installing Flask also installs a few other dependencies, which you will see when you run the following command:
$ pip freeze
click==6.6
Flask==0.11.1
itsdangerous==0.24
Jinja2==2.8
MarkupSafe==0.23
Werkzeug==0.11.11
What do all these packages do? Flask uses Click (Command Line Interface Creation Kit) for its command-line interface, which allows you to add custom shell commands for your app. ItsDangerous provides security when sending data using cryptographical signing. Jinja2 is a powerful template engine for Python, while MarkupSafe is a HTML string handling library. Werkzeug is a utility library for WSGI, a protocol that ensures web apps and web servers can communicate effectively.
You can save the output above in a file. This is good practice because anyone who wants to work on or run your project will need to know the dependencies to install. The following command will save the dependencies in a requirements.txt
file:
pip freeze > requirements.txt
Say "Hello World!" with Flask
I think any beginner programming tutorial would be remiss if it didn't start with the classic "Hello World!" So here's how to do this in Flask:
Create the following file, hello_world.py, in your favourite text editor (I'm an Atom girl, myself):
# hello_world.py
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello_world():
return 'Hello World!'
We begin by importing the Flask
class, and creating an instance of it. We use the __name__
argument to indicate the app's module or package, so that Flask knows where to find other files such as templates. Then we have a simple function that will display the string Hello World!
. The preceeding decorator simply tells Flask which path to display the result of the function. In this case, we have specified the route /
, which is the home URL.
Let's see this in action, shall we? In your terminal, run the following:
$ export FLASK_APP=hello_world.py
$ flask run
* Serving Flask app "hello_world"
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
The first command tells the system which app to run. The next one starts the server. Enter the specified URL (http://127.0.0.1:5000/
) in your browser. Voila! It works!
So far, we only have one functional file in our project: hello_world.py
. A real-world web project usually has more files than that. It's important to maintain a good directory structure, so as to organize the different components of the application separately. These are a few of the common directories in a Flask project:
/app
: This is a directory within my-project
. We'll put all our code in here, and leave other files, such as the requirements.txt
file, outside./app/templates
: This is where our HTML files will go./app/static
: This is where static files such as CSS and JavaScript files as well as images usually go. However, we won't be needing this folder for this tutorial since we won't be using any static files.$ mkdir app app/templates
Your project directory should now look like this:
├── my-project
├── app
│ ├── templates
├── hello_world.py
└── requirements.txt
The hello_world.py
seems a little out of place now, doesn't it? Don't worry, we'll fix that in the next section.
For the "Hello World!" example, we only had one file. To build our website, we'll need more files that serve various functions. Most Flask apps have the following basic file structure:
run.py
: This is the application's entry point. We'll run this file to start the Flask server and launch our application.config.py
: This file contains the configuration variables for your app, such as database details.app/__init__.py
: This file intializes a Python module. Without it, Python will not recognize the app
directory as a module.app/views.py
: This file contains all the routes for our application. This will tell Flask what to display on which path.app/models.py
: This is where the models are defined. A model is a representation of a database table in code. However, because we will not be using a database in this tutorial, we won't be needing this file.Some projects have more modules (for example, an app/views
directory with many views files in it), but this'll do for now. Go ahead and create these files, and delete hello_world.py
since we won't be needing it anymore:
$ touch run.py config.py
$ cd app
$ touch __init__.py views.py
$ rm hello_world.py
Here's our latest directory structure:
├── my-project
├── app
│ ├── __init__.py
│ ├── templates
│ └── views.py
├── config.py
├── requirements.txt
└── run.py
Now let's fill these empty files with some code!
ConfigurationThe config.py
file should contain one variable per line, like so:
# config.py
# Enable Flask's debugging features. Should be False in production
DEBUG = True
Note that this config file is very simplified and would not be appropriate for a more complex application. For bigger applications, you may choose to have different config.py
files for testing, development, and production, and put them in a config
directory, making use of classes and inheritance. You may have some variables that should not be publicly shared, such as passwords and secret keys. These can be put in an instance/config.py
file, which should not be pushed to version control.
Next, we have to initialize our app with all our configurations. This is done in the app/__init__.py
file. Note that if we set instance_relative_config
to True
, we can use app.config.from_object('config')
to load the config.py
file.
# app/__init__.py
from flask import Flask
# Initialize the app
app = Flask(__name__, instance_relative_config=True)
# Load the views
from app import views
# Load the config file
app.config.from_object('config')
Run, Flask, Run!
All we have to do now is configure our run.py
file so we can start the Flask server.
# run.py
from app import app
if __name__ == '__main__':
app.run()
To use the command flask run
like we did before, we would need to set the FLASK_APP
environment variable to run.py
, like so:
$ export FLASK_APP=run.py
$ flask run
We'll get a 404 page because we haven't written any views for our app. We'll be fixing that shortly.
ViewsFrom the "Hello World!" example, you already have an understanding of how views work. We use the @app.route
decorator to specify the path we'd like the view to be dispayed on. We've already seen how to write a view that returns a string. Let's see what else we can do with views.
# views.py
from flask import render_template
from app import app
@app.route('/')
def index():
return render_template("index.html")
@app.route('/about')
def about():
return render_template("about.html")
Flask provides a method, render_template
, which we can use to specifiy which HTML file should be loaded in a particular view. Of course, the index.html
and about.html
files don't exist yet, so Flask will give us a Template Not Found
error when we navigate to these paths. Go ahead; run the app and see:
Flask allows us to use a variety of template languages, but Jinja2 is by far the most popular one. Remember it from our installed dependencies? Jinja provides syntax that allows us to add some functionality to our HTML files, like if-else
blocks and for
loops, and also use variables inside our templates. Jinja also lets us implement template inheritance, which means we can have a base template that other templates inherit from. Cool, right?
Let's begin by creating the following three HTML files:
$ cd app/templates
$ touch base.html index.html about.html
We'll start with the base.html
file, using a slightly modified version of this example Bootstrap template:
<!-- base.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<title>{% block title %}{% endblock %}</title>
<!-- Bootstrap core CSS -->
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
<!-- Custom styles for this template -->
<link href="https://getbootstrap.com/examples/jumbotron-narrow/jumbotron-narrow.css" rel="stylesheet">
</head>
<body>
<div class="container">
<div class="header clearfix">
<nav>
<ul class="nav nav-pills pull-right">
<li role="presentation"><a href="/">Home</a></li>
<li role="presentation"><a href="/about">About</a></li>
<li role="presentation"><a href="http://flask.pocoo.org" target="_blank">More About Flask</a></li>
</ul>
</nav>
</div>
{% block body %}
{% endblock %}
<footer class="footer">
<p>© 2016 Your Name Here</p>
</footer>
</div> <!-- /container -->
</body>
</html>
Did you notice the {% block %}
and {% endblock %}
tags? We'll also use them in the templates that inherit from the base template:
<!-- index.html-->
{% extends "base.html" %}
{% block title %}Home{% endblock %}
{% block body %}
<div class="jumbotron">
<h1>Flask Is Awesome</h1>
<p class="lead">And I'm glad to be learning so much about it!</p>
</div>
{% endblock %}
<!-- about.html-->
{% extends "base.html" %}
{% block title %}About{% endblock %}
{% block body %}
<div class="jumbotron">
<h1>The About Page</h1>
<p class="lead">You can learn more about my website here.</p>
</div>
{% endblock %}
We use the {% extends %}
tag to inherit from the base template. We insert the dynamic content inside the {% block %}
tags. Everything else is loaded right from the base template, so we don't have to re-write things that are common to all pages, such as the navigation bar and the footer.
Let's refresh our browser and see what we have now:
ConclusionCongratulations for making it this far and getting your first Flask website up and running! I hope this introduction to Flask has whetted your appetite for exploring more. You now have a great foundation to start building more complex apps. Do have a look at the official documentation for more information.
Have you used other Python frameworks before? Where do you think Flask stands? Let's have a conversation in the comments below.
The code used in this tutorial is available for your reference on GitHub.
Python GUI Programming Projects using Tkinter and Python 3
Description
Learn Hands-On Python Programming By Creating Projects, GUIs and Graphics
Python is a dynamic modern object -oriented programming language
It is easy to learn and can be used to do a lot of things both big and small
Python is what is referred to as a high level language
Python is used in the industry for things like embedded software, web development, desktop applications, and even mobile apps!
SQL-Lite allows your applications to become even more powerful by storing, retrieving, and filtering through large data sets easily
If you want to learn to code, Python GUIs are the best way to start!
I designed this programming course to be easily understood by absolute beginners and young people. We start with basic Python programming concepts. Reinforce the same by developing Project and GUIs.
Why Python?
The Python coding language integrates well with other platforms – and runs on virtually all modern devices. If you’re new to coding, you can easily learn the basics in this fast and powerful coding environment. If you have experience with other computer languages, you’ll find Python simple and straightforward. This OSI-approved open-source language allows free use and distribution – even commercial distribution.
When and how do I start a career as a Python programmer?
In an independent third party survey, it has been revealed that the Python programming language is currently the most popular language for data scientists worldwide. This claim is substantiated by the Institute of Electrical and Electronic Engineers, which tracks programming languages by popularity. According to them, Python is the second most popular programming language this year for development on the web after Java.
Python Job Profiles
Software Engineer
Research Analyst
Data Analyst
Data Scientist
Software Developer
Python Salary
The median total pay for Python jobs in California, United States is $74,410, for a professional with one year of experience
Below are graphs depicting average Python salary by city
The first chart depicts average salary for a Python professional with one year of experience and the second chart depicts the average salaries by years of experience
Who Uses Python?
This course gives you a solid set of skills in one of today’s top programming languages. Today’s biggest companies (and smartest startups) use Python, including Google, Facebook, Instagram, Amazon, IBM, and NASA. Python is increasingly being used for scientific computations and data analysis
Take this course today and learn the skills you need to rub shoulders with today’s tech industry giants. Have fun, create and control intriguing and interactive Python GUIs, and enjoy a bright future! Best of Luck
Who is the target audience?
Anyone who wants to learn to code
For Complete Programming Beginners
For People New to Python
This course was designed for students with little to no programming experience
People interested in building Projects
Anyone looking to start with Python GUI development
Basic knowledge
Access to a computer
Download Python (FREE)
Should have an interest in programming
Interest in learning Python programming
Install Python 3.6 on your computer
What will you learn
Build Python Graphical User Interfaces(GUI) with Tkinter
Be able to use the in-built Python modules for their own projects
Use programming fundamentals to build a calculator
Use advanced Python concepts to code
Build Your GUI in Python programming
Use programming fundamentals to build a Project
Signup Login & Registration Programs
Quizzes
Assignments
Job Interview Preparation Questions
& Much More