In this tutorial, we’ll see how we can get the overall feeling of our users after they might have read our post and added their comments. We’ll build a simple blog where users can comment. Then we process the comment to determine the percentages of people that find the post interesting and those who don't.
As technologies are advancing, the way we process data is also taking a huge turn around. Taking advantage of natural language processing, we can determine from a group of comments, how our users feel about our blog post.
We also don’t have to reload a page to see a new comment from a blog post. We can make comments visible in realtime to every user.
We’ll be using Channels, Vue.js and Flask to build the app.
Here is a preview of what the final app will look like:
This tutorial uses the following:
You should have some familiarity with Python development to follow along with this tutorial. If you are not familiar with Vue but still want to follow along, you can go through the basics of Vue in the documentation to get you up to speed in a couple of minutes.
Before we start, let’s get your environment ready. Check that you have the appropriate installation and setup on your machine.
Open up a terminal on your machine and execute the below code:
$ python --version
If you have a Python 3.6+ installed on your machine, you will have a similar text printed out as python 3.6.0
. If you got an output similar to “Command not found”, you need to install Python on your machine. Head over to Python’s official website to download and get it installed.
If you have gotten all that installed, let's proceed.
We'll use Pusher Channels to handle all realtime functionalities. Before we can start using Pusher Channels, we need to get our API key. We need an account to be able to get the API key.
Head over to Pusher and log in to your account or create a new account if you don’t have one already. Once you are logged in, create a new app and then copy the app API keys.
Let’s create our backend app that will be responsible for handling all communication to Pusher Channels and getting the sentiment of a comment.
Create the following files and folder in a folder named live-comment-sentiment
in any convenient location on your system:
1live-comment-sentiment 2 ├── .env 3 ├── .flaskenv 4 ├── app.py 5 ├── requirements.txt 6 ├── static 7 │ ├── custom.js 8 │ └── style.css 9 └── templates 10 └── index.html 11 └── base.html
It’s a good idea to have an isolated environment when working with Python. virtualenv is a tool to create an isolated Python environment. It creates a folder which contains all the necessary executables to use the packages that a Python project would need.
From your command line, change your directory to the Flask project root folder, execute the below command:
$ python3 -m venv env
Or:
$ python -m venv env
The command to use depends on which associates with your Python 3 installation.
Then, activate the virtual environment:
$ source env/bin/activate
If you are using Windows, activate the virtualenv with the below command:
> \path\to\env\Scripts\activate
This is meant to be a full path to the activate script. Replace \path\to
with your correct path name.
Next, add the Flask configuration setting to the .flaskenv
file:
1FLASK_APP=app.py 2 FLASK_ENV=development
This will instruct Flask to use app.py
as the main entry file and start up the project in development mode.
Now, add your Pusher API keys to the .env
file:
1PUSHER_APP_ID=app_id 2 PUSHER_APP_KEY=key 3 PUSHER_APP_SECRET=secret 4 PUSHER_APP_CLUSTER=cluster
Make sure to replace app_id
, key
, secret
and cluster
with your own Pusher keys which you have noted down earlier.
Next, create a Flask instance by adding the below code to app.py
:
1# app.py 2 3 from flask import Flask, jsonify, render_template, request 4 from textblob import TextBlob 5 import pusher 6 import os 7 8 app = Flask(__name__) 9 10 @app.route('/') 11 def index(): 12 return render_template('index.html') 13 14 # run Flask app 15 if __name__ == "__main__": 16 app.run()
In the code above, after we instantiate Flask using app = Flask(__name__)
, we created a new route - /
which renders an index.html
file from the templates folder.
Now, add the following python packages to the requirements.txt
file:
1Flask==1.0.2 2 python-dotenv==0.8.2 3 pusher==2.0.1 4 textblob==0.15.1
The packages we added:
Next, install the library by executing the below command:
$ pip install -r requirements.txt
Once the packages are done installing, start up Flask:
$ flask run
If there is no error, our Flask app will now be available on port 5000. If you visit http://localhost:5000, you will see a blank page. This is because the templates/index.html
file is empty, which is ok for now.
To get the sentiment from comments, we’ll use the TextBlob Python library which provides a simple API for common natural language processing (NLP). We already have the library installed. What we’ll do now is install the necessary data that TextBlob will need.
From your terminal, make sure you are in the project root folder. Also, make sure your virtualenv is activated. Then execute the below function.
1# Download NLTK corpora 2 $ python -m textblob.download_corpora lite
This will download the necessary NLTK corpora (trained models).
Initialize the Pusher Python library by adding the below code to app.py
just after the app = Flask(__name__)
line:
1# app.py 2 3 pusher = pusher.Pusher( 4 app_id=os.getenv('PUSHER_APP_ID'), 5 key=os.getenv('PUSHER_APP_KEY'), 6 secret=os.getenv('PUSHER_APP_SECRET'), 7 cluster=os.getenv('PUSHER_APP_CLUSTER'), 8 ssl=True)
Now we are fully set.
We’ll create a simple page for adding comments. Since we won’t be building a full blog website, we won’t be saving the comments to a database.
We’ll use the template inheritance approach to build our views, which makes it possible to reuse the layouts instead of repeating some markup across pages.
Add the following markup to the templates/base.html
file:
1<!-- /templates/base.html --> 2 3 <!doctype html> 4 <html lang="en"> 5 <head> 6 <!-- Required meta tags --> 7 <meta charset="utf-8"> 8 <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> 9 <!-- Bootstrap CSS --> 10 <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css"> 11 <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}"> 12 <title>Live comment</title> 13 </head> 14 <body> 15 <div class="container" id="app"> 16 {% block content %} {% endblock %} 17 </div> 18 </div> 19 <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> 20 <script src="https://js.pusher.com/4.1/pusher.min.js"></script> 21 <script src="{{ url_for('static', filename='custom.js')}}"></script> 22 </body> 23 </html>
This is the base layout for our view. All other views will inherit from the base file.
In this file, we have added some libraries. This includes:
This will serve as the landing page of the application. Add the following to the templates/index.html
file:
1<!-- /templates/index.html --> 2 3 4 {% extends 'base.html' %} 5 6 {% block content %} 7 <div class="grid-container"> 8 <header class="header text-center"> 9 <img src="https://cdn1.imggmi.com/uploads/2018/10/13/1d5cff977fd6e3aac498e581ef681a1a-full.png"> 10 </header> 11 <main class="content"> 12 <div class="content-text"> 13 Our pioneering and unique technology is based on state-of-the-art <br/> 14 machine learning and computer vision techniques. Combining deep neural <br/> 15 networks and spectral graph theory with the computing... <br/> 16 </div> 17 </main> 18 <section class="mood"> 19 <div class="row"> 20 <div class="col text-center"> 21 <div class="mood-percentage">[[ happy ]]%</div> 22 <div>Happy</div> 23 </div> 24 <div class="col text-center"> 25 <div class="mood-percentage">[[ neutral ]]%</div> 26 <div>Neutral</div> 27 </div> 28 <div class="col text-center"> 29 <div class="mood-percentage">[[ sad ]]%</div> 30 <div>Sad</div> 31 </div> 32 </div> 33 </section> 34 <section class="comment-section"> 35 <div v-for="comment in comments"> 36 <comment 37 :comment="comment" 38 v-bind:key="comment.id" 39 > 40 </comment> 41 </div> 42 </section> 43 <section class="form-section"> 44 <form class="form" @submit.prevent="addComment"> 45 <div class="form-group"> 46 <input 47 type="text" 48 class="form-control" 49 v-model="username" 50 placeholder="Enter username"> 51 </div> 52 <div class="form-group"> 53 <textarea 54 class="form-control" 55 v-model="comment" 56 rows="3"></textarea> 57 </div> 58 <button type="submit" class="btn btn-primary btn-block">Add comment</button> 59 </form> 60 </section> 61 </div> 62 {% endblock %}
In the preceding code:
In the <section class="mood">… </section>
, we added three placeholders - [[ happy ]], [[ neutral ]] and [[ sad ]], which is the percentages of the moods of users who added comments. These placeholders will be replaced by their actual values when Vue takes over the page DOM (mounted).
Notice we are using
[[ ]]
instead of the normal Vue placeholders -{{ }}
. This is because we are using Jinja2 template that comes bundled with Flask to render our page. The Jinja2 uses{{ }}
placeholder to hold variables that will be substituted to their real values and so do Vue by default. So to avoid conflicts, we will change Vue to use[[ ]]
instead.
In the <section class="comment-section">
section, we are rendering the comments to the page.
Next, is the <section class="form-section">… </section>
, where we added a form for adding new comments. Also in the inputs fields, we declare a two-way data binding using the v-model directive.
In the form section - <form class="form" @submit.prevent="addComment">…
, notice that we have the @submit.prevent
directive. This will prevent the form from submitting normally when the user adds a new comment. Then we call the addComment
function to add a comment. We don’t have the addComment
function declared anywhere yet. We’ll do this when we initialize Vue.
Now, add some styles to the page. Add the below styles to the static/style.css
file:
1body { 2 width: 100%; 3 height: 100%; 4 } 5 .grid-container { 6 display: grid; 7 grid-template-rows: 250px auto auto 1fr; 8 grid-template-columns: repeat(3, 1fr); 9 grid-gap: 20px; 10 grid-template-areas: 11 '. header .' 12 'content content content' 13 'mood mood mood' 14 'comment-section comment-section comment-section' 15 'form-section form-section form-section'; 16 } 17 .content { 18 grid-area: content; 19 } 20 .comment-section { 21 grid-area: comment-section; 22 } 23 .content-text { 24 font-style: oblique; 25 font-size: 27px; 26 } 27 .mood { 28 grid-area: mood; 29 } 30 .header { 31 grid-area: header; 32 } 33 .form-section { 34 grid-area: form-section; 35 } 36 .comment { 37 border: 1px solid rgb(240, 237, 237); 38 border-radius: 4px; 39 margin: 15px 0px 5px 60px; 40 font-family: monospace; 41 } 42 .comment-text { 43 padding-top: 10px; 44 font-size: 17px; 45 } 46 .form { 47 margin-top: 50px; 48 } 49 .mood-percentage { 50 border: 1px solid gray; 51 min-height: 50px; 52 padding-top: 10px; 53 font-size: 30px; 54 font-weight: bolder; 55 }
Now we have all our user interface ready. If you visit the app URL again, you will see a similar page as below:
Now let’s initialize Channels. Since we have added the Pusher JavaScript library already, we’ll go ahead and initialize it.
Add the below code to the static/custom.js
file:
1// Initiatilze Pusher JavaScript library 2 var pusher = new Pusher('<PUSHER-APP-KEY>', { 3 cluster: '<CLUSTER>', 4 forceTLS: true 5 });
Replace <PUSHER-APP-KEY>
and <CLUSTER>
with your correct Pusher app details you noted down earlier.
If you view the /templates/index.html
file, in the <section class="comment-section">
section, you will notice we are calling the <comment>
component which we have not created yet. We need to create this component. Also, notice inside the file, we are calling the v-for (v-for="comment in comments"
) directive to render the comments.
Let’s create the component. Add the below code to static/custom.js
:
1Vue.component('comment', { 2 props: ['comment'], 3 template: ` 4 <div class="row comment"> 5 <div class="col-md-2"> 6 <img 7 src="https://cdn1.imggmi.com/uploads/2018/10/13/1d5cff977fd6e3aac498e581ef681a1a-full.png" 8 class="img-responsive" 9 width="90" 10 height="90" 11 > 12 </div> 13 <div class="col-md-10 comment-text text-left" v-html="comment.comment"> </div> 14 </div> 15 ` 16 })
Now let’s initialize Vue to take over the DOM manipulation.
Add the below code to the static/custom.js
file:
1var app = new Vue({ 2 el: '#app', 3 delimiters: ['[[', ']]'], 4 data: { 5 username: '', 6 comment: '', 7 comments: [], 8 happy: 0, 9 sad: 0, 10 neutral: 0, 11 socket_id: "" 12 }, 13 methods: {}, 14 created () {}, 15 })
In the preceding code:
var app = new Vue(…
passing to it a key-value object.el:
'``#app'
. The #app
is the ID we have declared in the /templates/base.html
.delimiters: ['[[', ']]'],
, we change the default Vue delimiter from {{ }}
to [[ ]]
so that it does not interfere with that of Jinja2.data: {….
.methods: {},
and created () {},
. We’ll add all the function we’ll declare inside the methods: {}
block and then the created () {}
is for adding code that will execute once Vue instance is created.Next, add a function to update the sentiment score. Add the below code to the methods: {}
block of the static/custom.js
file:
1updateSentiments () { 2 // Initialize the mood to 0 3 let [happy, neutral, sad] = [0, 0, 0]; 4 5 // loop through all comments, then get the total of each mood 6 for (comment of this.comments) { 7 if (comment.sentiment > 0.4) { 8 happy++; 9 } else if (comment.sentiment < 0) { 10 sad++; 11 } else { 12 neutral++; 13 } 14 } 15 16 const total_comments = this.comments.length; 17 18 // Get the percentage of each mood 19 this.sad = ((sad/total_comments) * 100).toFixed(); 20 this.happy = ((happy/total_comments) * 100).toFixed(); 21 this.neutral = ((neutral/total_comments) * 100).toFixed() 22 23 // Return an object of the mood values 24 return {happy, neutral, sad} 25 },
In the code above, we created a function that will loop through all the comments to get the number of each mood that appeared. Then we get the percentage of each mood then return their corresponding values.
Next, add a function to add a new comment. Add the below code to the methods: {} block right after the code you added above:
1addComment () { 2 3 fetch("/add_comment", { 4 method: "post", 5 headers: { 6 'Accept': 'application/json', 7 'Content-Type': 'application/json' 8 }, 9 body: JSON.stringify({ 10 id: this.comments.length, 11 username: this.username, 12 comment: this.comment, 13 socket_id: this.socket_id 14 }) 15 }) 16 .then( response => response.json() ) 17 .then( data => { 18 // Add the new comment to the comments state data 19 this.comments.push({ 20 id: data.id, 21 username: data.username, 22 comment: data.comment, 23 sentiment: data.sentiment 24 }) 25 26 // Update the sentiment score 27 this.updateSentiments(); 28 }) 29 30 this.username = ""; 31 this.comment = ""; 32 },
Here, we created a function that makes a request to the /add_comment
route to get the sentiment of a comment. Once we receive a response, we add the comment to the comments state. Then we call this.updateSentiments()
to update the sentiment percentage. This function will be called each time a user wants to add a new comment.
Next, let’s make comments visible to others in realtime. Add the below code to the created () {}
block in the static/custom.js:
1// Set the socket ID 2 pusher.connection.bind('connected', () => { 3 this.socket_id = pusher.connection.socket_id; 4 }); 5 6 // Subscribe to the live-comments channel 7 var channel = pusher.subscribe('live-comments'); 8 9 // Bind the subscribed channel (live-comments) to the new-comment event 10 channel.bind('new-comment', (data) => { 11 this.comments.push(data); 12 13 // Update the sentiment score 14 this.updateSentiments(); 15 });
Now, let’s add a function to get the sentiment of a message and then trigger a new-comment
event whenever a user adds a comment. Add the below code to app.py
1# ./api/app.py 2 3 @app.route('/add_comment', methods=["POST"]) 4 def add_comment(): 5 # Extract the request data 6 request_data = request.get_json() 7 id = request_data.get('id', '') 8 username = request_data.get('username', '') 9 comment = request_data.get('comment', '') 10 socket_id = request_data.get('socket_id', '') 11 12 # Get the sentiment of a comment 13 text = TextBlob(comment) 14 sentiment = text.polarity 15 16 comment_data = { 17 "id": id, 18 "username": username, 19 "comment": comment, 20 "sentiment": sentiment, 21 } 22 23 # Trigger an event to Pusher 24 pusher.trigger( 25 "live-comments", 'new-comment', comment_data, socket_id 26 ) 27 28 return jsonify(comment_data)
The sentiment property returns a tuple of the form (polarity, subjectivity) where polarity ranges from -1.0 to 1.0 and subjectivity ranges from 0.0 to 1.0. We will only use the polarity property.
In the pusher.trigger(…
, method, we are passing the socket_id
so that the user triggering the event won't get back the data sent.
Congrats! Now we have our live comments with sentiments. To test the app, open the app in your browser on two or more different tabs, then add comments and see them appear in realtime on other tabs.
Here is some sample comment you can try out:
If you are getting an error or nothing is working. Stop the server (Press CTRL+C) and then restart it ($ flask run
).
In this tutorial, we built a live comment with sentiment analysis. We used Vue for DOM manipulation, Flask for the server side and Channels for realtime functionality. We used the TextBlob python library to detect mood from text.