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) %}
+
+
+
+ {% else %}
+
+
+
{% 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)