43 KiB
Flask_from_zero_to_docker
Pre-requirements
Create a Python virtual enviroement
You can create a virtual enviroment using the venv command that comes with Python. this will create a virtual enviroment in the specified .venv directory. I like to make it hidden so that it doesn't show up during developpment.
python3 -m venv .venv
Activate the virtual enviroement
Then you need to activate the virtual enviroment.
- for Bash
source .venv/bin/activate
- for Fish
. .venv/bin/activate.fish
If executed corectly your (.venv) should apear on your terminal
(.venv) key@devbian ~/g/m/flask_to_docker >
Install libraries and make them exportable
Once this is done you can check the installed libraries using pip.
pip3 freeze
This should return empty as we didn't installed anything yet. So let's proceed and install Flaks
pip3 install flask
Let's chech once again with pip freeze.
pip3 freeze
This time you shuld get a list of installed libraries.
(.venv) key@devbian ~/g/m/flask_to_docker > pip3 freeze
blinker==1.8.2
click==8.1.7
Flask==3.0.3
itsdangerous==2.2.0
Jinja2==3.1.4
MarkupSafe==2.1.5
Werkzeug==3.0.3
You can also expoirt the required libraries to a text file.
pip3 freeze > requirements.txt
This file can afterward be usted to automatically install all the libraries used on this porject.
pip install -r requirements.txt
This make it very easy to add .venv to your git ingnore list and for someone colning your project to easely install pip libraries after cloning your porject.
Default application YouTube
The main funtion to start a Flask Server
This is slef explanatory but each Flask application start the same way. Let's create an app.py and start with the following steps.
- Import Flask
- We declare the app()
- Add decoration for routes @app.route('/')
- Define a function under the decoration index():
- Run the app wiht app.run()
from flask import Flask # Importing flask
app = Flask(__name__)
@app.route('/')
def index():
return "Hello World"
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=True)
In the run() funtion there are some main parameters that needs to be defined these correspond to:
-
host (str* | *None) – the hostname to listen on. Set this to 0.0.0.0 to have the server available externally as well. Defaults to 127.0.0.1 (AKA localhost) or the host in the SERVER_NAME config variable if present.
-
port (int* | *None) – the port of the webserver. Defaults to 5000 or the port defined in the SERVER_NAME config variable if present.
-
debug (bool* | *None) if given, enable or disable debug mode. See debug.
First Run
The firsat run is simple enought. Just execute your app.py. Just make sure that your venv is active.
(.venv) key@devbian ~/g/m/flask_to_docker > python3 app.py
you should be greeted wiht :
* Serving Flask app 'app'
* Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://192.168.1.128:5000
Press CTRL+C to quit
* Restarting with stat
* Debugger is active!
* Debugger PIN: 633-588-052
If this is tha case open http://localhost:5000/ on wour browser and you should be greeted wiht your hello wolrd message.
Congratulations you have a running Flask Application.
Flask URL's & Route YouTube
These are Some code snippets wiht examples please do not forget tha the start annd the end is the same as the Default Application described just before.
Main Route
Hereyou can see as before the main route (/)
@app.route('/')
def index():
return "Welcome"
URL proccessors
We can also use url proccessors to get something that we have typed on the url. In this case the url to visit would be http://localhost:5000/greet/kerem
@app.route('/greet/<name>')
def greet(name):
return f"Hello {name}"
Variables given to the url can also be typed here as integers. This allows us to do some advanced operations. In this case the url to visit would be http://localhost:5000/add/20/10
@app.route('/add/<int:number1>/<int:number2>')
def add(number1, number2):
return f'{number1} + {number2} = {number1+number2}'
Responses
Now to do generate some responses. Here you can use curl so see exactly what is happening
#We can also return some responses (like 404 page not found and co)
@app.route('/hello')
def hello():
response = make_response('Hello World')
response.status_code = 202
response.headers['content-type'] = 'application/octet-stream'
return response
So if we were to use curl we would gete here the following response.http://127.0.0.1:5000/hello is the same as http://localhost:5000/hello
key@devbian ~> curl -I http://127.0.0.1:5000/hello
HTTP/1.1 202 ACCEPTED
Server: Werkzeug/3.0.3 Python/3.11.2
Date: Wed, 26 Jun 2024 07:06:06 GMT
content-type: application/octet-stream
Content-Length: 11
Connection: close
Methods
This is the way to allow or block some Methods.
@app.route('/test_methodes', methods=['GET', 'POST'])
def test_methodes():
if request.method == 'GET':
return f"You made a GET request\n"
if request.method == 'POST':
return f"You made a POST request\n"
else:
return f"You will never see this message\n"
Parameters
You have surely seen some Parametes embedded in liks like ?user=bgfdrt56+pass=Rtrssdf. Flask alows some asy maniputation of those parameters. To test this : http://localhost:5000/handle_url_params?greeting=hello&name=kerem. If there was a mistmatch you would get the Error message instead.
#Handle parameters :
@app.route('/handle_url_params')
def handle_url_params():
if 'greeting' in request.args.keys() and 'name' in request.args.keys():
greeting = request.args['greeting']
name = request.args.get('name')
return f'{greeting}, {name}'
else:
return f'some params are missing'
Flask HTML files And TEMPLATES YouTube
Declare that we use Templates
To indicate that we will use templates first we need to create a directory containing those tmeplates and second we need to reference it in our code. let's create the directory
key@devbian ~/g/m/flask_to_docker > mkdir templates
key@devbian ~/g/m/flask_to_docker > cd templates
key@devbian ~/g/m/flask_to_docker > touch index.html
The sturcture should look like this. We can use Tree to find this out
key@devbian ~/g/m/flask_to_docker > tree
.
├── app.py
└── templates
└── index.html
Let's declare it in our code. template_folder take the source of app.py as starting route for directories.
app = Flask(__name__, template_folder='templates')
Using Templates
The render_template()
let's use use tem when returning the content of the page.
@app.route('/')
def index():
myval = 'Kynsight'
myres = 30
mylist = [10,20,30,40,50]
return render_template('index.html', value=myval, result=myres, list=mylist)
Now this allows us to have a huge flexibility that HTML doesn't otherwise allow. This is thnaks to Jinja. Let's see how it works.
HTML & Jinja
Now let's have a look to index.html
herre we can now se some Jinja templates funtionlaities.
<!DOCTYPE html>
<html lang="en">
<head charset="UTF-8">
<title>Flask APP</title>
</head>
<body>
<h1>My First Heading</h1>
<p>My value: {{ value }}</p>
<p>My result: {{ result }}</p>
<ul>
{% for item in list %}
{% if item == 30 %}
<li style="color: red">{{ item }}</li>
{% else %}
<li {%if item == 20 %} style="color: blue" {%endif%} >{{ item }}</li>
{% endif %}
{% endfor %}
</ul>
</body>
</html>
Jinja Allows us to use these kind of functions : {% for item in list %}
that are ususaly inpossible wiht HTML.
All Jinja Functions starts with {%
and ends with %}
.
- They can also be used inline like:
<li {%if item == 20 %} style="color: blue" {%endif%} >{{ item }}</li>
.
Extend Templates
We cna extend templates to other templates. What does this bring us ? Let's say we want ta have the same header on multiple pages we can use a template with the header and only add an content BLOCK onto this template.
In templeates directory we can now create a base html file let's call it base.htm and two pages that extends on it index.html and other.html.
key@devbian ~/g/m/flask_to_docker > cd templates
key@devbian ~/g/m/flask_to_docker > touch base.html other.html
the structure should look like this
key@devbian ~/g/m/flask_to_docker > tree
.
├── app.py
└── templates
├── base.html
├── index.html
└── other.html
base.html
<!DOCTYPE html>
<html lang="en">
<head charset="UTF-8">
<title>Flask APP</title>
</head>
<body>
<h1>My First Heading</h1>
<p>My value: {{ value }}</p>
<p>My result: {{ result }}</p>
<ul>
{% for item in list %}
{% if item == 30 %}
<li style="color: red">{{ item }}</li>
{% else %}
<li {%if item == 20 %} style="color: blue" {%endif%} >{{ item }}</li>
{% endif %}
{% endfor %}
</ul>
</body>
</html>
index.html Here you can see that we are extending frim base.html with {% extends "base.html" %}
.
- we can indicate a title using the
block
that we calle title. - we can fill the content using the
block
that we calle content.
{% extends "base.html" %}
{% block title %}Index Page{% endblock %}
{% block content %}
<h1>My First Heading</h1>
<p>My value: {{ value }}</p>
<p>My result: {{ result }}</p>
<ul>
{% for item in list %}
{% if item == 30 %}
<li style="color: red">{{ item }}</li>
{% else %}
<li {%if item == 20 %} style="color: blue" {%endif%} >{{ item }}</li>
{% endif %}
{% endfor %}
</ul>
{% endblock %}
other.html all the same rules applies here too.
{% extends "base.html" %}
{% block title %}Other Page{% endblock %}
{% block content %}
<h1>Other</h1>
{% endblock %}
but here we need to add a route to this page at app.py
@app.route('/other')
def other():
return render_template('other.html')
Jinja Filters
Filters that python uses lin .uppercase
can't be used in Jinja. There are some filters taht already exists but most importnatly we can create our own filters.
Here is an filter that alredy exitst which mase everything uppercase. But every filter needs to be piped trhpug Jinja thus the|
.
<p> {{ sometext|upper }}</p>
Let's create a exmaple file for filters. Create filter.html in templates, and add a custom Filter namde reverse_string
filter.html:
{% extends "base.html" %}
{% block title %}Filter Page{% endblock %}
{% block content %}
<h1>Filter</h1>
<p> {{ sometext }}</p>
<p> {{ sometext|upper }}</p>
<p> {{ sometext|lower }}</p>
<p> {{ sometext|replace('l', 'L') }}</p>
<p> {{ sometext|reverse_string }}</p>
{% endblock %}
We of course need to define reverse_string
in app.py as follows :
@app.template_filter('reverse_string')
def reverse_string(s):
return s[::-1]
you can see that we use a speciel decorator named template_filter to declare our custom filters.
Redirection
Redirections can be tricky but thankfully Flask has a way od dynamic redirection wich takes a function as argument
@app.route('/redirect_endpoint')
def redirect_endpoint():
return redirect(url_for('other'))
the other in return redirect(url_for('other'))
used above, is redirection to the def other():
defined under V:
@app.route('/wewwa')
def other():
return render_template('other.html')
this mean when we type http://localhost:5000/redirect_endpoint we will be redirected to http://localhost:5000/wewwa with the page defined at other.html
GET & POST
To have some fields is much better ! Here is an example how to handle some GET & POST Methods
app.py
@app.route('/', methods=['GET', 'POST'])
def index():
if request.method == 'GET':
return render_template('index.html')
elif request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
if username == 'kynsight' and password == 'pass':
return 'Success'
else:
return 'Failure'
inted.html
{% extends "base.html" %}
{% block title %}Index Page{% endblock %}
{% block content %}
<h1>Hellooo</h1>
<form method="POST" action="{{ url_for('index') }}">
<input type="text" name="username" placeholder="Username"><br>
<input type="text" name="password" placeholder="Password"><br>
<input type="submit" value="login">
</form>
{% endblock %}
So what is going on here ?
- First we check in
index():
function withif request.method == 'xxx':
What Method is being used. - If the Methode is GET we just print the page wiht the Form using
return render_template('index.html')
- If the Methode is POST we collect the information filler in form sent by pressing submit
- We get collect the information by referencing form items by their name attribute :
request.form.get('username')
- Then we can put these infiormation in varibales and use them as we please.
Uploading & downloading files
Be careful here are some notions that are not related to Flask or Jinja. Like Pandas for excel manipulation and os of opperating system functions. Nevertheless this is a very important topic with widely used applications for website.
Uploading a file
Here is the File Upload form located in index.html the most important element to know are :
- enctype="multipart/form-data" Used while uploading files
- accept="media-types" do define which types of files are allowed.
<h1>File Upload</h1>
<form method="POST" action="{{ url_for('file_upload') }}" enctype="multipart/form-data">
<input type="file" name="file" accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel, text/plain" ><br>
<input type="submit" value="upload File">
</form>
Wiht the action="{{ url_for('file_upload') }}"
we go back to our app.py funtion def file_upload(): where we treat this information. Where we do nothing else then to read the file and print his output.
- If it's a Text file we put directly it's output
- If it's a Excel file we read the file using pandas
df = pd.read_excel(file)
and return the result as html table usingreturn df.to_html()
import pandas as pd
@app.route('/file_upload', methods=['GET', 'POST'])
def file_upload():
file = request.files.get('file')
if file.content_type == 'text/plain':
return file.read().decode()
elif file.content_type == 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' or file.content_type == 'application/vnd.ms-excel':
df = pd.read_excel(file)
return df.to_html()
Download a file directly
Here is the File Upload form located in index.html tnothing mroe to know then the previous example:
<h1>Convert To CSV</h1>
<form method="POST" action="{{ url_for('convert_csv') }}" enctype="multipart/form-data">
<input type="file" name="file" accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel" ><br>
<input type="submit" value="upload File">
</form>
Wiht the action="{{ url_for('convert_csv') }}"
we go back to our app.py funtion def convert_csv(): where we convert the excel into csv but also directly donwload the ouput. It is a specific example and we don't need to waste too much time on it as there is, in my opignon a much better way to do this.
@app.route('/convert_csv', methods=['GET', 'POST'])
def convert_csv():
file = request.files.get('file')
df = pd.read_excel(file)
response = Response(
df.to_csv(),
mimetype = 'text/csv',
headers={'Content-Disposition': 'attachement; filename=result.csv'}
)
return response
Download a file thourgh a directory
Again nothing special to note here, same a the ones before.
<h1>Convert To CSV Two</h1>
<form method="POST" action="{{ url_for('convert_csv_two') }}" enctype="multipart/form-data">
<input type="file" name="file" accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel" ><br>
<input type="submit" value="upload File">
</form>
Wiht the action="{{ url_for('convert_csv_two') }}"
we go back to our app.py funtion def convert_csv_two():, convert the file to csv and make it redy for download:
os.makedirs('downloads')
we create a donwload directoryfilename = f'{uuid.uuid4()}.cvs'
we give the file a unique namedf.to_csv(os.path.join('downloads', filename))
we convert the file into csv and register in downloads with hisfilenamereturn render_template('download.html', filename=filename)
we return the download.html page with filename as argument
@app.route('/convert_csv_two', methods=['GET', 'POST'])
def convert_csv_two():
file = request.files.get('file')
df = pd.read_excel(file)
if not os.path.exists('downloads'):
os.makedirs('downloads')
filename = f'{uuid.uuid4()}.cvs'
df.to_csv(os.path.join('downloads', filename))
return render_template('download.html', filename=filename)
for that to work we need to create the download.html in templates directory:
href="{{ url_for('download', filename=filename) }}"
Directs the request todef download(filename):
function which generates the download proccess
{% extends "base.html" %}
{% block title %}Download Page{% endblock %}
{% block content %}
<h1>Download {{ filename }}</h1>
<a href="{{ url_for('download', filename=filename) }}"> Donwload </a>
{% endblock %}
and aso create a new route for download in app.py:
@app.route('/download/<filename>'
the route has de name route to /download but allso the argumentsend_from_directory('downloads', filename, download_name='result.csv')
alows us to send the file downloads directory and rename it at the same time
@app.route('/download/<filename>', methods=['GET', 'POST'])
def download(filename):
return send_from_directory('downloads', filename, download_name='result.csv')
JS/json Request for live update
todo if needed. for now i am nto so shure.
Static Files & Integrating Bootstrap YouTube
To declare static file location (previously created), it just must be added to the creator of the Flask app. This way we can give / as a root directory for each static file Ex: /css/style.css. Flaks now know that he needs to search under */static/css/style.css this is practical for the future if we dise to change the ridectory of the static file.
app = Flask(__name__, template_folder='templates', static_folder='static', static_url_path='/')
Then we cna implement them on our pages as follows -> for Example : bootsrap in our base.html file :
<link rel="stylesheet" type="text/css" href="/css/bootstrap.css"/>
To add bootstrap css<script src="/js/bootstrap.js">
To add bootstrap JavaScript
<!DOCTYPE html>
<html lang="en">
<head charset="UTF-8">
<title>{% block title %}Flask APP {% endblock %}</title>
<link rel="stylesheet" type="text/css" href="/css/style.css"/>
<link rel="stylesheet" type="text/css" href="/css/bootstrap.css"/>
</head>
<body>
<h1>This is from the base ;)</h1>
{% block content %} {% endblock %}
<script src="/js/bootstrap.js">
</body>
</html>
which gives us a file structure like so :
key@devbian ~/g/m/flask_to_docker > tree
.
├── app.py
├── README.md
├── static
│ ├── css
│ │ ├── bootstrap.css
│ │ .
│ │ .
│ │ ├── bootstrap-utilities.rtl.min.css.map
│ │ └── style.css
│ ├── img
│ │ └── logo.png
│ └── js
│ ├── bootstrap.js
│ .
│ .
│ └── hello.js
└── templates
├── base.html
└── index.html
As you can see we also have implmented our custom javascript hello.js and this can also be implmented on inuque pages like so :
window.onload = function()
{
setTimeout(function(){alert('Bye Bye')}, 5000);
};
We can now use it on our index.html like so:
<script src="/js/hello.js"></script>
{% extends "base.html" %}
{% block title %}Index Page{% endblock %}
{% block content %}
<h1>Hello Wolrd</h1>
<p class="special">Hello</p>
<a href="#" class="btn btn-primary">Button Text</a>
<img src="/img/logo.png" alt="Text"/>
<script src="/js/hello.js"></script>
{% endblock %}
Session and Cookies YouTube
Sessions and cookies are a very important part of a website. in shot
- Cookie are on the client side and can be accessed by the user !
- session on the other hand are stored on the server side and are very good to keep sensitive information that the user should not be anble to see or modify.
So let'a jump in to our app.py and let me show you how where we create, read, and delete cookies and sessions
- inludes and intitaltion
from flask import session
app.secret_key = 'SOME KEY'
- create a session and implment some variables called keys and render index.html with a message
@app.route('/set_data')
def set_data():
session['name'] = 'Session1'
session['other'] = 'HelloWorld'
return render_template('index.html', message='Session data set.')
- First we check if the key exist with
if 'name' in session.keys()
- Then we can put them into into varibale, and generate index.html with a message inculde those variables
@app.route('/get_data')
def get_data():
if 'name' in session.keys() and 'other' in session.keys():
name = session['name']
other = session['other']
return render_template('index.html', message=f'Name: {name} Content: {other}.')
else:
return render_template('index.html', message='Session has no data set.')
- here is how to clear a session.
@app.route('/clear_session')
def clear_session():
session.clear()
return render_template('index.html', message='Session has been cleared')
- creating a cookie requires a response command as it is stored on the client side side
- First we create a response containig the index.html to render.
- and add a cookie to this response.
@app.route('/set_cookie')
def set_cookie():
response = make_response(render_template('index.html', message='Cookie Set'))
response.set_cookie('cookie_name', 'cookie_value')
return response
- To get the cookies content we need a request, again as it is stored on de client side.
@app.route('/get_cookie')
def get_cookie():
cookie_value = request.cookies['cookie_name']
return render_template('index.html', message=f'Cookie value: {cookie_value}')
- To dekete the cookies content we a requires a response, again this time with
response.set_cookie('cookie_name', expires=0)
expires=0 indicating that the cookie has expired.
@app.route('/remove_cookie')
def remove_cookie():
response = make_response(render_template('index.html', message='Cookie removed'))
response.set_cookie('cookie_name', expires=0)
return response
Message Flashing YouTube
This can be made without using javascript, so it is cool ! flash module form flask allows us to implment flashing messages that jinja can intreprate. This can be very usefull to implment when we need to track the flow of opperation on a website. Here is an example how to implement it on our app.py, index.html, base.html and showing it by creating a small login.html
- The route to be created at app.py dont forget to inlcude flash from FLask:
- With the 'GET' indicates that we are oppneing the page so we generate login.html
- Afterwards whne we put our infomration an send a 'POST request trough login.html we check if the data is correct an flash a message accordingly. Ex:
flash('Successfull Login!')
.
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'GET':
return render_template('login.html')
elif request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
if username == 'kynsight' and password == 'pass':
flash('Successfull Login!')
return render_template('index.html', message="")
else:
flash('Login Failed!')
return render_template('index.html', message="")
- content of login.html
- with
<input type="submit" value="login">
we cna see that we point back todef login():
.
{% extends "base.html" %}
{% block title %}login Page{% endblock %}
{% block content %}
<h1>Login</h1>
<form action="{{ url_for('login') }}" method='POST'>
<input type="text" name="username" placeholder="user name">
<input type="text" name="password" placeholder="password">
<input type="submit" value="login">
</form>
{% endblock %}
- when we flash the message we want it to be seen on each page so we include it on base.html on the
<body>
- Please be careful and note that the
get_flashed_messages()
function is not placed in the{% block content %} {% endblock %}
<body>
<h1>This is from the base ;)</h1>
{% with messages = get_flashed_messages() %}
{% if messages %}
<ul>
{% for message in messages %}
<li>{{ message }}</li>
{% endfor%}
</ul>
{% endif %}
{% endwith %}
{% block content %} {% endblock %}
<script src="/js/bootstrap.js">
</body>
Flask SQLalchemy YouTube
Here we need to restructure all of our code due to what is called Circular Imports in Python.
Have a look here : we have created a main folder dbapp which now contains all the required files to run our application. On top we have now app.py alongside run.py and routes.py thesse are now crutial to propoerly run our application. models.py will be explained further down.
key@devbian ~/g/m/f/.o/7 (master)> tree -L 3
.
└── dbapp
├── app.py
├── models.py
├── routes.py
├── run.py
├── static
│ ├── css
│ ├── img
│ └── js
└── templates
├── base.html
├── index.html
└── login.html
let's start with run,py. here we now only import flask create the app and then run it in __main__. So in the future we will have to call python3 run.py
to run our application
create_app()
will be defined in app.py we will come to that later.
from app import create_app
flask_app = create_app()
if __name__ == '__main__':
flask_app.run(host='0.0.0.0', debug='Ture')
Now for routes.py here we change the routes definition so that we can call it with def register_routes(app, db):
durin initiation at app.py. We also need to pass the database db as we will implement it just afterwards.
from flask import Flask, render_template, request
from models import Person
def register_routes(app, db):
@app.route('/', methods=['GET','POST'])
def index():
return "Hello World"
Last but not least app.py here comes everything together:
- we declare our main imports which doesn't cause any circular import issues
- we create our db with
db = SQLAlchemy()
create_app()
will is defined.db.init_app(app)
link our db to our app.- Here is one way to avoid Circular Imports :
from routes import register_routes
we import our routes.py in the funtiondef create_app():
register_routes(app, db)
we create our routesMigrate(app, db)
we migrate our database
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
db = SQLAlchemy()
def create_app():
app = Flask(__name__, template_folder='templates', static_folder='static', static_url_path='/')
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///./test.db'
db.init_app(app)
from routes import register_routes
register_routes(app, db)
Migrate(app, db)
return app
How to create and update/upgrade the database.
Let's stard by definin some tables in our models.py
class Person(db.Model):
Our person class is a Database Model__tablename__ = 'person'
will be the table namedef __repr__(self):
Will format the default return wehn we do a query
from app import db
class Person(db.Model):
__tablename__ = 'person'
pid = db.Column(db.Integer, primary_key=True)
name = db.Column(db.Text, nullable=False)
age = db.Column(db.Integer)
job = db.Column(db.Text)
def __repr__(self):
return f'Person with name: {self.name} and age {self.age}'
Now we can initiate -> migrate -> upgrade our database Be careful!! each time we change a table in models.py we then need to migrate -> upgrade our database once more.
(.venv) key@devbian ~/g/m/f/authapp (master)> flask db init
Creating directory '/home/key/git/minibase/flask_to_docker/authapp/migrations' ... done
Creating directory '/home/key/git/minibase/flask_to_docker/authapp/migrations/versions' ... done
Generating /home/key/git/minibase/flask_to_docker/authapp/migrations/README ... done
Generating /home/key/git/minibase/flask_to_docker/authapp/migrations/alembic.ini ... done
Generating /home/key/git/minibase/flask_to_docker/authapp/migrations/script.py.mako ... done
Generating /home/key/git/minibase/flask_to_docker/authapp/migrations/env.py ... done
Please edit configuration/connection/logging settings in '/home/key/git/minibase/flask_to_docker/authapp/migrations/alembic.ini'
before proceeding.
(.venv) key@devbian ~/g/m/f/authapp (master)> flask db migrate
INFO [alembic.runtime.migration] Context impl SQLiteImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.autogenerate.compare] Detected added table 'person'
Generating /home/key/git/minibase/flask_to_docker/authapp/migrations/versions/0b1a6cc67556_.py ... done
(.venv) key@devbian ~/g/m/f/authapp (master)> 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 -> 0b1a6cc67556, empty message
These commands will then create some new directories and detect the tables created in models.py.
key@devbian ~/g/m/f/.o/7 (master)> tree -L 3
.
└── dbapp
├── app.py
├── instance
│ └── test.db
├── migrations
│ ├── alembic.ini
│ ├── env.py
│ ├── __pycache__
│ ├── README
│ ├── script.py.mako
│ └── versions
├── models.py
├── __pycache__
│ ├── app.cpython-311.pyc
│ ├── models.cpython-311.pyc
│ └── routes.cpython-311.pyc
├── routes.py
├── run.py
├── static
│ ├── css
│ ├── img
│ └── js
└── templates
├── base.html
├── index.html
└── login.html
Creating User Login function (Implmentation of the database)YouTube
Now let's implement on top of what we did before, a login manager.
- models.py
- We import UserMixin to add some aditiona properties to our users like
get_id(self):
from app import db
from flask_login import UserMixin
class User(db.Model, UserMixin):
__tablename__ = 'users'
uid = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String, nullable=False)
password = db.Column(db.String, nullable=False)
role = db.Column(db.String, nullable=True)
description = db.Column(db.String, nullable=True)
def __repr__(self):
return f'<User:{self.username}, {self.role}>'
def get_id(self):
return self.uid
from flask import Flask, render_template, request
from models import Person
def register_routes(app, db):
@app.route('/', methods=['GET','POST'])
def index():
if request.method == 'GET':
people = Person.query.all()
return render_template('index.html', people=people)
elif request.method == 'POST':
name = request.form.get('name')
age = int(request.form.get('age'))
job = request.form.get('job')
person = Person(name=name, age=age, job=job)
db.session.add(person)
db.session.commit()
people = Person.query.all()
return render_template('index.html', people=people)
@app.route('/delete/<pid>', methods=['DELETE'])
def delete(pid):
Person.query.filter(Person.pid==pid).delete()
people = Person.query.all()
db.session.commit()
people = Person.query.all()
return render_template('index.html', people=people)
Now let's go spte by step through routes.py
- te required imports
from flask import Flask, render_template, request, redirect, url_for
from models import User
from flask_login import login_user, logout_user, current_user, login_required
- sign up: we get the information from the from in signup.html $
- Hash the password to safely save it in our DB with
hashed_pass = brcypt.generate_password_hash(password)
- then create a user object with
user = User(username=username, password=hashed_pass)
- add it to the database
db.session.add(user)
- commit (save) the database
db.session.commit()
@app.route('/signup', methods = ['GET','POST'])
def signup():
if request.method == 'GET':
return render_template('signup.html')
elif request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
hashed_pass = brcypt.generate_password_hash(password)
user = User(username=username, password=hashed_pass)
db.session.add(user)
db.session.commit()
return redirect(url_for('index'))
- Login is very similar to Signup only that we compare the given password to the hashed one registered on the DB
login_user(user)
Is where the flask_login come into play. now we have a user loged our session and it is tracked.
@app.route('/login', methods = ['GET','POST'])
def login():
if request.method == 'GET':
return render_template('login.html')
elif request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
user = User.query.filter(User.username == username).first()
if brcypt.check_password_hash(user.password, password):
login_user(user)
return redirect(url_for('index'))
else:
return f"failed"
- as the user is tracked we can also easyly log him out with
logout_user()
. you can see that we don't have to pass any argument to it.
@app.route('/logout')
def logout():
logout_user()
return redirect(url_for('index'))
- now hpw dp we do if we want to limit the acces to a page to only logged mebers?
@login_required
as decorator would be enought !
@app.route('/secret')
@login_required
def secret():
return "my SercreT"
Finishing it with app.py is also involved wiht the declaration of the login session and callbacks if needed
from flask import Flask, redirect, url_for
from flask_login import LoginManager
from flask_bcrypt import Bcrypt
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
- now nothing changed much from the last chapter exept :
@login_manager.user_loader
Telling at flask_login how to handle an user log in.@login_manager.unauthorized_handler
ot define a custom action when unauthorized access is generated. etherwise we get the default message.
db = SQLAlchemy()
def create_app():
app = Flask(__name__, template_folder='templates', static_folder='static', static_url_path='/')
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///./test.db'
app.secret_key = 'SOME KEY'
db.init_app(app)
login_manager = LoginManager()
login_manager.init_app(app)
from models import User
@login_manager.user_loader
def load_user(uid):
return User.query.get(uid)
@login_manager.unauthorized_handler
def unauthorized_callback():
return redirect(url_for('index'))
bcrypt = Bcrypt(app)
from routes import register_routes
register_routes(app, db, bcrypt)
Migrate(app, db)
return app
Blueprints YouTube
This is more of a restructuration of the whole project. let's first have a look into the file structure. as you can see bluprints tend to repeate themselves and can be used are modular build oject in the future.
key@devbian ~/g/m/flask_to_docker (master)> tree
.
├── blueprintapp
│ ├── app.py
│ ├── blueprints
│ │ ├── core
│ │ │ ├── __init__.py
│ │ │ ├── routes.py
│ │ │ └── templates
│ │ │ └── core
│ │ │ └── index.html
│ │ ├── people
│ │ │ ├── __init__.py
│ │ │ ├── models.py
│ │ │ ├── routes.py
│ │ │ └── templates
│ │ │ └── people
│ │ │ ├── create.html
│ │ │ └── index.html
│ │ └── todos
│ │ ├── __init__.py
│ │ ├── models.py
│ │ ├── routes.py
│ │ └── templates
│ │ └── todos
│ │ ├── create.html
│ │ └── index.html
│ ├── __init__.py
│ └── templates
│ └── base.html
└── run.py
let's have alook how to declare the core
Blueprint by goin to the corresponding blueprintapp/blueprints/core/routes.py:
Blueprint('core', __name__, template_folder='templates')
is wehre we declare the blueprint- The given
template_folder='templates'
corenponds to : blueprintapp/blueprints/core/templates/ - We also have new decorations now have a llok at
@core.route
instead of the classica @app.route
from flask import Flask, render_template, Blueprint
core = Blueprint('core', __name__, template_folder='templates')
@core.route('/', methods=['GET', 'POST'])
def index():
return render_template('core/index.html')
now how to inlcude Bluprint to the main app:
- we firs timport the blueprint
from blueprintapp.blueprints.core.routes import core
- and then we regiter them with
app.register_blueprint(core, url_prefix='/')
the url_prefix='/' is the prefix that is given on the Internet Browser. - so wehn we give http://localhost:5000/ we are pointing to blueprintapp/blueprints/core/templates/core/index.html
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
db = SQLAlchemy()
def create_app():
app = Flask(__name__, template_folder='templates', static_folder='static', static_url_path='/')
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///./test.db'
app.secret_key = 'SOME KEY'
db.init_app(app)
# import and register all blueprints.
from blueprintapp.blueprints.core.routes import core
from blueprintapp.blueprints.todos.routes import todos
from blueprintapp.blueprints.people.routes import people
app.register_blueprint(core, url_prefix='/')
app.register_blueprint(todos, url_prefix='/todos')
app.register_blueprint(people, url_prefix='/people')
Migrate(app, db)
return app