This is the twenty-first part of the Flask Mega-Tutorial, in which I will add the function of private messages, as well as notifications to users that appear on the navigation bar without the need to refresh the page.
Under the spoiler is a list of all articles in the 2018 series.
Note 1: If you are looking for old versions of this course, this is here .
Note 2: If suddenly you would like to speak in support of my (Miguel) work, or simply do not have the patience to wait for the article for a week, I (Miguel Greenberg) offer the full version of this manual (in English) in the form of an electronic book or video. For more information, visit learn.miguelgrinberg.com .
In this chapter, I want to continue working on improving the user interface of Microblog. One aspect that applies to many applications is the presentation of alerts or notifications to the user. Social networks show these notifications so that you know that you have new mentions or private messages, usually showing a small number icon in the top navigation bar. Although this is the most obvious use, the notification template can be applied to many other types of applications to inform the user that something needs their attention.
But in order to show you the methods associated with creating custom notifications, I needed to extend the functionality of Microblog. Therefore, in the first part of this chapter, I am going to build a user messaging system that allows any user to send a private message to another user. This is actually simpler than it seems, and it will be a good excuse to brush up on the basics of working with Flask and reminding you how effective and interesting programming is with Flask. And as soon as the messaging system is installed, I'm going to discuss some options for implementing a notification icon that shows the number of unread messages.
GitHub links for this chapter: Browse , Zip , Diff .
The private messaging feature I'm going to implement will be very simple. When you visit the user profile page, there will be a link to send a private message to this user. The link will take you to a new page that has a web form for receiving a message. To read messages sent to you, the navigation bar at the top of the page will have a new "Messages" link, which will lead you to a page that is similar in structure to the index or explore pages, but instead of displaying blog posts, it will show messages from other users sent to you.
The following sections describe the steps I took to implement this feature.
The first task is to expand the database to support personal messages. Here is the new message model:
app / models.py : Message model.
class Message(db.Model): id = db.Column(db.Integer, primary_key=True) sender_id = db.Column(db.Integer, db.ForeignKey('user.id')) recipient_id = db.Column(db.Integer, db.ForeignKey('user.id')) body = db.Column(db.String(140)) timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow) def __repr__(self): return '<Message {}>'.format(self.body)
This model class is similar to the Post
model, with the only difference that there are two user foreign keys, one for the sender and one for the receiver. The User
model can get relationships for these two users, as well as a new field indicating what was the last time users read their private messages:
app / models.py : Support for private messages in the User model .
class User(UserMixin, db.Model): # ... messages_sent = db.relationship('Message', foreign_keys='Message.sender_id', backref='author', lazy='dynamic') messages_received = db.relationship('Message', foreign_keys='Message.recipient_id', backref='recipient', lazy='dynamic') last_message_read_time = db.Column(db.DateTime) # ... def new_messages(self): last_read_time = self.last_message_read_time or datetime(1900, 1, 1) return Message.query.filter_by(recipient=self).filter( Message.timestamp > last_read_time).count()
These two links will return messages sent and received for this user, and on the side of the Message
relationship will be added links to author
and recipient
. The reason why I used backref (back link) author
instead of perhaps a more appropriate sender
(sender) is that with the help of author
I can visualize these posts using the same logic that I use for blog posts. The last_message_read_time
field will contain last time when the user last visited the message page, and will be used to determine if there are unread messages that will have a newer time stamp than in this field. The helper method new_messages()
actually uses this field to return the number of unread messages from the user. By the end of this chapter, I will have this number displayed as an icon in the navigation bar at the top of the page.
This completes the database changes, so now it's time to create a new migration and update the database with it:
(venv) $ flask db migrate -m "private messages" (venv) $ flask db upgrade
Next, I'm going to work on sending messages. I will need a simple web form that allows you to type a message:
app / main / forms.py: The class of the private message form.
class MessageForm(FlaskForm): message = TextAreaField(_l('Message'), validators=[ DataRequired(), Length(min=0, max=140)]) submit = SubmitField(_l('Submit'))
And I also need an HTML template that displays this form on a webpage:
app / templates / send_message.html: HTML template Send a private message.
{% extends "base.html" %} {% import 'bootstrap/wtf.html' as wtf %} {% block app_content %} <h1>{{ _('Send Message to %(recipient)s', recipient=recipient) }}</h1> <div class="row"> <div class="col-md-4"> {{ wtf.quick_form(form) }} </div> </div> {% endblock %}
Next, I'm going to add new / send_message / to handle the actual sending of the private message:
app / main / routes.py: Route of sending a private message.
from app.main.forms import MessageForm from app.models import Message # ... @bp.route('/send_message/<recipient>', methods=['GET', 'POST']) @login_required def send_message(recipient): user = User.query.filter_by(username=recipient).first_or_404() form = MessageForm() if form.validate_on_submit(): msg = Message(author=current_user, recipient=user, body=form.message.data) db.session.add(msg) db.session.commit() flash(_('Your message has been sent.')) return redirect(url_for('main.user', username=recipient)) return render_template('send_message.html', title=_('Send Message'), form=form, recipient=recipient)
I think that the logic in this function should be basically understandable. The action of sending a private message is simply performed by adding a new Message
instance to the database.
The last change that binds together is to add a link to the above route on the user profile page:
app / templates / user.html: The link to send a private message on the user profile page.
{% if user != current_user %} <p> <a href="{{ url_for('main.send_message', recipient=user.username) }}"> {{ _('Send private message') }} </a> </p> {% endif %}
The second big part of this feature is viewing personal messages. To do this, I'm going to add another route to / messages , which works quite similarly to the index and explores the pages, including full support for pagination:
app / main / routes.py: Viewing messages route.
@bp.route('/messages') @login_required def messages(): current_user.last_message_read_time = datetime.utcnow() db.session.commit() page = request.args.get('page', 1, type=int) messages = current_user.messages_received.order_by( Message.timestamp.desc()).paginate( page, current_app.config['POSTS_PER_PAGE'], False) next_url = url_for('main.messages', page=messages.next_num) \ if messages.has_next else None prev_url = url_for('main.messages', page=messages.prev_num) \ if messages.has_prev else None return render_template('messages.html', messages=messages.items, next_url=next_url, prev_url=prev_url)
The first thing I do in this view function is to update the User.last_message_read_time
field with the current time. Mark all messages that were sent to this user as read. Then I query the Message
model for a list of messages, sorted by label from new to old. I decided to use the POSTS_PER_PAGE
configuration element from the page of posts and messages, which will be very similar, but, of course, if the pages are common, it may make sense to add a separate variable for the messages. The paging logic is identical to what I used for messages, so you should be familiar with all this.
The view function above ends with the rendering of the template file /app/templates/messages.html , which you can see below:
app / templates / messages.html: HTML message view template.
{% extends "base.html" %} {% block app_content %} <h1>{{ _('Messages') }}</h1> {% for post in messages %} {% include '_post.html' %} {% endfor %} <nav aria-label="..."> <ul class="pager"> <li class="previous{% if not prev_url %} disabled{% endif %}"> <a href="{{ prev_url or '#' }}"> <span aria-hidden="true">←</span> {{ _('Newer messages') }} </a> </li> <li class="next{% if not next_url %} disabled{% endif %}"> <a href="{{ next_url or '#' }}"> {{ _('Older messages') }} <span aria-hidden="true">→</span> </a> </li> </ul> </nav> {% endblock %}
Here I resorted to another little trick. I noticed that the Post
and Message
instances have almost the same structure, except that Message
gets an extra connection to the recipient
(which I don’t need to show on the Messages page, as this is always the current user). Therefore, I decided to reuse the app / templates / _post.html sub-template also for displaying private messages. For this reason, this template uses a specific for-loop for for post in messages
, so all references to the post
in the sub-template also work with messages.
To give users access to a new viewing function, the navigation page receives a new "Messages" link:
app / templates / base.html: Messages link in the navigation bar.
{% if current_user.is_anonymous %} ... {% else %} <li> <a href="{{ url_for('main.messages') }}"> {{ _('Messages') }} </a> </li> ... {% endif %}
This function has now been completed, but within all of these changes some new texts have appeared, which have been added in several places, and they should be included in the language translations. The first step is to update all language directories:
(venv) $ flask translate update
Then each of the languages in app / translations should have its own messages.po file updated with new translations. You can find Spanish translations in the GitHub repository for this project or in a zip file .
Now the private messaging function is implemented, but, of course, there is nothing that tells the user that there are private messages awaiting perusal. The simplest implementation of the navigation bar indicator can be displayed as part of the base template using the Bootstrap badge widget:
app / templates / base.html: Static message counter icon in the navigation bar.
... <li> <a href="{{ url_for('main.messages') }}"> {{ _('Messages') }} {% set new_messages = current_user.new_messages() %} {% if new_messages %} <span class="badge">{{ new_messages }}</span> {% endif %} </a> </li> ...
Here I call the new_messages()
method, which I added to the User
model above directly from the template, and store this number in the template variable new_messages
. Then, if this variable is not zero, I simply add the icon with the number next to the Messages link. Here is how it looks on the page:
The solution presented in the previous section is a decent and easy way to show a notification, but it has the disadvantage that the icon appears only when loading a new page. If a user reads content on one page for a long time, without clicking on any links, new messages that arrive during this time will not be displayed until the user finally clicks on the link and loads a new page.
To make this application more useful for my users, I want the icon to update the number of unread messages on my own, without having to click on links and download new pages. One of the problems with the solution from the previous section is that the icon is displayed on the page only when the number of messages at the time of loading the page was non-zero. What is really more convenient is to always include the icon in the navigation bar and mark it as hidden when the number of messages is zero. This makes it easy to make the icon visible using javascript:
app / templates / base.html: Unread message icon, compatible with JavaScript.
<li> <a href="{{ url_for('main.messages') }}"> {{ _('Messages') }} {% set new_messages = current_user.new_messages() %} <span id="message_count" class="badge" style="visibility: {% if new_messages %}visible {% else %}hidden {% endif %};"> {{ new_messages }} </span> </a> </li>
In this version of the badge it is always on. But! The CSS property visible
set to visible
when new_messages
nonzero and hidden if it is zero. I also added an id
attribute to the <span>
element, which represents an icon, to make it easier to access this element using the jQuery $('#message_count')
selector $('#message_count')
.
Now I can encode a short JavaScript function that updates this icon to the new number:
app / templates / base.html: Static message counter icon in the navigation bar.
... {% block scripts %} <script> // ... function set_message_count(n) { $('#message_count').text(n); $('#message_count').css('visibility', n ? 'visible' : 'hidden'); } </script> {% endblock %}
The new set_message_count()
function sets the number of messages in the badge element, and also adjusts the visibility so that the icon is hidden when the counter is 0, and is otherwise visible.
It now remains to add a mechanism by which the client receives periodic updates regarding the number of unread messages that the user has. When one of these updates occurs, the client calls the set_message_count()
function to make the update known to users.
There are actually two methods on the server to deliver these updates to the client, and, as you probably guess, both have pros and cons, so which one to choose depends largely on the project. In the first approach, the client periodically requests updates from the server, sending an asynchronous request. The response to this request is a list of updates that the client can use to update various page elements, such as the unread message counter icon. The second approach requires a special type of connection between the client and the server, which allows the server to freely transfer data to the client. Please note that regardless of the approach, I want to consider notifications as general entities so that I can extend this platform to support other types of events besides the icon of unread messages.
The most important thing in the first solution is that it is easy to implement. All I need to do is add another route to the application, say / notifications , which will return the JSON list of notifications. The client application then scans the list of notifications and applies the necessary changes to the page for each of them. The disadvantage of this solution is that there will be a delay between the actual event and the notification for it, because the client will request a list of notifications at regular intervals. For example, if a client requests a notification every 10 seconds, a notification can be received with a delay of up to 10 seconds.
The second solution requires changes at the protocol level, since HTTP has no conditions for the server to send data to the client without a client request. Today, the most common way to implement messages initiated by a server is to extend the server to support WebSocket connections in addition to HTTP. WebSocket is a protocol that, in contrast to HTTP, establishes a permanent connection between a server and a client. The server and the client can simultaneously send data to the other side, without paying attention to the other side. The advantage of this mechanism is that whenever an event of interest to the client occurs, the server can send a notification without any delay. The disadvantage is that WebSocket requires more complex configuration than HTTP, because the server needs to maintain a constant connection with each client. Imagine that a server, which, for example, has four workflows, can usually serve several hundred HTTP clients, since HTTP connections are short-lived and are constantly being processed. The same server will only be able to process four WebSocket clients, which in most cases will be insufficient. It is for this limitation that WebSocket applications are usually developed around asynchronous servers, since these servers are more efficient in managing large numbers of working and active connections.
The good news is that regardless of the method you use, in the client you will have a callback function that will be called with a list of updates. Therefore, I could start with the first option, which is much easier to implement, and then, if I find it insufficient, I will switch to the WebSocket server, which can be configured to call the same client callback. In my opinion, for this type of application the first solution is actually acceptable. A WebSocket-based implementation would be useful for an application that requires updates to be delivered with an almost zero delay.
If you're interested, Twitter also uses the first approach for navigation bar notifications. Facebook uses a variant called Long_polling , which removes some of the limitations of direct polling, while still using HTTP requests. Stack Overflow and Trello are two Sites that implement WebSocket for their notifications. You can find out what type of background activity occurs on any site by viewing the Network tab in the browser debugger.
So let's continue and implement the survey solution. First, I'm going to add a new model to track notifications for all users, as well as relationships in the user model.
app / models.py: Model of notifications.
import json from time import time # ... class User(UserMixin, db.Model): # ... notifications = db.relationship('Notification', backref='user', lazy='dynamic') # ... class Notification(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(128), index=True) user_id = db.Column(db.Integer, db.ForeignKey('user.id')) timestamp = db.Column(db.Float, index=True, default=time) payload_json = db.Column(db.Text) def get_data(self): return json.loads(str(self.payload_json))
The notification will have the name of the associated user, a Unix timestamp, and a payload. The timestamp gets its default value from the time.time()
function. The payload will be different for each type of notification, so I write it as a JSON string, as this will allow me to write lists, dictionaries, or individual values, such as numbers or strings. For convenience, I added the get_data()
method so that the caller doesn’t have to worry about JSON deserialization.
These changes need to be included in the new database migration:
(venv) $ flask db migrate -m "notifications" (venv) $ flask db upgrade
For convenience, I'm going to add new Message
and Notification
models to the shell context, so that when the shell is started using the flask shell
command, the model class is automatically imported for me:
microblog.py: Add a message model to a shell context.
# ... from app.models import User, Post, Notification, Message # ... @app.shell_context_processor def make_shell_context(): return {'db': db, 'User': User, 'Post': Post, 'Message': Message 'Notification': Notification}
I am also going to add the add_notification()
helper method to the user model in order to simplify working with these objects:
app / models.py: Model notification.
class User(UserMixin, db.Model): # ... def add_notification(self, name, data): self.notifications.filter_by(name=name).delete() n = Notification(name=name, payload_json=json.dumps(data), user=self) db.session.add(n) return n
This method not only adds a notification for the user to the database, but also ensures that if a notification with the same name already exists, it will be deleted. The notification I'm going to work with will be called unread_message_count
. If the database already has a notification with this name, for example, with 3 values at the time when the user receives a new message, and the number of messages reaches 4, I want to replace the old notification.
In any place where the number of unread messages changes, I need to call add_notification()
to update my notifications for the user. There are two places where it changes. First, when a user receives a new private message in the send_message()
function:
app / main / routes.py: Update user notification.
@bp.route('/send_message/<recipient>', methods=['GET', 'POST']) @login_required def send_message(recipient): # ... if form.validate_on_submit(): # ... user.add_notification('unread_message_count', user.new_messages()) db.session.commit() # ... # ...
The second place where I need to notify the user is when the user goes to the messages page, after which the unread message counter returns to zero:
app / main / routes.py: Viewing messages route.
@bp.route('/messages') @login_required def messages(): current_user.last_message_read_time = datetime.utcnow() current_user.add_notification('unread_message_count', 0) db.session.commit() # ...
Now that all notifications for users are maintained in the database, I can add a new route that the client can use to retrieve notifications for a registered user:
app / main / routes.py: The function of viewing notifications.
from app.models import Notification # ... @bp.route('/notifications') @login_required def notifications(): since = request.args.get('since', 0.0, type=float) notifications = current_user.notifications.filter( Notification.timestamp > since).order_by(Notification.timestamp.asc()) return jsonify([{ 'name': n.name, 'data': n.get_data(), 'timestamp': n.timestamp } for n in notifications])
This is a fairly simple function that returns the payload in JSON with a list of notifications to the user. Each notification is provided as a dictionary with three elements, the name of the notification, additional data related to the notification (for example, the number of messages), and a timestamp. Notifications are delivered in the order in which they were created, from the oldest to the newest.
, , . since
«» URL- . , , , .
, . — , :
app/templates/base.html: .
... {% block scripts %} <script> // ... {% if current_user.is_authenticated %} $(function() { var since = 0; setInterval(function() { $.ajax('{{ url_for('main.notifications') }}?since=' + since).done( function(notifications) { for (var i = 0; i < notifications.length; i++) { if (notifications[i].name == 'unread_message_count') set_message_count(notifications[i].data); since = notifications[i].timestamp; } } ); }, 10000); }); {% endif %} </script>
conditional, , . , , .
$(function() { ...})
jQuery's 20 . . , . setTimeout()
JavaScript, , . setInterval()
, setTimeout()
, . 10 ( ), .
, , Ajax- , . unread_message_count
, , , .
, since
, . 0. URL-, url_for()
Flask, , url_for()
, since
. /notifications?since=0 , , since
. , , , , . , since
, , , , .
. , . . , , 10 . "Messages" .
Source: https://habr.com/ru/post/354322/
All Articles