📜 ⬆️ ⬇️

Flask Mega-Tutorial, Chapter 8: Subscribers, Contacts and Friends (Edition 2018)

blog.miguelgrinberg.com


Miguel grinberg




<<< previous next >>>


This article is a translation of the eighth part of the new edition of Miguel Greenberg’s textbook, the issue of which the author plans to complete in May 2018. The previous translation has long since lost its relevance.




This is the eighth part of the Flask Mega-Tutorial series, in which I will tell you how to implement the "subscribers" function, similar to the functions of Twitter and other social networks.


For reference, below is a list of articles in this 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 on this blog, or simply do not have the patience to wait for a week of the article, I (Miguel Greenberg) offer a full version of this guide a packed e-book or video. For more information, visit learn.miguelgrinberg.com .


In this chapter, I will work a little more on the structure of the database. I want users of the application to easily organize a subscription to their content. Therefore, I am going to make changes to the database so that it can keep track of who is tracking whom, which is somewhat more complicated than you think.


GitHub links for this chapter: Browse , Zip , Diff .


Again database links


As I said above, I want to maintain a list of users "followed" and "followers" ("followed" and "follower") for each user. Unfortunately, the relational database does not have a list type, which I can use for these very lists, all that is - these are tables with records and relationships between these records.


There is a table in the database representing users users , so it remains to come up with the right type of relationship that the followed / follower link can model. Let's review the basic types of relationships in the database:


One to many (One-to-Many)


I have already used one-to-many relationships in chapter 4 . Here is a diagram for this link:



The two objects associated with this relationship are users and messages. We see that the user has many messages, and the message has one user (or author). The link is represented in the database using the foreign key on the multi side. In the above link, the foreign key is the user_id field added to the posts table.
This field links each message to its author’s entry in the user table.


Obviously, the user_id field provides direct access to the author of this message, but what about the reverse direction? For the connection to be useful, I must be able to get a list of messages written by this user.
The user_id field in the posts table is also sufficient to answer this question, because the databases have indexes that allow you to create effective queries, so we "retrieve (retrieve) all messages that have user_id from X".


Many-to-many


The many-to-many relationship is somewhat more complicated. As an example, consider a database in which there are students and teachers teachers . I can say that a student has many teachers, and a teacher has many students. It looks like two interconnected one-to-many relationships from both ends.
For relationships of this type, I must be able to query the database and get a list of the teachers who teach the student and the list of students in the classroom. This is not trivial to view in a relational database, since it cannot be done by adding foreign keys to existing tables.


The many-to-many representation of a multi-valued representation requires the use of an auxiliary table, called the association table . Here is an example of organizing a search for students and teachers in the database:



Perhaps to someone it may seem obscure, the table of associations with two foreign keys effectively responds to all requests for relationships.


Many-to-one and one-to-one (Many-to-One and One-to-One)


Many-to-one looks like a one-to-many relationship. The difference is that this connection is viewed by the “Many”.


One-to-one is a one-to-many special case. The view is similar, but a restriction is added to the database to prevent the “Many” side from forbidding to have more than one link.
Although there are cases when this type of relationship is useful, but it is not as common as other types.


Submission Subscribers


Based on the sum of the analysis of all the types of relationships presented above, it is easy to determine that the correct data model for tracking followers followers is a many-to-many relationship, since the tracker follows many users and the user has many subscribers (followers). But there is a setup. In the example of students and teachers, I had two objects that were interconnected by many-to-many relationships. But in the case of followers, I have users who follow other users, so there are only users. So what is the second structure of many-to-many relationships?


The second relationship object is also the user.
A relationship in which class instances are associated with other instances of the same class is called a self-referential relationship, and this is what I have here.


Here is a diagram of many-to-many self-referencing follower followers:



The followers table is a relationship association table or relative relationship table. Foreign keys in this table point to entries in the user table because they associate users with users. Each entry in this table represents one relationship between the follower user subscriber and the subscribed user followed user. As an example of students and teachers, a setting like this allows the database to answer all the questions about subscribed and followers that I will ever need. Pretty neat.


Database Model Representation


Let's add followers to the database first. Here is the subscriber association table:


 followers = db.Table('followers', db.Column('follower_id', db.Integer, db.ForeignKey('user.id')), db.Column('followed_id', db.Integer, db.ForeignKey('user.id')) ) 

This is a live translation of the table of associations from my diagram above. Please note that I do not declare this table as a model, as I did for user tables and messages. Since this is an auxiliary table that does not have data other than foreign keys, I created it without a corresponding model class.


Now I can declare a many-to-many relationship in the users table:


 class User(UserMixin, db.Model): # ... followed = db.relationship( 'User', secondary=followers, primaryjoin=(followers.c.follower_id == id), secondaryjoin=(followers.c.followed_id == id), backref=db.backref('followers', lazy='dynamic'), lazy='dynamic') 

Note followers translator . c .follower_id "c" is an attribute of SQLAlchemy tables that are not defined as models. For these tables, the table columns are displayed as sub-attributes of this “c” attribute.

Setting the relationship is nontrivial. As with the one-to-many posts relationship, I use the db.relationship function to define relationships in the model class. This relationship binds User instances to other User instances, since the agreement allows us to say that for a couple of users connected by this relationship, the left side user follows the user of the right side. I define a connection, as seen from the left side with the name followed , because when I request this link on the left side, I will get a list of subsequent users (ie, those on the right side). Let's look at all the arguments to db.relationship() one by one:



Do not worry if it is difficult to understand. I will show you how to work with these requests and then in an instant everything will become clearer.


Changes in the database must be recorded in the new database migration:


 (venv) $ flask db migrate -m "followers" INFO [alembic.runtime.migration] Context impl SQLiteImpl. INFO [alembic.runtime.migration] Will assume non-transactional DDL. INFO [alembic.autogenerate.compare] Detected added table 'followers' Generating /home/miguel/microblog/migrations/versions/ae346256b650_followers.py ... done (venv) $ 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 37f06a334dbf -> ae346256b650, followers 

Adding and deleting "follows" (subscriber)


Thanks to the SQLAlchemy ORM, a user who subscribes to another user’s tracking can be written to the database as followed if it were a list. For example, if I had two users that are stored in the variables user1 and user2 , I can make the first user1 follow the second user2 with this simple statement:


 user1.followed.append(user2) 

In order for user1 to abandon tracking user2 , you need to do this:


 user1.followed.remove(user2) 

Although adding and removing subscribers is fairly easy, I want to simplify reuse in my code, so I’m not going to “add” (appends) and “delete” (removes) through code. Instead, I'm going to implement the "follow" and "unfollow" functionality as methods in the User model. It is always better to move the application logic away from the view functions to the model or to other auxiliary classes or modules, because, as you will see later in this chapter, this makes unit testing much easier.


Below are the changes in the custom model for adding and removing links:


 class User(UserMixin, db.Model): #... def follow(self, user): if not self.is_following(user): self.followed.append(user) def unfollow(self, user): if self.is_following(user): self.followed.remove(user) def is_following(self, user): return self.followed.filter( followers.c.followed_id == user.id).count() > 0 

The follow() and unfollow() methods use the object's append() and remove() methods, as shown above, but before they are applied, they use the is_following() check method to make sure that the requested action has meaning. For example, if I ask user1 to follow user2 , but it turns out that such a task already exists in the database, then why create a duplicate. The same logic can be applied to unfollowing .


The is_following() method is_following() whether the relationship exists between two users. I used to use the SQLAlchemy query filter_by() method, for example, to find a user by their username . The filter() method that I use here is similar, but lower level, because it can include arbitrary filtering conditions, unlike filter_by() , which can only check equality for a constant value. The condition I use in is_following() looks for entries in the association table that have the left side foreign key set for the user self , and the right side for the user argument. The request is terminated by the count() method, which returns the number of records. The result of this query will be 0 or 1 , so checking that the counter is 1 or greater than 0 is actually equivalent. The other query terminators you've seen in the past are all() and first() .


Receiving Messages from Followed Users


Subscriber support in the database is almost complete, but in fact I’m missing one important feature. On the index page of the application, I am going to show blog entries written by all people followed by a registered user, so I need to create a database query that will return these messages.


The most obvious solution is to run a query that returns a list of followed users, which, as you already know, will be user.followed.all() . Then for each of these users, I can run a request to receive their messages. When I have all the messages, I can combine them into one list and sort them by date. It sounds good? Well, not quite.


This approach has several problems. What happens if a user’s subscription has thousands of people? I need to perform thousands of database queries to collect all the messages. And then I will need to combine and sort the thousands of lists in memory. As a secondary problem, consider that the application's home page will eventually be broken down by page, so it will not display all the available messages, but only the first few, with a link to get more if needed. If I am going to display messages sorted by date, how can I find out which messages are the last of all users followed, unless I receive all the messages and sort them first? This is a really creepy solution that doesn’t scale well.


There is no way to avoid this merging and sorting blog posts, but doing THIS in the application leads to a very inefficient process. This kind of work is what makes relational databases different. The database has indexes that allow it to perform queries and sorting in a much more efficient way. So I really want to create a single database query that defines the information I want to get, and then let the database know how to extract this information in the most efficient way.


Here is the query:


 class User(db.Model): #... def followed_posts(self): return Post.query.join( followers, (followers.c.followed_id == Post.user_id)).filter( followers.c.follower_id == self.id).order_by( Post.timestamp.desc()) 

This is perhaps the most complex query that I used in this application. I will try to decrypt this request at once. If you look at the structure of this query, you will notice that there are three main sections developed by the join() , filter() and order_by() methods of the SQLAlchemy query object:


 Post.query.join(...).filter(...).order_by(...) 

join operations - Joins


To understand what the join operation does, let's look at an example. Suppose I have a User table with the following contents:



For simplicity, I do not show all the fields in the user model, but only those that are important for this query.


Suppose that the followers association table says that the user john following the users susan and david , the user susan watching mary , and the user mary watching the david . The data that are above are:



As a result, the posts table returns one message from each user:


Here is the join() call that I defined for this query again:


Post.query.join(followers, (followers.c.followed_id == Post.user_id))


I invoke the join operation on the posts table. The first argument is the subscriber association table, and the second argument is the join condition. What I form with this challenge is that I want the database to create a temporary table that combines data from posts and subscribers tables. The data will be merged according to the condition I gave as an argument.


The condition I used says that the followed_id field of the follower table must be equal to the user_id the posts table. To perform this merge, the database takes each record from the message table (left side of the connection) and adds any records from the followers table (right side of the connection) that match the condition. If several followers matched, then the record will be repeated for each. If there are no matches for this message in followers, then this entry is not part of join.


The result of the merge operation:



Note that in all cases the user_id and followed_id columns are equal, since this is a join condition. The message from john is not displayed in the joined table, because there are no records in subscriptions that have john as an interesting user, or in other words, no one is tracking john messages. But records about david appear twice, because two different users follow this user.


It is not immediately immediately clear what I will receive by executing this request, but I continue, since this is just one part of a larger request.


filters


The join operation gave me a list of all the messages that a particular user is following, and this is more than the amount of data that I really want. I am only interested in a subset of this list, messages that are monitored by only one user, so I need to truncate all the entries I don’t need and I can do this by calling filter() .


Here is part of the query filter:


 filter(followers.c.follower_id == self.id) 

Since this query is in the method of the User class, the expression self.id refers to the user id of the user of interest. The filter() call selects items in the joined table for which the follower_id column points to this user, which, in other words, means that I only save records in which this user is a subscriber.


Suppose that I am interested in the user john , whose id field is set to 1 . Here is the result of the query after filtering:



And these are exactly the posts that I wanted to see!


Remember that the query was sent to the Post class, so despite the fact that I received a temporary table created by the database as part of this query, the result will be the records included in this temporary table, without additional columns added by the join operation.


Sorting


The final step is to sort the results. The part of the request that says:


 order_by(Post.timestamp.desc()) 

Here I say I want the results to be sorted by timestamp messages in descending order. Under this condition, the first result will be the most recent blog post.


Merge your own messages and subscribed messages


The query I demonstrated in the followed_posts () function is extremely useful, but it has one limitation. People expect to see their own messages in their chronology with the signed ones, but it was not there. The request does not have this feature.


, . — , , , . , , , . , . , , . — , , union, .


, . follow_posts() , , :


 def followed_posts(self): followed = Post.query.join( followers, (followers.c.followed_id == Post.user_id)).filter( followers.c.follower_id == self.id) own = Post.query.filter_by(user_id=self.id) return followed.union(own).order_by(Post.timestamp.desc()) 

, followed , .


UnitTest User Model


, «» , , . , , , , . , , , , — , , .


Python unittest , . User tests.py :


 from datetime import datetime, timedelta import unittest from app import app, db from app.models import User, Post class UserModelCase(unittest.TestCase): def setUp(self): app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite://' db.create_all() def tearDown(self): db.session.remove() db.drop_all() def test_password_hashing(self): u = User(username='susan') u.set_password('cat') self.assertFalse(u.check_password('dog')) self.assertTrue(u.check_password('cat')) def test_avatar(self): u = User(username='john', email='john@example.com') self.assertEqual(u.avatar(128), ('https://www.gravatar.com/avatar/' 'd4c74594d841139328695756648b6bd6' '?d=identicon&s=128')) def test_follow(self): u1 = User(username='john', email='john@example.com') u2 = User(username='susan', email='susan@example.com') db.session.add(u1) db.session.add(u2) db.session.commit() self.assertEqual(u1.followed.all(), []) self.assertEqual(u1.followers.all(), []) u1.follow(u2) db.session.commit() self.assertTrue(u1.is_following(u2)) self.assertEqual(u1.followed.count(), 1) self.assertEqual(u1.followed.first().username, 'susan') self.assertEqual(u2.followers.count(), 1) self.assertEqual(u2.followers.first().username, 'john') u1.unfollow(u2) db.session.commit() self.assertFalse(u1.is_following(u2)) self.assertEqual(u1.followed.count(), 0) self.assertEqual(u2.followers.count(), 0) def test_follow_posts(self): # create four users u1 = User(username='john', email='john@example.com') u2 = User(username='susan', email='susan@example.com') u3 = User(username='mary', email='mary@example.com') u4 = User(username='david', email='david@example.com') db.session.add_all([u1, u2, u3, u4]) # create four posts now = datetime.utcnow() p1 = Post(body="post from john", author=u1, timestamp=now + timedelta(seconds=1)) p2 = Post(body="post from susan", author=u2, timestamp=now + timedelta(seconds=4)) p3 = Post(body="post from mary", author=u3, timestamp=now + timedelta(seconds=3)) p4 = Post(body="post from david", author=u4, timestamp=now + timedelta(seconds=2)) db.session.add_all([p1, p2, p3, p4]) db.session.commit() # setup the followers u1.follow(u2) # john follows susan u1.follow(u4) # john follows david u2.follow(u3) # susan follows mary u3.follow(u4) # mary follows david db.session.commit() # check the followed posts of each user f1 = u1.followed_posts().all() f2 = u2.followed_posts().all() f3 = u3.followed_posts().all() f4 = u4.followed_posts().all() self.assertEqual(f1, [p2, p4, p1]) self.assertEqual(f2, [p2, p3]) self.assertEqual(f3, [p3, p4]) self.assertEqual(f4, [p4]) if __name__ == '__main__': unittest.main(verbosity=2) 

, , . setUp() tearDown() — , . setUp() , , . sqlite:// SQLAlchemy SQLite . db.create_all() . , . , .


:


 (venv) $ python tests.py test_avatar (__main__.UserModelCase) ... ok test_follow (__main__.UserModelCase) ... ok test_follow_posts (__main__.UserModelCase) ... ok test_password_hashing (__main__.UserModelCase) ... ok ---------------------------------------------------------------------- Ran 4 tests in 0.494s OK 

, , , , . , , , .



, , , . , , , .


, :


 @app.route('/follow/<username>') @login_required def follow(username): user = User.query.filter_by(username=username).first() if user is None: flash('User {} not found.'.format(username)) return redirect(url_for('index')) if user == current_user: flash('You cannot follow yourself!') return redirect(url_for('user', username=username)) current_user.follow(user) db.session.commit() flash('You are following {}!'.format(username)) return redirect(url_for('user', username=username)) @app.route('/unfollow/<username>') @login_required def unfollow(username): user = User.query.filter_by(username=username).first() if user is None: flash('User {} not found.'.format(username)) return redirect(url_for('index')) if user == current_user: flash('You cannot unfollow yourself!') return redirect(url_for('user', username=username)) current_user.unfollow(user) db.session.commit() flash('You are not following {}.'.format(username)) return redirect(url_for('user', username=username)) 

, , , , .


, View , . :


  ... <h1>User: {{ user.username }}</h1> {% if user.about_me %}<p>{{ user.about_me }}</p>{% endif %} {% if user.last_seen %}<p>Last seen on: {{ user.last_seen }}</p>{% endif %} <p>{{ user.followers.count() }} followers, {{ user.followed.count() }} following.</p> {% if user == current_user %} <p><a href="{{ url_for('edit_profile') }}">Edit your profile</a></p> {% elif not current_user.is_following(user) %} <p><a href="{{ url_for('follow', username=user.username) }}">Follow</a></p> {% else %} <p><a href="{{ url_for('unfollow', username=user.username) }}">Unfollow</a></p> {% endif %} ... 

, , , . , "" (Edit), :








, . , , URL , , . , susan , http://localhost:5000/user/susan , . , , , .


c index , , , . , .


<<< previous next >>>


')

Source: https://habr.com/ru/post/347450/


All Articles