From d81f038422b1a9e764ac8f348211f305b36a7e52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Monta=C3=B1ana?= Date: Sun, 4 Jun 2023 01:00:41 +0200 Subject: [PATCH] Chapeter 8 --- app.db | Bin 32768 -> 36864 bytes app/__init__.py | 5 +- app/forms.py | 4 + app/models.py | 36 ++++++ app/routes.py | 42 ++++++- app/templates/user.html | 15 +++ migrations/versions/619062c0486b_followers.py | 33 +++++ tests.py | 114 ++++++++++++++++++ 8 files changed, 245 insertions(+), 4 deletions(-) create mode 100644 migrations/versions/619062c0486b_followers.py create mode 100644 tests.py diff --git a/app.db b/app.db index c0257ca4134944509fe59ae025f66218442df6ff..dfff9758a6037e052ca6439419332a8ada4f5de6 100644 GIT binary patch delta 340 zcmZo@U}{*vG(lQWhk=2C6Nq7ebE1y1tPX=-+AChZYz7W)Q3ifq{+-zu0c8qaN!g*VK@IES5J381#j0#4XDnPc<;;x%ob~mOf delta 120 zcmZozz|_#dG(lQWgMop81BhXOW1^0+v<8D-+AChZYzB7j`waZN{5!et^GWj_;$`Gn zvsq9em3#AjZZQ#V3xgD6Lql^Dvs8;s3SaqA_y#%r{D!7hrbbpK=6a?E24;pPo2%_x F9RTFq8>#>R diff --git a/app/__init__.py b/app/__init__.py index 3f12961..211341f 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -2,12 +2,10 @@ import os from flask import Flask from config import Config import logging -from loggin.handlers import RotatingFileHandler -from logging.handlers import SMTPHandler +from logging.handlers import RotatingFileHandler, SMTPHandler from flask_sqlalchemy import SQLAlchemy from flask_migrate import Migrate from flask_login import LoginManager -from app import routes, models, errors app = Flask(__name__) app.config.from_object(Config) @@ -54,3 +52,4 @@ if not app.debug: app.logger.setLevel(logging.INFO) app.logger.info("Microblog startup") +from app import routes, models, errors diff --git a/app/forms.py b/app/forms.py index f62d31a..6668c52 100644 --- a/app/forms.py +++ b/app/forms.py @@ -57,3 +57,7 @@ class EditProfileForm(FlaskForm): user = User.query.filter_by(username=self.username.data).first() if user is not None: raise ValidationError("Please use a different username.") + + +class EmptyForm(FlaskForm): + submit = SubmitField("Submit") diff --git a/app/models.py b/app/models.py index 8b253ae..bcb6253 100644 --- a/app/models.py +++ b/app/models.py @@ -10,6 +10,13 @@ def load_user(id): return User.query.get(int(id)) +followers = db.Table( + "followers", + db.Column("follower_id", db.Integer, db.ForeignKey("user.id")), + db.Column("followed_id", db.Integer, db.ForeignKey("user.id")), +) + + class User(UserMixin, db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(64), index=True, unique=True) @@ -18,6 +25,14 @@ class User(UserMixin, db.Model): posts = db.relationship("Post", backref="author", lazy="dynamic") about_me = db.Column(db.String(140)) last_seen = db.Column(db.DateTime, default=datetime.utcnow) + 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", + ) def __repr__(self): return "".format(self.username) @@ -34,6 +49,27 @@ class User(UserMixin, db.Model): digest, size ) + 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 + ) + + 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()) + class Post(db.Model): id = db.Column(db.Integer, primary_key=True) diff --git a/app/routes.py b/app/routes.py index 1272572..5526ada 100644 --- a/app/routes.py +++ b/app/routes.py @@ -3,7 +3,7 @@ from flask import render_template, flash, redirect, url_for, request from flask_login import current_user, login_user, logout_user, login_required from werkzeug.urls import url_parse from app import app, db -from app.forms import LoginForm, RegistrationForm, EditProfileForm +from app.forms import LoginForm, RegistrationForm, EditProfileForm, EmptyForm from app.models import User @@ -94,3 +94,43 @@ def edit_profile(): return render_template( "edit_profile.html", title="Edit Profile", form=form ) + + +@app.route("/follow/", methods=["POST"]) +@login_required +def follow(username): + form = EmptyForm() + if form.validate_on_submit(): + 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)) + else: + return redirect(url_for("index")) + + +@app.route("/unfollow/", methods=["POST"]) +@login_required +def unfollow(username): + form = EmptyForm() + if form.validate_on_submit(): + 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)) + else: + return redirect(url_for("index")) diff --git a/app/templates/user.html b/app/templates/user.html index 29b529a..89e2471 100644 --- a/app/templates/user.html +++ b/app/templates/user.html @@ -17,10 +17,25 @@ +

{{ user.followers.count() }} followers, {{ user.followed.count() }} following.

{% if user == current_user %}

Edit your profile

+ {% elif not current_user.is_following(user) %} +

+

+ {{ form.hidden_tag() }} + {{ form.submit(value = 'Follow') }} +
+

+ {% else %} +

+

+ {{ form.hidden_tag() }} + {{ form.submit(value = 'Unfollow') }} +
+

{% endif %}
{% for post in posts %} diff --git a/migrations/versions/619062c0486b_followers.py b/migrations/versions/619062c0486b_followers.py new file mode 100644 index 0000000..c51638b --- /dev/null +++ b/migrations/versions/619062c0486b_followers.py @@ -0,0 +1,33 @@ +"""followers + +Revision ID: 619062c0486b +Revises: 680d311746e8 +Create Date: 2023-06-03 21:28:40.382514 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '619062c0486b' +down_revision = '680d311746e8' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('followers', + sa.Column('follower_id', sa.Integer(), nullable=True), + sa.Column('followed_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['followed_id'], ['user.id'], ), + sa.ForeignKeyConstraint(['follower_id'], ['user.id'], ) + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('followers') + # ### end Alembic commands ### diff --git a/tests.py b/tests.py new file mode 100644 index 0000000..4dd04e8 --- /dev/null +++ b/tests.py @@ -0,0 +1,114 @@ +import os + +os.environ["DATABASE_URL"] = "sqlite://" + +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): + self.app_context = app.app_context() + self.app_context.push() + db.create_all() + + def tearDown(self): + db.session.remove() + db.drop_all() + self.app_context.pop() + + 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)