24
loading...
This website collects cookies to deliver better user experience
mkdir blogger && cd blogger
mkdir app && cd app
touch app.py routes.py models.py
mkdir templates static
blogger
and create the following files
touch blog.py config.py
all is set and your project structure should be looking like this:-
blogger/
|-app/
|-templates #html files directory
|-static #css& images directory
|-__init__.py
|-routes.py #site navigation capability
|-models.py #db logic goes here
|-blog.py # main apprunning module
|-config.py
#tip: identation shows file location, eg; templates is in app dir
pip install virtualenv
#create a virtual environment
virtualenv bloggerenv
# you can name your env any name
# activating
# windows
cd bloggerenv/Scripts/activate
# linux
source bloggerenv/bin/activate
#deactivating the env
deactivate
(bloggerenv) $pip install Flask
# this are the underlying packages to start a project
#__init__.py
from flask import Flask
app = Flask(__name__) #flask object instance
from app import routes
routes
module, to be created laterfrom app import app
@app.route('/')
@app.route('/index')
def index():
return "Hello, World!"
@app.route
are called decorators, they create urls for the web-app.#blog.py
from app import app
if __name__ == '__main__:
app.run()
FLASK_APP
environment.(bloggerenv) $ export FLASK_APP=blog.py
set
instead of export
.(bloggerenv) $ flask run
* Serving Flask app 'blog.py' (lazy loading)
* Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: off
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
index.html
file and open it.app/
|-templates/
|-index.html
from flask import render_template
from app import app
@app.route('/')
@app.route('/index')
def index():
user = {'username' : 'Developer'}
return render_template('index.html', title='Home', user=user)
render_template
function from the flask
module. It handles html
rendering in pythonindex.html
file:-<!doctype html>
<html>
<head>
<title>{{ title }} - blogger</title>
</head>
<body>
<h1>Hello, {{ user.username }}!</h1>
</body>
</html>
{%..%}
index.html
that will print the documents title when provided in the render_template()
call and prints out a default title if None is provided.<head>
section:{% if title %}
<title>{{ title }}</title>
{% else %}
<title>App | blogger</title>
{% endif %}
routes.py
file for postsfrom flask import render_template
from app import app
@app.route('/')
@app.route('/index')
def index():
user = {'username' : 'Developer'}
posts =[
{
'author': {'username': 'Guido'},
'body': 'I designed python language'
},
{
'author': {'username': 'Jack'},
'body': 'Blue is a cool color'
}
]
return render_template('index.html', title='Homer', user=user, posts=posts)
posts
with nested dictionaries, where each element is a dictionary that has author
& body
fields.index.html
and handle the rendering of posts in the browser, posts can be of any number and we need to tell the temlate how to render them all, for that case we use a for
loop.<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ title }} - blogger</title>
</head>
<body>
{% if user.username %}
<p>Hello, {{ user.username }}</p>
{% else %}
<p>Hello, World!</p>
{% endif %}
<h2>My posts!</h2>
{% for post in posts %}
<b>{{ post.author.username }}</b>
<p>{{ post.body }}</p>
{% endfor %}
</body>
</html>
base.html
in the templates directory.
<!-- base.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
{% block title %}
<title>Document</title>
{% endblock %}
</head>
<body>
{% block content %}
{% endblock %}
</body>
</html>
index.html
file:-{% extends %}
tag, at the top of the child template.{% extends 'base.html' %}
{% block title %}
{% if user.username %}
<title>{{ title }}- blogger</title>
{% else %}
<p>Blogger</p>
{% endif %}
{% endblock %}
{% block content %}
<h2>My posts!</h2>
{% for post in posts %}
<b>{{ post.author.username }}</b>
<p>{{ post.body }}</p>
{% endfor %}
{% endblock %}
(bloggerenv) $ pip install flask-wtf
config.py
module in the top most directory.# config.py
import os
class Config(object):
SECRET_KEY = os.environ.get('SECRET_KEY') or 'you-will-never-guess'
SECRET_KEY
configuration variable is an important config in most flask apps, it is used by the Flask-WTF to protect web forms against the Cross Site Request Forgery(CSRF) attack.SECRET_KEY
. The second term, is just a hardcoded string. urandom
module>>>from os import urandom
>>>urandom(20)
b'\x1d\x00\x08\x8b \xd8\xae\xe9....
__init__.py
using the app.config.from_object()
method:.from flask import Flask
from config import Config
app = Flask(__name__)
app.config.from_object(Config)
from app import routes
app directory
create a file forms.py
#app/forms.py
from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField, SubmitField
from wtforms.validators import DataRequired
class PostForm(FlaskForm):
username = StringField('User name', validators=[DataRequired()])
post = TextAreaField('Post', validators=[DataRequired()])
submit = SubmitField('Submit')
validators
field is used to attach validation behaviors to form fields.
-The DataRequired
validator checks that a field is not submitted empty.LoginForm
class knows how to render itself as HTML, this makes the next part fairly simple.new_post.html
in app/template/new_post.html
N/B:For uniformity inherit the base.html
template inside the new template.
{% extends 'base.html' %}
{% block title %}
{% if title %}
<title>{{ title }}- blogger</title>
{% else %}
<p>Blogger</p>
{% endif %}
{% endblock %}
{% block content %}
<form action="" method="POST" novalidate>
{{ form.hidden_tag() }}
<p>
{{ form.username.label }} <br>
{{ form.username(size=32) }}
</p>
<p>
{{ form.post.label }} <br>
{{ form.post(size=120) }}
</p>
<p>
{{ form.submit() }}
</p>
</form>
{% endblock %}
PostForm
class to be given as an argument.Which is referenced as form
, the argument will be sent by the post
view function.<form>
element is used as the container for the web form.action
attribute tells the browser the url to use when the form data is submitted. when set to an empty string the form is submitted to the current URL in the browser.novalidate
tells the browser not to validate the forms as that is the work of Flask application running in the server.form.hidden_tag()
template argument generates a hidden field that includes a token that is used to protect the form against CSRF attacks.{{ form.<field_name>.label }}
replaces the label element, and the {{ form.<field_name>() }}
goes where the form field is needed.
Read more on the jinja2 documentation.#....
# route to the post form
@app.route('/post')
def post():
form = PostForm()
return render_template('new_post.html', title='Add post', form=form)
PostForm
class from forms.py, instantiated an object form = PostForm()
from it and sent the object down to the template form=form
.form=form
parses the form object to the template form, it is what's required to render the form fields.http://127.0.0.1:5000/post
a html form is displayed. But when you try to submit it a method not allowed
is thrown. I'll fix that in a minute.base.html
file.<!-- .... -->
<body>
<div>
<b>Blogger:</b>
<a href="{{ url_for('index') }}">Home</a>
<a href="{{ url_for('post') }}">Add Post</a>
</div>
<hr>
<!-- .... -->
body
tag, when the app is run we get a simple navbar at the top. That actually works 😉.url_for()
function from Flask. which generates URLs using iternal mapping of urls to view functions.Read ahead and practice working with forms.post
view function didn't understand what to do.methods=['POST', 'GET']
in the route decorator
.# route to the post form
from flask import Flask, redirect, flash
@app.route('/post', methods=['POST', 'GET'])
def post():
form = PostForm()
if form.validate_on_submit():
print(f"Hey, {form.username.data}! Your post '{form.post.data}' was successfully submitted! ")
return redirect('/index')
return render_template('new_post.html', title='Add post', form=form)
if
statement to validate the form
data in the view function.validated_on_submit()
runs validation of a form, Call validate only if the form is submitted. This is a shortcut for form.is_submitted() and form.validate().print()
function to print a custom message in the terminal, later I will be using the Flask flash()
function to flash messages in the web page.redirect
: this function redirects to a specified url.flash
: it flashes a message to the web page after an action has been carried. (blogger) $ pip install flask-sqlalchemy
Flask-migrate
.(blogger) $ pip install flask-migrate
config.py
module:import os
basedir = os.path.abspath(os.path.dirname(__file__))
class Config(object):
SECRET_KEY = os.environ.get('SECRET_KEY') or 'you-will-never-guess'
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'sqlite:///' + os.path.join(basedir, 'app.db')
SQLALCHEMY_TRACK_MODIFICATIONS = False
SQLALCHEMY_DATABASE_URI
configuration variable.SECRET_KEY
variable fallback, I also provide a fallback value if the config variable doesn't define the database url.SQLALCHEMY_TRACK_MODIFICATIONS
configuration if set to False
disables a Flask-SQLAlchemy
feature that sends a signal to the application every time a change is about to be made to the database. app/__init__.py
file.from flask import Flask
from config import Config
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
app = Flask(__name__)
app.config.from_object(Config) #configuration
db = SQLAlchemy(app) #database instance
migrate = Migrate(app, db)
from app import routes, models
models
, This module defines the structure of the database.app/models.py
from app import db
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(32), index=True, unique=True)
post = db.Column(db.String(120), index=True)
def __repr__(self):
return(f"<User>: {self.username}, <Post>: {self.post}")
id
field is usually in all models, and is used as the primary key. Each user is assigned a new id
and is stored in this field.username
& post
field are defined as strings(in database is known as VARCHAR
) and their maximum lengths specified.db.Column
class, whih takes field type as arguments.__repr__
tells python how to print objects of this class. It is useful for debugging.
Open python in a terminal try to assign some data to our class objects.
(blogger) $ python
>>> from app import db
>>> from app.models import Post
>>> p = Post(username="John", post="Hello, World!")
>>> p
<User>: John, <Post>: Hello, World!
>>>
To apply the migrations to a database, these migration scripts are executed in the sequence they were created.
The flask db
sub-command is added by flask-migrate to manage everything related to database migrations.
To create a migration repository :
(blogger) $ flask db init
Creating directory E:\code\projects\blogger\migrations ... done
Creating directory E:\code\projects\blogger\migrations\versions ... done
Generating E:\code\projects\blogger\migrations\alembic.ini ... done
Generating E:\code\projects\blogger\migrations\env.py ... done
Generating E:\code\projects\blogger\migrations\README ... done
Generating E:\code\projects\blogger\migrations\script.py.mako ... done
Please edit configuration/connection/logging settings in 'E:\\code\\projects\\blogger\\migrations\\alembic.ini' before proceeding.
flask
command relies on the FLASK_APP
environment variable, after a successful run a new migrations
directory is added.(blogger) $ flask db migrate -m 'posts table'
INFO [alembic.runtime.migration] Context impl SQLiteImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.autogenerate.compare] Detected added table 'post'
INFO [alembic.autogenerate.compare] Detected added index 'ix_post_post' on '['post']'
INFO [alembic.autogenerate.compare] Detected added index 'ix_post_username' on '['username']'
Generating E:\code\projects\blogger\migrations\versions\549b927398fe_posts_table.py ... done
flask
sub-command flask db upgrade
in the python shell context.$ flask db upgrade
INFO [alembic.runtime.migration] Context impl SQLiteImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.runtime.migration] Running upgrade -> 549b927398fe, posts table
db
changes are done in a database session context.db.session.add(p)
, p is the object created for the corresponding database table.db.session.commit()
.
(blogger) $ python
>>> from app import db
>>> from app.models import Post
>>> p = Post(username="John", post="Hello, World!")
>>> p
<User>: John, <Post>: Hello, World!
>>> db.session.add(p)
>>> db.session.commit()
>>> Post.query.all()
[<User>: Ariana, <Post>: Testing 2, <User>: John, <Post>: Hello, World!]
>>>
models
module.Post.query.all()
answers queries and returns all posts in the database./post
.app/routes.py
should look like this.# route to the post form
@app.route('/post', methods=['POST', 'GET'])
def post():
form = PostForm()
if form.validate_on_submit():
add = Post(username=form.username.data, post=form.post.data)
db.session.add(add)
db.session.commit()
flash(f"Hey, {form.username.data}! Your post '{form.post.data}' was successfully submitted! ")
return redirect('/index')
return render_template('new_post.html', title='Add post', form=form)
for
loop to loop through the available posts in the database.app/templates/all_posts.html
{% extends 'base.html' %}
{% block title %}
{% if title %}
<title>{{ title }}- blogger</title>
{% else %}
<p>Blogger</p>
{% endif %}
{% endblock %}
{% block content %}
<h2>Published Posts.</h2>
{% for post in posts %}
<b>{{ post.username }}</b>
<p>{{ post.post }}</p>
{% endfor %}
{% endblock %}
http://127.0.0.1:5000/view
all our posts are displayed in the web-page.base.html
file:<a href="{{ url_for('view') }}">Posts</a>
base
template file. Preview the app in the browser and a new a navbar link has been added.style.css
inside the app/static
directory
body {
background-color: #7fffd4;
}