Welcome to part 3 of my getting started guide. If you missed part 2 you can find it here.
Up until now we've been putting in our posts manually. This will get old fast so lets create a way to do this efficiently. Create an `admin` folder inside of `templates`. Inisde of that copy `templates/layout.html` to `templates/admin/layout.html`. Also create `templates/index.html` and put in the following code.
{% extends "admin/layout.html" %} {% block content %} <h1>Admin Section</h1> {% endblock %}
Here we will be using a different layout for our admin section so lets edit it a bit to make it more simple and obvious we're in a privledged section. Open up `templates/admin/layout.html` and modify it like below.
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous"> <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js" integrity="sha384-9/reFTGAW83EW2RDu2S0VKaIzap3H66lZH81PoYlFhbGU+6BZp6G7niu735Sk7lN" crossorigin="anonymous"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" integrity="sha384-B4gt1jrGC7Jh4AgTPSdUtOBvfO8shuf57BaghqFfPlYxofvL8/KUEfYiJOMMV+rV" crossorigin="anonymous"></script> <title>My Simple Blog</title> <style> .os { color: #000080; font-weight: bold; } .code { background-color: lightgrey; padding: 2px; } .navbar { background-color: #033a71!important; } </style> </head> <body> <nav class="navbar navbar-expand-md navbar-dark bg-dark mb-4"> <a class="navbar-brand" href="/">My Simple Blog</a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarCollapse"> <ul class="navbar-nav mr-auto"> <li class="nav-item"> <a class="nav-link" href="/admin">Admin</a> </li> </ul> <div class="md-md-0"> <ul class="navbar-nav mr-auto"> <li class="nav-item"> <a class="nav-link" href="/logout">Logout</a> </li> </ul> </div> </div> </nav> <div class="container"> <div class="row"> <div class="col-lg-12"> {% block content %} {% endblock %} </div> </div> </div> </body> </html>
Summary of changed
Now lets add some links that will take us to a place to edit our posts. In `app.py` add a `/admin` route like so.
@app.route('/admin') def route(): all_posts = Post.query.with_entities(Post.id, Post.name, Post.slug).order_by(Post.id.desc()).all() return render_template('admin/index.html', all_posts=all_posts)
And in `templates/admin/index.html` lets add our list of posts with links under our Admin tag along with some links for creating new blogs.
<h1>Admin Section</h1> <div class="row"> <div class="col-lg-9"> <h2>Blogs</h2> <ul> {% for post in all_posts %} <li><a href="/admin/blog/{{ post.id }}">{{ post.name }}</a></li> {% else %} <li><a href="/admin/blog/0">New blog</a></li> {% endfor %} </ul> </div> <div class="col-lg-3"> <h3>Blog Menu</h3> <a href="/admin/blog/0">New blog</a> </div> </div>
Of course if you follow those links now it will take you no where so lets create the route and templates. Create and open `templates/admin/blog.html` and put in the following.
{% extends "admin/layout.html" %} {% block head %} <script src="https://cdn.tiny.cloud/1/g90ylpumukvhzbhj0v83bur1hamikiaw7rdoufwbcjkdq70o/tinymce/5/tinymce.min.js" referrerpolicy="origin"></script> <script> tinymce.init({ selector: '#blogBody' }); </script> {% endblock %} {% block content %} <h1>Edit BLog</h1> <form method="POST" action="/admin/blog/{% if post.id %}{{ post.id }}{% else %}0{% endif %}" type="multipart/form-data"> <div class="form-group"> <label for="blogName">Blog Name</label> <input type="text" class="form-control" name="blogName" id="blogName" aria-describedby="blogName" placeholder="Enter blog name" value="{{ post.name }}"> <small id="blogName" class="form-text text-muted">This should be something fun to get the reader's attention.</small> </div> <div class="form-group"> <label for="blogSlug">Blog Slub</label> <input type="text" class="form-control"name="blogSlug" id="blogSlug" aria-describedby="blogSlug" placeholder="Enter blog slug" value="{{ post.slug }}"> <small id="blogSlug" class="form-text text-muted">This should be something easy to type.</small> </div> <div class="form-group"> <label for="blogBody">Example textarea</label> <textarea class="form-control"name="blogBody" id="blogBody" rows="10">{{ post.body }}</textarea> </div> <input type="hidden"name="id" value="{{ post.id }}" /> <button type="submit" class="btn btn-primary">Save</button> </form> {% endblock %}
Now for the routes, open `app.py` and place the following routes under our `about` route.
@app.route('/admin') def route(): all_posts = Post.query.with_entities(Post.id, Post.name, Post.slug).order_by(Post.id.desc()).all() return render_template('admin/index.html', all_posts=all_posts) @app.route('/admin/blog/<id>', methods=['get', 'post']) def admin_blog(id): if request.form: edit_post = Post.query.filter_by(id=id).first() if edit_post is None: edit_post = Post(name=request.form["blogName"], slug=request.form["blogSlug"], body=request.form["blogBody"]) db.session.add(edit_post) db.session.commit() return redirect('/admin/blog/' + str(edit_post.id)) else: edit_post.name = request.form["blogName"] edit_post.slug = request.form["blogSlug"] edit_post.body = request.form["blogBody"] db.session.add(edit_post) db.session.commit() return render_template("admin/blog.html", post=edit_post) post = Post.query.filter_by(id=id).first() return render_template('admin/blog.html', post=post)
Don't forget to add `request` and `redirect` to the `from Flask import` statement on top.
At this point we have a functional bare bones blog platform. We can add a link to the admin section on the main layout in `templates/layout.html` so we can access the admin section. Just under the main nav links, add a new div like this.
<div class="md-md-0"> <ul class="navbar-nav mr-auto"> <li class="nav-item"> <a class="nav-link" href="/admin">Admin</a> </li> </ul> </div>
But now we have a problem. Anybody can access our admin and post something! Lets fix that but adding authentication!
The first thing we will need to do is install `flask-login` via pip.
Windows
# (venv) C:\Users\aipiggy\Documents\blog> pip install flask-login
Then we need to modify `app.py` to give our application some clues about how to use our new login ability. Put the imports below at the top of the file and above all of our routes add the `login_manager` bits and below the `/` route add the route for login. Don't forget to protect the admin routes too!
from flask_login import login_required, login_user, logout_user, current_user, LoginManager, UserMixin from werkzeug.security import generate_password_hash, check_password_hash ... login_manager = LoginManager() login_manager.login_view = '/login' login_manager.init_app(app) ... @app.route('/admin') @login_required def admin(): .... @app.route('/admin/blog/<id>', methods=['get', 'post']) @login_required def admin_blog(id): ... @app.route('/login', methods=['get','post']) def login(): if request.form: user = User.query.filter_by(email=request.form["email"]).first() if not user or not check_password_hash(user.password, request.form["password"]): return 'Please check your login details and try again.' else: login_user(user, remember=request.form["remember"]) return redirect('/admin') return render_template('auth/login.html') @app.route('/logout') def logout(): logout_user() return redirect('/')
We also need to edit our User class to include password and email and add our user_loader.
@login_manager.user_loader def load_user(user_id): # since the user_id is just the primary key of our user table, use it in the query for the user return User.query.get(int(user_id)) class User(UserMixin, db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(128)) email = db.Column(db.String(100), unique=True) password = db.Column(db.String(100))
Lets migrate our database changes to make sure everything is OK so far.
Windows
# (venv) C:\Users\aipiggy\Documents\blog> flask db migrate -m "Added user login" # (venv) C:\Users\aipiggy\Documents\blog> flask db upgrade
Since we created our user data before the password and email was a part of it we will have to add them manually in the command line.
Windows
# (venv) C:\Users\aipiggy\Documents\blog> python # >>> from werkzeug.security import generate_password_hash # >>> from app import User, db # >>> user = User.query.filter_by(id=1).first() # if your database's user id is not 1, substitute it here # >>> user.email = 'test@email.com' # >>> user.password = generate_password_hash('password') # >>> db.session.add(user) # >>> db.session.commit()
We can test this in the command prompt to make sure it worked
Windows
# >>> from werkzeug.security import check_password_hash # >>> check_password_hash(user.password, 'password') True # >>>
Exit out of the python prompt and start flask again then lets continue. We will build our log in form next. Create and open `templates/auth/login.html`
{% extends "layout.html" %} {% block content %} <h1>Login</h1> <form method="POST" action="/login" type="multipart/form-data"> <div class="form-group"> <label for="email">Email</label> <input type="text" class="form-control" name="email" id="email" aria-describedby="email" placeholder="Email"> </div> <div class="form-group"> <label for="password">Password</label> <input type="password" class="form-control" name="password" id="password" placeholder="Password"> </div> <div class="form-group"> <label for="rememberme">Remember Me</label> <input type="checkbox" id="rememberme" name="rememberme" value="rememberme" /> </div> <button type="submit" class="btn btn-primary">Login</button> </form> {% endblock %}
After saving this go to http://127.0.0.1:5000/login and try to log in with the credentials you saved above. On a successful attempt you should be taken to the main admin page.
The error message that is shown off site is not very pretty so lets fix this by using a function called `flash()`. This is a method of sending a message to the front end that will only last for one page load. This is useful for letting the user know something was updated or their password is wrong. In `app.py` add the following
from flask import Flask, ..., flash ... @app.route('/login', methods=['get','post']) def login(): if request.form: user = User.query.filter_by(email=request.form["email"]).first() if not user or not check_password_hash(user.password, request.form["password"]): flash('Please check your login details and try again.') return redirect('/login') else: login_user(user) return redirect('/admin') return render_template('auth/login.html')
Inside `templates/auth/login.html` add this above the form tag.
{% with messages = get_flashed_messages() %} {% if messages %} <div class="alert alert-warning" role="alert"> {{ messages[0] }} </div> {% endif %} {% endwith %}
Now we get a nice message in site that lets us know something is wrong if we try to use incorrect credentials. The same is easy to do on the blog edit form. Try it out.
We still don't know for sure if some one has logged in or not so lets add their name to the layout templates and hide the login link if they are already logged in. Lets start with the admin layout `templates/admin/layout.html` and add a <li> to display the name.
... <div class="md-md-0"> <ul class="navbar-nav mr-auto"> <li class="nav-item"> <span class="nav-link">Hi {{ current_user.name }}!</span> </li> <li class="nav-item"> <a class="nav-link" href="/logout">Logout</a> </li> </ul> </div> ...
The changes to `templates/layout.html` will include a check to see if the user is logged in or not to show a login or logout link.
... <div class="md-md-0"> <ul class="navbar-nav mr-auto"> {% if current_user.is_authenticated %} <li class="nav-item"> <span class="nav-link">Hi {{ current_user.name }}!</span> </li> <li class="nav-item"> <a class="nav-link" href="/logout">Logout</a> </li> {% else %} <li class="nav-item"> <a class="nav-link" href="/login">Login</a> </li> {% endif %} <li class="nav-item"> <a class="nav-link" href="/admin">Admin</a> </li> </ul> </div> ...
That's it for this post. You have a functional blog platform that has basic authentication, a basic editor, and a way to show the posts. Next time we will add a bit more to the Post model to display and save the user that created the post.
There is no registration page at this time as I have plans for a future post on user creation with captcha protection in the future.
In our final installment we will set up Apache and WSGI to make our app run in a production environment.