Skip to content

Commit

Permalink
feat(create notifications): create notifications feature
Browse files Browse the repository at this point in the history
- create notitifcations app
- setup folder structure
- setup models

feat(create notifications): write tests for endpoints

- write tests for retrieving all notififcations
- write tests for opting in or out of notifications

feat(create notifications): create comment notifications

- setup comment notification model
- setup comment notification serializer
- setup comment notification views
- implement tests for the views
  • Loading branch information
zaabu committed Oct 21, 2018
1 parent 3204619 commit 8b1a373
Show file tree
Hide file tree
Showing 30 changed files with 2,279 additions and 52 deletions.
3 changes: 2 additions & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
[run]
omit =
*/settings/*
*/wsgi.py
*/wsgi.py
authors/apps/notifications/cron_job.py
22 changes: 18 additions & 4 deletions authors/apps/articles/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@
from rest_framework.permissions import (AllowAny, IsAdminUser, IsAuthenticated,
IsAuthenticatedOrReadOnly)
from rest_framework.response import Response

from taggit.models import Tag

from .exceptions import CatHasNoArticles, TagHasNoArticles
from .models import (Article, Bookmark, Category, LikeArticle, RateArticle,
Reported)
Expand All @@ -26,6 +24,12 @@
TagSerializer)
from .utils import shareArticleMail

from .exceptions import TagHasNoArticles, CatHasNoArticles
from .models import Article, LikeArticle, RateArticle, Category
from rest_framework import filters
from django.db.models.signals import post_save
from django.dispatch import receiver
from authors.apps.notifications.models import notify_follower

class TagListAPIView(generics.ListAPIView):
""" List all tags """
Expand Down Expand Up @@ -54,7 +58,7 @@ class CategoryListCreateAPIView(generics.ListCreateAPIView):
""" List / Create categories """

queryset = Category.objects.all()
permission_classes = (AllowAny,)
permission_classes = (IsAuthenticated,)
serializer_class = CategorySerializer
renderer_classes = (CategoryJSONRenderer,)

Expand All @@ -69,7 +73,7 @@ def create(self, request):

class CategoryRetrieveAPIView(generics.RetrieveAPIView):
""" Get articles under a specific category """
permission_classes = (AllowAny,)
permission_classes = (IsAuthenticated,)
serializer_class = ArticleSerializer

def retrieve(self, request, *args, **kwargs):
Expand Down Expand Up @@ -117,6 +121,16 @@ def get_queryset(self):
queryset = queryset.filter(title__icontains=title)
return queryset

@receiver(post_save, sender=Article)
def notify_follower_reciever(sender, instance, created, **kwargs):
"""
Send a notification after the article being created is saved.
"""
if created:
message = ("Author " + instance.author.username +
" has published an article. Title: " + instance.title)
notify_follower(instance.author, message, instance)


class ArticleAPIDetailsView(generics.RetrieveUpdateDestroyAPIView):
"""retreive, update and delete an article """
Expand Down
19 changes: 19 additions & 0 deletions authors/apps/comments/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@
CommentThreadJSONRenderer)
from .serializers import (CommentChildSerializer, CommentHistorySerializer,
CommentSerializer, LikeCommentSerializer)

from django.db.models.signals import post_save
from django.dispatch import receiver
from authors.apps.profiles.models import Profile
from authors.apps.notifications.models import notify_comment_follower


class CommentListCreateView(generics.ListCreateAPIView):
Expand Down Expand Up @@ -62,6 +67,20 @@ def get(self, request, *args, **kwargs):
serializer = self.serializer_class(comment, many=True)
return Response(serializer.data)

@receiver(post_save, sender=Comment)
def notify_follower_reciever(sender, instance, created, **kwargs):
"""
Send a notification after the article being created is saved.
"""
if created:
message = (instance.author.username +
" has commented on an article that you favorited.")
#import pdb;pdb.set_trace()

article_id=instance.slug.id

notify_comment_follower(article_id, message, instance)


class CommentsView(generics.RetrieveUpdateDestroyAPIView):
serializer_class = CommentSerializer
Expand Down
Empty file.
9 changes: 9 additions & 0 deletions authors/apps/notifications/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from django.contrib import admin
from .models import Notification, CommentNotification

# Register your models here.

admin.site.register(Notification)
admin.site.register(CommentNotification)


5 changes: 5 additions & 0 deletions authors/apps/notifications/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from django.apps import AppConfig


class NotificationsConfig(AppConfig):
name = 'notifications'
52 changes: 52 additions & 0 deletions authors/apps/notifications/cron_job.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from django_cron import CronJobBase, Schedule
from django.conf import settings
from django.template.loader import render_to_string
from django.core.mail import EmailMessage

from .models import Notification, CommentNotification


class EmailNotificationCron(CronJobBase):
"""Create the cron job for email sending."""

RUN_EVERY_MINS = settings.RUN_EVERY_MINS
schedule = Schedule(run_every_mins=RUN_EVERY_MINS)
code = 'authors.apps.notifications.cron_job.EmailNotificationCron'

def do(self):
"""Send emails to all the persons to be notified."""
subject = 'Authors Haven Notification'

article_recipients = []
notifications = Notification.objects.all()
message_template = "article_notification.html"
self.send_notification(
notifications, article_recipients, message_template, subject)

comment_recipients = []
notifications = CommentNotification.objects.all()
message_template = "comment_notification.html"
self.send_notification(
notifications, comment_recipients, message_template, subject)

def send_notification(self, notifications, recipients, message_template, subject):
for notification in notifications:
if not notification.email_sent:
self.get_recipients(notification, recipients)
content = {'notification': notification}
message = render_to_string(message_template, content)
mail = EmailMessage(
subject=subject,
body=message,
to=recipients,
from_email=settings.EMAIL_HOST_USER)
mail.content_subtype = "html"
mail.send(fail_silently=False)
notification.email_sent = True
notification.save()

def get_recipients(self, notification, recipients):
for user in notification.notified.all():
if (user not in notification.read.all()
and user.profile.email_notification_enabled):
recipients.append(user.email)
51 changes: 51 additions & 0 deletions authors/apps/notifications/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Generated by Django 2.1.1 on 2018-10-19 09:45

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

initial = True

dependencies = [
('comments', '0007_merge_20181017_1744'),
('articles', '0012_merge_20181019_1051'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.CreateModel(
name='CommentNotification',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('notification', models.TextField()),
('created_at', models.DateTimeField(auto_now_add=True)),
('classification', models.TextField(default='comment')),
('email_sent', models.BooleanField(default=False)),
('comment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='comments.Comment')),
('notified', models.ManyToManyField(blank=True, related_name='comment_notified', to=settings.AUTH_USER_MODEL)),
('read', models.ManyToManyField(blank=True, related_name='comment_read', to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='Notification',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('notification', models.TextField()),
('created_at', models.DateTimeField(auto_now_add=True)),
('classification', models.TextField(default='article')),
('email_sent', models.BooleanField(default=False)),
('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='articles.Article')),
('notified', models.ManyToManyField(blank=True, related_name='notified', to=settings.AUTH_USER_MODEL)),
('read', models.ManyToManyField(blank=True, related_name='read', to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['-created_at'],
},
),
]
Empty file.
100 changes: 100 additions & 0 deletions authors/apps/notifications/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
from django.db import models
from authors.apps.authentication.models import User
from authors.apps.profiles.models import Profile
from authors.apps.articles.models import Article
from authors.apps.comments.models import Comment
from authors.apps.profiles.models import FollowingUser


class Notification(models.Model):
"""
Defines fields for notifications.
"""

class Meta:
# Order notification by time notified
ordering = ['-created_at']

# article to send
article = models.ForeignKey(Article, on_delete=models.CASCADE)
# notification message
notification = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
# users to be notified
notified = models.ManyToManyField(
User, related_name='notified', blank=True)
# users that have read
read = models.ManyToManyField(User, related_name='read', blank=True)
classification = models.TextField(default="article")
# check whether email has been sent
email_sent = models.BooleanField(default=False)

def __str__(self):
"Returns a string representation of notification."
return self.notification


def notify_follower(author, notification, article):
"""
Function that adds a notification to the Notification model.
in order to add them to the notified column of the notification.
"""
created_notification = Notification.objects.create(
notification=notification, classification="article", article=article)

userlist = FollowingUser.objects.filter(
followed_user=author).values_list(
'following_user', flat=True)
followers = User.objects.filter(id__in=userlist)

for follower in followers:
# checks if notification is set to True
if follower.profile.app_notification_enabled is True:
created_notification.notified.add(follower.id)
created_notification.save()


class CommentNotification(models.Model):
"""
Defines fields for notifications.
"""

class Meta:
# Order notification by time notified
ordering = ['-created_at']

# article to send
comment = models.ForeignKey(Comment, on_delete=models.CASCADE)
# notification message
notification = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
# users to be notified
notified = models.ManyToManyField(
User, related_name='comment_notified', blank=True)
# users that have read
read = models.ManyToManyField(
User, related_name='comment_read', blank=True)
classification = models.TextField(default="comment")
# check whether email has been sent
email_sent = models.BooleanField(default=False)

def __str__(self):
"Returns a string representation of notification."
return self.notification


def notify_comment_follower(article_id, notification, comment):
"""
Function that adds a notification to the Notification model.
in order to add them to the notified column of the notification.
"""
created_notification = CommentNotification.objects.create(
notification=notification, classification="comment", comment=comment)

followers = Profile.objects.filter(favorites=article_id)

for follower in followers:
# checks if notification is set to True
if follower.app_notification_enabled is True:
created_notification.notified.add(follower.id)
created_notification.save()
25 changes: 25 additions & 0 deletions authors/apps/notifications/renderers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import json

from rest_framework.renderers import JSONRenderer


class NotificationJSONRenderer(JSONRenderer):
charset = 'utf-8'

def render(self, data, media_type=None, renderer_context=None):
"""
Check for errors key in data
"""
errors = data.get('errors', None)

if errors:
"""
We will let the default JSONRenderer handle
rendering errors.
"""
return super(NotificationJSONRenderer, self).render(data)

# Finally, we can render our data under the "profile" namespace.
return json.dumps({
'notification': data
})
Loading

0 comments on commit 8b1a373

Please sign in to comment.