Skip to content

Commit

Permalink
Add icons for reactions and templatetags to show them
Browse files Browse the repository at this point in the history
Add svg icons for like and dislike
- fill them if the logged in user has liked or disliked a comment
- add templatetags that make this feature possible

Add post save signal to Comment model
- Reaction instance is created when a new comment is created.

Changes in ReactionInstance model
- change related name for user attribute -> 'reactions' seems more natural.
  • Loading branch information
abhiabhi94 committed May 12, 2020
1 parent 96e9f0a commit 334dea9
Show file tree
Hide file tree
Showing 6 changed files with 84 additions and 18 deletions.
2 changes: 1 addition & 1 deletion comment/manager/reactions.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
"""Manager for Reaction Model"""
from django.db import models


class ReactionManager(models.Manager):
"""
Manager to optimize SQL queries for reactions.
"""

def get_queryset(self):
return super().get_queryset().select_related('comment')
4 changes: 2 additions & 2 deletions comment/migrations/0004_reaction_reactioninstance.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by Django 3.0.6 on 2020-05-11 22:35
# Generated by Django 3.0.6 on 2020-05-12 18:09

from django.conf import settings
from django.db import migrations, models
Expand Down Expand Up @@ -29,7 +29,7 @@ class Migration(migrations.Migration):
('reaction_type', models.SmallIntegerField(choices=[(1, 'LIKE'), (2, 'DISLIKE')])),
('date_reacted', models.DateTimeField(auto_now=True)),
('reaction', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reactions', to='comment.Reaction')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='users', to=settings.AUTH_USER_MODEL)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reactions', to=settings.AUTH_USER_MODEL)),
],
options={
'unique_together': {('user', 'reaction')},
Expand Down
11 changes: 10 additions & 1 deletion comment/models/comments.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericForeignKey
from django.db import models
from django.db.models.signals import post_save
from django.dispatch import receiver

from comment.manager import CommentManager, ReactionManager

from comment.manager import CommentManager

class Comment(models.Model):
user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE, default=None)
Expand Down Expand Up @@ -57,3 +60,9 @@ def likes(self):
@property
def dislikes(self):
return self._get_reaction_count('dislikes')

@receiver(post_save, sender=Comment)
def add_reaction(sender, instance, created, raw, using, update_fields, **kwargs):
"""Add a Reaction instance when a comment is created"""
if created:
instance.reactions.create(comment=instance)
16 changes: 7 additions & 9 deletions comment/models/reactions.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
from enum import IntEnum, unique

from django.conf import settings
from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
from django.db import models
from django.db.models.signals import post_delete
from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
Expand All @@ -14,7 +13,6 @@


class Reaction(models.Model):

comment = models.ForeignKey(Comment, related_name='reactions', on_delete=models.CASCADE)
likes = models.PositiveIntegerField(default=0)
dislikes = models.PositiveIntegerField(default=0)
Expand Down Expand Up @@ -49,7 +47,7 @@ def _increase_reaction_count(self, reaction):
Increase reaction count(likes/dislikes)
Args:
reaction ([int]): The integral value that matches to the reaction value in
reaction (int): The integral value that matches to the reaction value in
the database
Returns:
Expand All @@ -65,7 +63,7 @@ def _decrease_reaction_count(self, reaction):
Decrease reaction count(likes/dislikes)
Args:
reaction ([int]): The integral value that matches to the reaction value in
reaction (int): The integral value that matches to the reaction value in
the database
Returns:
Expand All @@ -78,16 +76,16 @@ def _decrease_reaction_count(self, reaction):


class ReactionInstance(models.Model):

@unique
class ReactionType(IntEnum):
LIKE = 1
DISLIKE = 2

reaction_choices = [(r.value, r.name) for r in ReactionType]
reaction_choices = [(r.value, r.name) for r in ReactionType]

reaction = models.ForeignKey(Reaction, related_name='reactions', on_delete=models.CASCADE)
user = models.ForeignKey(get_user_model(), related_name='users', on_delete=models.CASCADE)
user = models.ForeignKey(get_user_model(), related_name='reactions', on_delete=models.CASCADE)
reaction_type = models.SmallIntegerField(choices=reaction_choices)
date_reacted = models.DateTimeField(auto_now=timezone.now())

Expand Down
23 changes: 19 additions & 4 deletions comment/templates/comment/content.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{% load comment_tags %}
{% load static %}
{% load i18n %}
<div class="{% block text_space %}{% if has_valid_profile %}col-10 col-md-11
{% else %}co-12 mx-3{% endif %}{% endblock text_space %}">
Expand All @@ -21,10 +22,24 @@
Repl{% if obj.replies.count <= 1 %}y{% else %}ies{% endif %}
</a>
{% endif %}
| <span class="js-reply-number text-dark">{{ obj.likes }}</span>
<small class="js-comment-like {% block like_link_cls %} btn btn-link" {% endblock like_link_cls %} href="{% url 'comment:react' comment_id=obj.id reaction='like' %}">{% trans "Like" %}</small>
| <span class="js-reply-number text-dark">{{ obj.dislikes }}</span>
<small class="js-comment-dislike {% block dislike_link_cls %} btn btn-link" {% endblock dislike_link_cls %} href="{% url 'comment:react' comment_id=obj.id reaction='dislike' %}">{% trans "Dislike" %}</small>
</small>
<div class="js-comment-reactions mt-2">
<span class="js-reply-number text-dark">{{ obj.likes }}</span>
<a class="js-comment-like {% block like_link_cls %}" {% endblock like_link_cls %} href="{% url 'comment:react' comment_id=obj.id reaction='like' %}">
{% block like_img_icon %}
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill={% if obj|add_one_arg:user|has_reacted:"like" %}"#ffc966"{% else %}"none"{% endif %} stroke="#427297" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" class="feather feather-thumbs-up">
<path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"></path>
</svg>
{% endblock like_img_icon %}
</a>
<span class="ml-4 js-reply-number text-dark">{{ obj.dislikes }}</span>
<a class="js-comment-dislike {% block dislike_link_cls %}" {% endblock dislike_link_cls %} href="{% url 'comment:react' comment_id=obj.id reaction='dislike' %}">
{% block dislike_img_icon %}
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill={% if obj|add_one_arg:user|has_reacted:"dislike" %}"#ffc966"{% else %}"none"{% endif %} stroke="#427297" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" class="feather feather-thumbs-down">
<path d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17"></path>
</svg>
{% endblock dislike_img_icon %}
</a>
</div>
</footer>
</div>
46 changes: 45 additions & 1 deletion comment/templatetags/comment_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.contrib.contenttypes.models import ContentType

from comment.models import Comment
from comment.models import Comment, ReactionInstance
from comment.forms import CommentForm
from comment.utils import has_valid_profile

Expand Down Expand Up @@ -140,3 +140,47 @@ def include_bootstrap():
def render_field(field, **kwargs):
field.field.widget.attrs.update(kwargs)
return field

@register.filter(name='add_one_arg')
def add_one_arg(arg1, arg2):
"""
Use this filter to when you need to pass more than 2 arguments to your filter.
Args:
arg1 (`Any`): First Argument
arg2 (`Any`): Second Argument
Returns:
tuple: a tuple containing both the arguments in the order that they were passed.
"""
return arg1, arg2

@register.filter(name='has_reacted')
def has_reacted(comment_and_user, reaction):
"""
Returns whether a user has reacted with a particular reaction on a comment or not.
Args:
comment_and_user : tuple
arg1(comment:comment): comment to be queried about.
arg2(user:User): user to be queried about.
reaction (str): reaction to be queried about.
Returns:
bool
"""
comment, user = comment_and_user
if user.is_authenticated:
reaction_type = getattr(ReactionInstance.ReactionType, reaction.upper(), None)
if not reaction_type:
raise template.TemplateSyntaxError(
'Reaction must be an valid ReactionManager.RelationType. {} is not'.format(reaction)
)
return ReactionInstance.objects.filter(
user=user,
reaction_type=reaction_type.value,
reaction__comment=comment
).exists()

return False

0 comments on commit 334dea9

Please sign in to comment.