8

So I am trying to add a like button feature to my code that allows users to like specific posts. The likes will be linked to a logged in user and the number of likes will be shown. Implementing the front end won't be difficult but I am having a problem with the back end.

I am using this post here as a guide which does a follower system instead

This is what I have so far?

I have created a table for likes in models.py :

likers = db.Table('likers',
    db.Column('liker_id', db.Integer, db.ForeignKey('post.id')),
    db.Column('liked_id', db.Integer, db.ForeignKey('post.id'))
)

In my Models.py for my user class:

class User(db.Model, UserMixin):
#Code 
liked = db.relationship(
    'User', secondary=likers,
    primaryjoin=(likers.c.liker_id == id),
    secondaryjoin=(likers.c.liked_id == id),
    backref = db.backref('likers', lazy='dynamic'), lazy='dynamic')
def like(self, post):
    if not self.is_liking(post):
        self.liked.append(post)

def unlike(self, post):
    if self.is_liking(post):
        self.liked.remove(post)

def is_liking(self, post):
    return self.liked.filter(
        likers.c.liked_id == post.id).count() > 0

In my routes.py for my users blueprint I have:

@users.route("/like/<int:post_id>")
@login_required
def like(post_id):
 post = Post.query.get_or_404(post_id)
 current_user.like(post)
 db.session.commit()
 flash('Post has been liked')
 return redirect(url_for('posts.post', post_id=post.id))

@users.route("/unlike/<int:post_id>")
@login_required
def unlike(post_id):
 post = Post.query.get_or_404(post_id)
 current_user.unlike(post)
 db.session.commit()
 flash('Post has been unliked')
 return redirect(url_for('posts.post', post_id=post.id))    

What am I doing wrong? I keep getting errors such as:

builtins.KeyError
KeyError: 'likers'

I have done a comment section and I know the relationship for the likes will be similar to the comments but I am struggling to implement it. I am relatively new to flask and I have tried using the documentations but haven't found anything to help me...

This is my final hope.

Patrick Yoder
  • 1,065
  • 4
  • 14
  • 19
  • On your 'likers' table your foreign key for liker_id should refer to user.id, not post.id. Posts don't like posts, users like posts. –  Oct 06 '18 at 04:55
  • @onosendi i have done this but i am still getting problems –  Oct 06 '18 at 12:11

3 Answers3

17
class User(UserMixin, db.Model):
    # Code
    liked = db.relationship(
        'PostLike',
        foreign_keys='PostLike.user_id',
        backref='user', lazy='dynamic')

    def like_post(self, post):
        if not self.has_liked_post(post):
            like = PostLike(user_id=self.id, post_id=post.id)
            db.session.add(like)

    def unlike_post(self, post):
        if self.has_liked_post(post):
            PostLike.query.filter_by(
                user_id=self.id,
                post_id=post.id).delete()

    def has_liked_post(self, post):
        return PostLike.query.filter(
            PostLike.user_id == self.id,
            PostLike.post_id == post.id).count() > 0


class PostLike(db.Model):
    __tablename__ = 'post_like'
    id = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
    post_id = db.Column(db.Integer, db.ForeignKey('post.id'))


@app.route('/like/<int:post_id>/<action>')
@login_required
def like_action(post_id, action):
    post = Post.query.filter_by(id=post_id).first_or_404()
    if action == 'like':
        current_user.like_post(post)
        db.session.commit()
    if action == 'unlike':
        current_user.unlike_post(post)
        db.session.commit()
    return redirect(request.referrer)

Then when you're listing your posts, set your anchors something like this:

{% for post in posts %}
  {% if current_user.has_liked_post(post) %}
    <a href="{{ url_for('like_action', post_id=post.id, action='unlike') }}">Unlike</a>
  {% else %}
    <a href="{{ url_for('like_action', post_id=post.id, action='like') }}">Like</a>
  {% endif %}
{% endfor %}

Let's assume your Post model looks something like this:

class Post(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    body = db.Column(db.Text)
    author_id = db.Column(db.Integer, db.ForeignKey('user.id'))
    recipient_id = db.Column(db.Integer, db.ForeignKey('user.id'))

    likes = db.relationship('PostLike', backref='post', lazy='dynamic')

You'd use:

p = Post.query.filter_by(id=1).first()
p.likes.count()

Or, you'd use this in your .html file:

{% for post in posts %}
  {% if current_user.has_liked_post(post) %}
    <a href="{{ url_for('like_action', post_id=post.id, action='unlike') }}">Unlike</a>
  {% else %}
    <a href="{{ url_for('like_action', post_id=post.id, action='like') }}">Like</a>
  {% endif %}
  {{ post.likes.count() }} likes
{% endfor %}
0

Your error indicates that the user instance does not have a likers relationship properly defined.

I suspect there are several errors here:

  1. Your likers table should have the foreign key for liker_id pointing at the users table, not posts.
  2. Your liked relationship in the User model should be a relationship with the Post model (with the likers table as secondary) and not be in a relationship with itself (ie the User model). Try this for your relationship:

    liked = db.relationship(
    'Post', secondary="likers",
    primaryjoin="likers.liker_id == users.id",
    secondaryjoin="likers.liked_id == posts.id",
    backref=db.backref('likers', lazy='dynamic'), lazy='dynamic')
    
  3. Your is_liking() method seems strange. I would write it as either:

    (if not expecting many liked posts per user) return post in self.liked

    (if expecting many liked posts per user) return db.session.execute("SELECT COUNT(*) FROM likers WHERE liked_id = :post_id AND liker_id = :user_id", {'user_id': self.id, 'post_id': post.id}).fetchone()[0] > 0

  4. (not related but possibly incorrect) The order that your User model inherits from db.Model and UserMixin matters, currently your UserMixin is only used for methods which are not found in db.Model and will not override anything (maybe that's what you want). Ref: Python's Method Resolution Order (MRO)

Michael Cho
  • 746
  • 7
  • 14
  • i know have this problem `builtins.AttributeError AttributeError: 'Table' object has no attribute 'liker_id'` –  Oct 05 '18 at 16:17
0

Above code works but logout of user maker error because of the "current_user" . So our html file must be like this

{% for post in posts %}
    {% if current_user.is_authenticated %}
        {% if current_user.has_liked_post(post) %}
            <a href="{{ url_for('like_action', post_id=post.id, action='unlike') }}">Unlike</a>
        {% else %}
            <a href="{{ url_for('like_action', post_id=post.id, action='like') }}">Like</a>
        {% endif %}
        {{ post.likes.count() }} likes
    {% else %}
        {{ post.likes.count() }} likes
    {% endif %}{% endfor %}