In our previous Python tutorial, we have developed Online Web Chat using Flask and Python. In this tutorial, we will build a Discussion Forum system with Python, Flask & MySQL.
A forum system is a program which allows member to hold discussions online. The discussion is started by a member by posting a topic and other members reply on that topic. This allows members to share information and ideas.
So let’s proceed with developing Discussion Forum System Project:
1. Project Setup and Module Installation
First we will create our discussion forum project discussion-forum-python-flask-mysql using below command.
$ mkdir discussion-forum-python-flask-mysql
and moved to the project.
$ cd discussion-forum-python-flask-mysql
Then we will install required modules for our application. As we will develop a web based application, so we will install Flask micro framework module to create web application.
$ pip install Flask
As we will use MySQL database, so we will also install flask_mysqldb module. This is Python package that can be used to connect to MySQL database. We will install it using the below command:
pip install flask_mysqldb
2. Initialize Application
We will create project file app.py and import required modules. We will create flask instance and also configure MySQL database connection.
from flask import Flask, render_template, request, redirect, url_for, session, jsonify from flask_mysqldb import MySQL import MySQLdb.cursors from datetime import date import re import os import sys import hashlib app = Flask(__name__) app.secret_key = 'abcd21234455' app.config['MYSQL_HOST'] = 'localhost' app.config['MYSQL_USER'] = 'root' app.config['MYSQL_PASSWORD'] = '' app.config['MYSQL_DB'] = 'forums' mysql = MySQL(app)
3. Implement User Login
We will implement user login functionality by creating a function login()
in app.py
. We will display login form if user not yet login otherise implement login and redirect to category listing page.
@app.route('/login', methods =['GET', 'POST']) def login(): mesage = '' if request.method == 'POST' and 'email' in request.form and 'password' in request.form: email = request.form['email'] #password = hashlib.md5((request.form['password']).encode('utf-8')) password = hashlib.md5((request.form['password']).encode('ISO-8859-1 ')).hexdigest() cursor = mysql.connection.cursor(MySQLdb.cursors.DictCursor) cursor.execute('SELECT * FROM forum_users WHERE email = % s AND password = % s', (email, password, )) user = cursor.fetchone() if user: session['loggedin'] = True session['userid'] = user['user_id'] session['name'] = user['name'] session['email'] = user['email'] session['role'] = user['usergroup'] mesage = 'Logged in successfully !' return redirect(url_for('category')) else: mesage = 'Please enter correct email / password !' return render_template('login.html', mesage = mesage)
We will create template file login.html
for login page.
{% include 'header.html' %} <body> <div class="container-fluid" id="main"> {% include 'top_menus.html' %} <div class="row row-offcanvas row-offcanvas-left"> <div class="col-md-9 col-lg-10 main"> <h2>User Login</h2> <form action="{{ url_for('login') }}" method="post"> {% if mesage is defined and mesage %} <div class="alert alert-warning">{{ mesage }}</div> {% endif %} <div class="form-group"> <label for="email">Email:</label> <input type="email" class="form-control" id="email" name="email" placeholder="Enter email" name="email"> </div> <div class="form-group"> <label for="pwd">Password:</label> <input type="password" class="form-control" id="password" name="password" placeholder="Enter password" name="pswd"> </div> <button type="submit" class="btn btn-primary">Login</button> </form> <hr> </div> </div> </div> </body> </html>
4. List Category and Topics
We will create function category()
to list categories and also topics from categories.
@app.route("/category", methods =['GET', 'POST']) def category(): if request.args.get('category_id'): cursor = mysql.connection.cursor(MySQLdb.cursors.DictCursor) cursor.execute('SELECT c.name, t.category_id, t.subject, t.topic_id, t.user_id, t.created, count(p.post_id) AS total_post FROM forum_topics as t LEFT JOIN forum_posts as p ON t.topic_id = p.topic_id LEFT JOIN forum_category as c ON t.category_id = c.category_id WHERE t.category_id = %s GROUP BY t.topic_id ORDER BY t.topic_id DESC', (request.args.get('category_id'),)) topics = cursor.fetchall() cursor.execute('SELECT category_id, name FROM forum_category WHERE category_id = %s ', (request.args.get('category_id'),)) category = cursor.fetchone() return render_template("category.html", topics = topics, category = category, request=request) else: cursor = mysql.connection.cursor(MySQLdb.cursors.DictCursor) cursor.execute('SELECT category.category_id, category.name, category.description, count(topic.category_id) AS total_topic FROM forum_category category LEFT JOIN forum_topics topic ON category.category_id = topic.category_id GROUP BY category.category_id ORDER BY category_id DESC') categories = cursor.fetchall() return render_template("category.html", categories = categories, request=request)
We will create template file category.html
to list categories and topics.
{% include 'header.html' %} <body> <div class="container"> <div class="row"> <h1>Discussion Forum</h1><br> {% include 'top_menus.html' %} {% if request.args.get('category_id') %} <div class="single category"> <span style="font-size:20px;"><a href="{{ url_for('category') }}"><< {{category.name}}</a></span> <br> <br> <ul class="list-unstyled"> <li class="text-right"> <a type="button" class="btn btn-primary" href="{{url_for('compose', category_id=category.category_id)}}"><span style="font-size:20px;font-weight:bold;color:white;">Create New Topic</span></a> </li><br> <li><span style="font-size:20px;font-weight:bold;">Topics</span> <span class="pull-right"><span style="font-size:15px;font-weight:bold;">Posts</span></span></li> {% for topic in topics %} <li><a href="{{url_for('post', topic_id=topic.topic_id)}}" title="">{{topic.subject}} <span class="pull-right">{{topic.total_post}}</span></a></li> {% endfor %} </ul> </div> {% else %} <div class="single category"> <ul class="list-unstyled"> <li><span style="font-size:25px;font-weight:bold;">Categories</span> <span class="pull-right"><span style="font-size:20px;font-weight:bold;">Topics</span></span></li> {% for category in categories %} <li><a href="{{url_for('category', category_id=category.category_id)}}">{{category.name}}<span class="pull-right">{{category.total_topic}}</span></a></li> {% endfor %} </ul> </div> {% endif %} </div> </div> </body> </html>
5. Compose Topics
We will create function compose()
to create a new topic.
@app.route("/compose", methods =['GET', 'POST']) def compose(): if 'loggedin' in session: if request.args.get('category_id'): cursor = mysql.connection.cursor(MySQLdb.cursors.DictCursor) cursor.execute('SELECT category_id, name FROM forum_category WHERE category_id = %s ', (request.args.get('category_id'),)) category = cursor.fetchone() return render_template("compose.html", category = category) return redirect(url_for('login'))
we will create template file comose.html
to create a new topic.
{% include 'header.html' %} <body> <div class="container"> <div class="row"> <h1>Discussion Forum</h1><br> {% include 'top_menus.html' %} <br> <span style="font-size:20px;"><a href="{{ url_for('category') }}"><< {{category.name}}</a></span> <br><br> <div id="createNewtopic"> <form id="topicForm" name="topicForm" method="post" action="{{ url_for('save_topic')}}"> <div class="form-group"> <label for="email">Topic Name:</label> <input type="text" name="topicName" id="topicName" class="form-control"> </div> <div class="form-group"> <label for="email">Message:</label> <textarea class="form-control" name="message" id="message" rows="5"></textarea> </div> <input type="hidden" name="action" value="createTopic"> <input type="hidden" name="categoryId" value="{{category.category_id}}"> <button type="submit" id="saveTopic" name="saveTopic" class="btn btn-info">Create Topic</button> </form> </div> </div> </div> </body> </html>
6. List Posts
We will create function post()
to list posts.
@app.route("/post", methods =['GET', 'POST']) def post(): cursor = mysql.connection.cursor(MySQLdb.cursors.DictCursor) if request.args.get('topic_id'): cursor.execute('SELECT topic_id, subject, category_id FROM forum_topics WHERE topic_id = %s ', (request.args.get('topic_id'),)) topic = cursor.fetchone() cursor.execute('SELECT p.post_id, p.message, p.topic_id, p.user_id, p.name, p.created, u.name AS username FROM forum_posts p LEFT JOIN forum_users u ON u.user_id = p.user_id WHERE p.topic_id = %s ', (request.args.get('topic_id'),)) posts = cursor.fetchall() return render_template("post.html", topic = topic, posts = posts)
We will create a template file post.html
to list posts.
{% include 'header.html' %} <body> <div class="container"> <div class="row"> <h1>Discussion Forum</h1><br> {% include 'top_menus.html' %} <br> <div id="postLsit"> <div class="posts list"> <span style="font-size:20px;"><a href="{{url_for('category', category_id=topic.category_id)}}"><< {{topic.subject}}</a></span> <br><br> {% for post in posts %} <article class="row" id="postRow_{{post.post_id}}"> <div class="col-md-2 col-sm-2 hidden-xs"> <figure class="thumbnail"> <img class="img-responsive" src="{{url_for('static', filename='images/user-avatar.png')}}" /> <figcaption class="text-center">{{post.username}}</figcaption> </figure> </div> <div class="col-md-10 col-sm-10"> <div class="panel panel-default arrow left"> <div class="panel-body"> <header class="text-left"> <div class="comment-user"><i class="fa fa-user"></i> By: {{post.username}}</div> <time class="comment-date" datetime="16-12-2014 01:05"><i class="fa fa-clock-o"></i> {{post.created}}</time> </header> <br> <div class="comment-post" id="post_message_{{post.post_id}}"> {{post.message}} </div> <textarea name="message" data-topic-id="{{post.topic_id}}" id="{{post.post_id}}" style="visibility: hidden;"></textarea><br> <div class="text-right"> <a href="{{url_for('edit_post', post_id=post.post_id)}}" class="btn btn-default btn-sm"><i class="fa fa-reply"></i> Edit</a> <a class="btn btn-default btn-sm"><i class="fa fa-reply"></i> Delete</a> </div> <div id="editSection_{{post.post_id}}" class="hidden"> <button type="submit" id="save_{{post.post_id}}" name="save" class="btn btn-info saveButton">Save</button> <button type="submit" id="cancel_{{post.post_id}}" name="cancel" class="btn btn-info saveButton">Cancel</button> </div> </div> </div> </div> </article> {% endfor %} </div> </div> </div> </div> </body> </html>
7. Implement Post Reply
We will create form with create post in a topic with action save_post
to save post.
{% if session['userid'] %} <form id="posts" name="posts" method="post" action="{{ url_for('save_post')}}"> <textarea name="message" id="message" class="form-control" rows="3" placeholder="Write here..."></textarea><br> <input type="hidden" name="action" id="action" value="save" /> <input type="hidden" name="topic_id" value="{{topic.topic_id}}"> <button type="submit" id="save" name="save" class="btn btn-info saveButton">Post</button> </form> {% else %} <a href="{{ url_for('login') }}">Login to reply</a> {% endif %}
We will create function save_post()
to implement to save post.
@app.route("/save_post", methods =['GET', 'POST']) def save_post(): if 'loggedin' in session: cursor = mysql.connection.cursor(MySQLdb.cursors.DictCursor) if request.method == 'POST' and 'topic_id' in request.form and 'message' in request.form: topicId = request.form['topic_id'] message = request.form['message'] cursor.execute('INSERT INTO forum_posts (`message`, `topic_id`, `user_id`) VALUES (%s, %s, %s)', (message, topicId, session['userid'])) mysql.connection.commit() return redirect(url_for('post', topic_id = topicId)) return redirect(url_for('login'))