Skip to content

Commit

Permalink
Merge 5ff2233 into 4078394
Browse files Browse the repository at this point in the history
  • Loading branch information
IssaIan committed May 10, 2019
2 parents 4078394 + 5ff2233 commit 443ce21
Show file tree
Hide file tree
Showing 39 changed files with 961 additions and 160 deletions.
File renamed without changes.
3 changes: 3 additions & 0 deletions authors/apps/appnotifications/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from django.contrib import admin

# Register your models here.
22 changes: 22 additions & 0 deletions authors/apps/appnotifications/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Generated by Django 2.2 on 2019-05-10 10:13

from django.db import migrations, models


class Migration(migrations.Migration):

initial = True

dependencies = [
]

operations = [
migrations.CreateModel(
name='UserNotification',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('email_notifications_subscription', models.BooleanField(default=True)),
('in_app_notifications_subscription', models.BooleanField(default=True)),
],
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 2.2 on 2019-05-10 10:13

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


class Migration(migrations.Migration):

initial = True

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('appnotifications', '0001_initial'),
]

operations = [
migrations.AddField(
model_name='usernotification',
name='user',
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='notification_preferences', to=settings.AUTH_USER_MODEL),
),
]
Empty file.
34 changes: 34 additions & 0 deletions authors/apps/appnotifications/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from django.conf import settings
from django.db import models
from django.db.models.signals import post_save
from django.dispatch import receiver

from authors.apps.authentication.models import User

from ...utils import notification_handlers


class UserNotification(models.Model):
"""
User Notification model stores user's preferences for notifications.
By default all users are "opted in" to notifications
"""

user = models.OneToOneField(User, on_delete=models.CASCADE, unique=True,
related_name='notification_preferences')
email_notifications_subscription = models.BooleanField(default=True)
in_app_notifications_subscription = models.BooleanField(default=True)


@receiver(post_save, sender=User)
def setup_notification_permissions(sender, **kwargs):
instance = kwargs.get('instance')
created = kwargs.get('created', False)

if created:
data = {
'user': instance,
'email_notifications_subscription': True,
'in_app_notifications_subscription': True
}
UserNotification.objects.create(**data)
33 changes: 33 additions & 0 deletions authors/apps/appnotifications/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from rest_framework import serializers
from notifications.models import Notification
from authors.apps.articles.models import Article
from authors.apps.authentication.models import User
from .models import UserNotification


class Subscription(serializers.ModelSerializer):
"""
serializer class for unsubscribing from either email or in-app
notifications
"""
class Meta:
model = UserNotification
fields = ('email_notifications_subscription',
'in_app_notifications_subscription')


class NotificationSerializer(serializers.ModelSerializer):
"""
serializer class for notification objects
"""
timestamp = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S")

class Meta:
model = Notification
fields = (
'id',
'unread',
'verb',
'timestamp',
'description'
)
Empty file.
17 changes: 17 additions & 0 deletions authors/apps/appnotifications/tests/basetest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from ...articles.tests.basetests import BaseTest
from rest_framework.reverse import reverse


class NotificationBaseTest(BaseTest):
"""
Base test case for testing notifications
"""

follow_url = reverse("profiles:follow", args=["adam"])
notification_url = reverse("notifications:all-notifications")
unread_notification_url = reverse("notifications:unread-notifications")
subscribe_unsubscribe_url = reverse("notifications:subscription")

def follow_user(self):
self.is_authenticated("jim@gmail.com", "@Us3r.com")
self.client.post(self.follow_url)
73 changes: 73 additions & 0 deletions authors/apps/appnotifications/tests/test_notifications.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
from rest_framework import status
from .basetest import NotificationBaseTest
from rest_framework.reverse import reverse


class TestNotifications(NotificationBaseTest):
"""
Class to test notifications
"""

def test_successful_notification_article(self):
"""
Test for successful notification of a user upon article creation
"""
self.follow_user()
self.create_article()
self.is_authenticated("jim@gmail.com", "@Us3r.com")
response = self.client.get(self.notification_url)
self.assertEqual(
response.data['message'], 'You have 1 notification(s)')
self.assertIn('notifications', str(response.data))
self.assertEqual(response.status_code, status.HTTP_200_OK)

def test_no_notifications(self):
"""
Test user having no notifications
"""
self.is_authenticated("jim@gmail.com", "@Us3r.com")
response = self.client.get(self.notification_url)
self.assertEqual(response.status_code, status.HTTP_200_OK)

def test_get_unread_notifications(self):
"""
Test retrieval of unread notifications
"""
self.follow_user()
self.create_article()
self.is_authenticated("jim@gmail.com", "@Us3r.com")
response = self.client.get(self.unread_notification_url)
self.assertIn('notifications', str(response.data))
self.assertEqual(response.status_code, status.HTTP_200_OK)

def test_subscribe_notififications(self):
"""
Test subscribing and unsubscribing of notifications
"""
self.is_authenticated("jim@gmail.com", "@Us3r.com")
response = self.client.patch(self.subscribe_unsubscribe_url, {
'email_notifications_subscription': 'false',
'in_app_notifications_subscription': 'false'})
self.assertEqual(response.status_code, status.HTTP_200_OK)

def test_unsubscribe_email_notification(self):
"""
Test unsubscribing from email notifications
"""
response = self.client.get(reverse("notifications:opt_out_link",
args=[self.get_token()]))
self.assertEqual(response.data['message'],
'You have unsubscribed from email notifications')
self.assertEqual(response.status_code, status.HTTP_200_OK)

def test_notification_from_comments(self):
"""
Test notifications from comments
"""
self.is_authenticated("jim@gmail.com", "@Us3r.com")
self.post_favorite()
self.is_authenticated("adam@gmail.com", "@Us3r.com")
self.post_comment()
self.is_authenticated("jim@gmail.com", "@Us3r.com")
response = self.client.get(self.notification_url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
27 changes: 27 additions & 0 deletions authors/apps/appnotifications/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from django.urls import path
from . import views

app_name = 'notifications'

urlpatterns = [
path(
'notifications/',
views.AllNotificationsAPIview.as_view(),
name="all-notifications"
),
path(
'notifications/unread/',
views.UnreadNotificationsAPIview.as_view(),
name="unread-notifications"
),
path(
'notifications/subscription/',
views.SubscribeUnsubscribeAPIView.as_view(),
name="subscription"
),
path(
'notifications/unsubscribe_email/<str:token>/',
views.UnsubscribeEmailAPIView.as_view(),
name="opt_out_link"
)
]
90 changes: 90 additions & 0 deletions authors/apps/appnotifications/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
from django.shortcuts import get_object_or_404
from django.utils import timezone
from notifications.models import Notification
from rest_framework import status
from rest_framework.authtoken.models import Token
from rest_framework.generics import (GenericAPIView, ListAPIView,
RetrieveUpdateAPIView)
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response

from authors.apps.appnotifications.models import UserNotification
from authors.apps.authentication.models import User

from .serializers import NotificationSerializer, Subscription


class SubscribeUnsubscribeAPIView(RetrieveUpdateAPIView):
"""
lets users to subscribe and unsubscribe to notifications.
"""
permission_classes = [IsAuthenticated]
serializer_class = Subscription

def update(self, request, *args, **kwargs):
user = UserNotification.objects.get(user=request.user)
serializer_data = request.data
serializer = self.serializer_class(
instance=user, data=serializer_data, partial=True)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)


class UnsubscribeEmailAPIView(GenericAPIView):
"""
lets users to unsubscribe from email notifications.
"""
serializer_class = Subscription

def get(self, request, *args, **kwargs):
token = kwargs['token']
token_object = Token.objects.get(key=token)
user = token_object.user
user = UserNotification.objects.get(user=user)
user.email_notifications_subscription = False
user.save()
resp = {
"message": "You have unsubscribed from email notifications"
}
return Response(data=resp, status=status.HTTP_200_OK)


class NotificationApiView(ListAPIView):
serializer_class = NotificationSerializer
permission_classes = (IsAuthenticated,)

def get(self, request, *args, **kwargs):
notifications = self.notifications(request)
serializer = self.serializer_class(
notifications, many=True
)
if notifications.count() == 0:
resp = {
"message": "You have no notifications"
}
else:
resp = {
"message": f"You have {notifications.count()} notification(s)",
"notifications": serializer.data
}
return Response(resp)


class AllNotificationsAPIview(NotificationApiView):
"""
list all user's notifications
"""
def notifications(self, request):
request.user.notifications.mark_all_as_read()
request.user.notifications.mark_as_sent()
return request.user.notifications


class UnreadNotificationsAPIview(NotificationApiView):
"""
list all user's unread notifications
"""
def notifications(self, request):
request.user.notifications.mark_as_sent()
return request.user.notifications.unread()
8 changes: 8 additions & 0 deletions authors/apps/articles/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from django.apps import AppConfig


class ArticlesConfig(AppConfig):
name = 'articles'

def ready(self):
from authors.utils import notification_handlers
16 changes: 14 additions & 2 deletions authors/apps/articles/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by Django 2.2 on 2019-05-10 08:25
# Generated by Django 2.2 on 2019-05-10 10:13

import autoslug.fields
import cloudinary.models
Expand Down Expand Up @@ -27,14 +27,26 @@ class Migration(migrations.Migration):
('body', models.TextField()),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('image', cloudinary.models.CloudinaryField(max_length=255, verbose_name='image')),
('favorited', models.BooleanField(default=False)),
('favoritesCount', models.PositiveSmallIntegerField(default=0)),
('image', cloudinary.models.CloudinaryField(max_length=255, verbose_name='image')),
],
options={
'ordering': ['created_at'],
},
),
migrations.CreateModel(
name='Comment',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('body', models.TextField()),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now_add=True)),
],
options={
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='Favorite',
fields=[
Expand Down
Loading

0 comments on commit 443ce21

Please sign in to comment.