feat: Added notification APIs (#32232)
* feat: added api to get notifications for users * feat: added api to get count of notifications * feat: added api to mark notifications as seen * feat: added index on app_name in notification table * refactor: updated api view parent and url * refactor: resolved linter issue
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.19 on 2023-05-12 14:14
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('notifications', '0002_notificationpreference'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='notification',
|
||||
name='app_name',
|
||||
field=models.CharField(choices=[('DISCUSSION', 'Discussion')], db_index=True, max_length=64),
|
||||
),
|
||||
]
|
||||
@@ -48,7 +48,7 @@ class Notification(TimeStampedModel):
|
||||
.. no_pii:
|
||||
"""
|
||||
user = models.ForeignKey(User, related_name="notifications", on_delete=models.CASCADE)
|
||||
app_name = models.CharField(max_length=64, choices=NotificationApplication.choices)
|
||||
app_name = models.CharField(max_length=64, choices=NotificationApplication.choices, db_index=True)
|
||||
notification_type = models.CharField(max_length=64, choices=NotificationType.choices)
|
||||
content = models.CharField(max_length=1024)
|
||||
content_context = models.JSONField(default={})
|
||||
|
||||
@@ -5,7 +5,7 @@ from rest_framework import serializers
|
||||
|
||||
from common.djangoapps.student.models import CourseEnrollment
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
from openedx.core.djangoapps.notifications.models import NotificationPreference
|
||||
from openedx.core.djangoapps.notifications.models import Notification, NotificationPreference
|
||||
|
||||
|
||||
class CourseOverviewSerializer(serializers.ModelSerializer):
|
||||
@@ -52,3 +52,22 @@ class UserNotificationPreferenceSerializer(serializers.ModelSerializer):
|
||||
setattr(instance, key, val)
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
|
||||
class NotificationSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Serializer for the Notification model.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = Notification
|
||||
fields = (
|
||||
'id',
|
||||
'app_name',
|
||||
'notification_type',
|
||||
'content',
|
||||
'content_context',
|
||||
'content_url',
|
||||
'last_read',
|
||||
'last_seen',
|
||||
)
|
||||
|
||||
@@ -7,13 +7,13 @@ from django.dispatch import Signal
|
||||
from django.urls import reverse
|
||||
from edx_toggles.toggles.testutils import override_waffle_flag
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APIClient
|
||||
from rest_framework.test import APIClient, APITestCase
|
||||
|
||||
from common.djangoapps.student.models import CourseEnrollment
|
||||
from common.djangoapps.student.tests.factories import UserFactory
|
||||
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
|
||||
from openedx.core.djangoapps.notifications.config.waffle import ENABLE_NOTIFICATIONS
|
||||
from openedx.core.djangoapps.notifications.models import NotificationPreference
|
||||
from openedx.core.djangoapps.notifications.models import Notification, NotificationPreference
|
||||
from openedx.core.djangoapps.notifications.serializers import NotificationCourseEnrollmentSerializer
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
@@ -217,3 +217,159 @@ class UserNotificationPreferenceAPITest(ModuleStoreTestCase):
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
expected_data = self._expected_api_response(overrides=updated_notification_config_data)
|
||||
self.assertEqual(response.data, expected_data)
|
||||
|
||||
|
||||
class NotificationListAPIViewTest(APITestCase):
|
||||
"""
|
||||
Tests suit for the NotificationListAPIView.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.user = self.user = UserFactory()
|
||||
self.url = reverse('notifications-list')
|
||||
|
||||
def test_list_notifications(self):
|
||||
"""
|
||||
Test that the view can list notifications.
|
||||
"""
|
||||
# Create a notification for the user.
|
||||
Notification.objects.create(
|
||||
user=self.user,
|
||||
app_name='app1',
|
||||
notification_type='info',
|
||||
content='This is a notification.',
|
||||
)
|
||||
self.client.login(username=self.user.username, password='test')
|
||||
|
||||
# Make a request to the view.
|
||||
response = self.client.get(self.url)
|
||||
|
||||
# Assert that the response is successful.
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = response.data['results']
|
||||
# Assert that the response contains the notification.
|
||||
self.assertEqual(len(data), 1)
|
||||
self.assertEqual(data[0]['app_name'], 'app1')
|
||||
self.assertEqual(data[0]['notification_type'], 'info')
|
||||
self.assertEqual(data[0]['content'], 'This is a notification.')
|
||||
|
||||
def test_list_notifications_with_app_name_filter(self):
|
||||
"""
|
||||
Test that the view can filter notifications by app name.
|
||||
"""
|
||||
# Create two notifications for the user, one for each app name.
|
||||
Notification.objects.create(
|
||||
user=self.user,
|
||||
app_name='app1',
|
||||
notification_type='info',
|
||||
content='This is a notification for app1.',
|
||||
)
|
||||
Notification.objects.create(
|
||||
user=self.user,
|
||||
app_name='app2',
|
||||
notification_type='info',
|
||||
content='This is a notification for app2.',
|
||||
)
|
||||
self.client.login(username=self.user.username, password='test')
|
||||
|
||||
# Make a request to the view with the app_name query parameter set to 'app1'.
|
||||
response = self.client.get(self.url + "?app_name=app1")
|
||||
|
||||
# Assert that the response is successful.
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Assert that the response contains only the notification for app1.
|
||||
data = response.data['results']
|
||||
self.assertEqual(len(data), 1)
|
||||
self.assertEqual(data[0]['app_name'], 'app1')
|
||||
self.assertEqual(data[0]['notification_type'], 'info')
|
||||
self.assertEqual(data[0]['content'], 'This is a notification for app1.')
|
||||
|
||||
def test_list_notifications_without_authentication(self):
|
||||
"""
|
||||
Test that the view returns 403 if the user is not authenticated.
|
||||
"""
|
||||
# Make a request to the view without authenticating.
|
||||
response = self.client.get(self.url)
|
||||
|
||||
# Assert that the response is unauthorized.
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
|
||||
class NotificationCountViewSetTestCase(APITestCase):
|
||||
"""
|
||||
Tests for the NotificationCountViewSet.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
# Create a user.
|
||||
self.user = UserFactory()
|
||||
self.url = reverse('notifications-count')
|
||||
# Create some notifications for the user.
|
||||
Notification.objects.create(user=self.user, app_name='App Name 1', notification_type='Type A')
|
||||
Notification.objects.create(user=self.user, app_name='App Name 1', notification_type='Type B')
|
||||
Notification.objects.create(user=self.user, app_name='App Name 2', notification_type='Type A')
|
||||
Notification.objects.create(user=self.user, app_name='App Name 3', notification_type='Type C')
|
||||
|
||||
def test_get_unseen_notifications_count(self):
|
||||
"""
|
||||
Test that the endpoint returns the correct count of unseen notifications.
|
||||
"""
|
||||
self.client.login(username=self.user.username, password='test')
|
||||
response = self.client.get(self.url)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.data['count'], 4)
|
||||
self.assertEqual(response.data['count_by_app_name'], {'App Name 1': 2, 'App Name 2': 1, 'App Name 3': 1})
|
||||
|
||||
def test_get_unseen_notifications_count_for_unauthenticated_user(self):
|
||||
"""
|
||||
Test that the endpoint returns 403 for an unauthenticated user.
|
||||
"""
|
||||
response = self.client.get(self.url)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_get_unseen_notifications_count_for_user_with_no_notifications(self):
|
||||
"""
|
||||
Test that the endpoint returns 0 for a user with no notifications.
|
||||
"""
|
||||
# Create a user with no notifications.
|
||||
user = UserFactory()
|
||||
self.client.login(username=user.username, password='test')
|
||||
response = self.client.get(self.url)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.data['count'], 0)
|
||||
self.assertEqual(response.data['count_by_app_name'], {})
|
||||
|
||||
|
||||
class MarkNotificationsUnseenAPIViewTestCase(APITestCase):
|
||||
"""
|
||||
Tests for the MarkNotificationsUnseenAPIView.
|
||||
"""
|
||||
def setUp(self):
|
||||
self.user = UserFactory()
|
||||
|
||||
# Create some sample notifications for the user
|
||||
Notification.objects.create(user=self.user, app_name='App Name 1', notification_type='Type A')
|
||||
Notification.objects.create(user=self.user, app_name='App Name 1', notification_type='Type B')
|
||||
Notification.objects.create(user=self.user, app_name='App Name 2', notification_type='Type A')
|
||||
Notification.objects.create(user=self.user, app_name='App Name 3', notification_type='Type C')
|
||||
|
||||
def test_mark_notifications_unseen(self):
|
||||
# Create a POST request to mark notifications as unseen for 'App Name 1'
|
||||
app_name = 'App Name 1'
|
||||
url = reverse('mark-notifications-unseen', kwargs={'app_name': app_name})
|
||||
self.client.login(username=self.user.username, password='test')
|
||||
response = self.client.put(url)
|
||||
# Assert the response status code is 200 (OK)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
# Assert the response data contains the expected message
|
||||
expected_data = {'message': 'Notifications marked unseen.'}
|
||||
self.assertEqual(response.data, expected_data)
|
||||
|
||||
# Assert the notifications for 'App Name 1' are marked as unseen for the user
|
||||
notifications = Notification.objects.filter(user=self.user, app_name=app_name, last_seen__isnull=False)
|
||||
self.assertEqual(notifications.count(), 2)
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
"""
|
||||
URLs for the notifications API.
|
||||
"""
|
||||
from django.urls import path
|
||||
from django.urls import re_path
|
||||
from django.conf import settings
|
||||
from django.urls import path, re_path
|
||||
from rest_framework import routers
|
||||
from .views import CourseEnrollmentListView, UserNotificationPreferenceView
|
||||
|
||||
from .views import (
|
||||
CourseEnrollmentListView,
|
||||
MarkNotificationsUnseenAPIView,
|
||||
NotificationCountView,
|
||||
NotificationListAPIView,
|
||||
UserNotificationPreferenceView
|
||||
)
|
||||
|
||||
router = routers.DefaultRouter()
|
||||
|
||||
@@ -17,6 +23,14 @@ urlpatterns = [
|
||||
UserNotificationPreferenceView.as_view(),
|
||||
name='notification-preferences'
|
||||
),
|
||||
path('', NotificationListAPIView.as_view(), name='notifications-list'),
|
||||
path('count/', NotificationCountView.as_view(), name='notifications-count'),
|
||||
path(
|
||||
'mark-notifications-unseen/<app_name>/',
|
||||
MarkNotificationsUnseenAPIView.as_view(),
|
||||
name='mark-notifications-unseen'
|
||||
),
|
||||
|
||||
]
|
||||
|
||||
urlpatterns += router.urls
|
||||
|
||||
@@ -1,16 +1,25 @@
|
||||
"""
|
||||
Views for the notifications API.
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db.models import Count
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from rest_framework import generics, permissions, status
|
||||
from rest_framework.generics import UpdateAPIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from common.djangoapps.student.models import CourseEnrollment
|
||||
from openedx.core.djangoapps.notifications.models import NotificationPreference
|
||||
|
||||
from .serializers import NotificationCourseEnrollmentSerializer, UserNotificationPreferenceSerializer
|
||||
from .models import Notification
|
||||
from .serializers import (
|
||||
NotificationCourseEnrollmentSerializer,
|
||||
NotificationSerializer,
|
||||
UserNotificationPreferenceSerializer
|
||||
)
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
@@ -130,3 +139,132 @@ class UserNotificationPreferenceView(APIView):
|
||||
serializer.save()
|
||||
return Response(serializer.data)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class NotificationListAPIView(generics.ListAPIView):
|
||||
"""
|
||||
API view for listing notifications for a user.
|
||||
|
||||
**Permissions**: User must be authenticated.
|
||||
**Response Format** (paginated):
|
||||
|
||||
{
|
||||
"results" : [
|
||||
{
|
||||
"id": (int) notification_id,
|
||||
"app_name": (str) app_name,
|
||||
"notification_type": (str) notification_type,
|
||||
"content": (str) content,
|
||||
"content_context": (dict) content_context,
|
||||
"content_url": (str) content_url,
|
||||
"last_read": (datetime) last_read,
|
||||
"last_seen": (datetime) last_seen
|
||||
},
|
||||
...
|
||||
],
|
||||
"count": (int) total_number_of_notifications,
|
||||
"next": (str) url_to_next_page_of_notifications,
|
||||
"previous": (str) url_to_previous_page_of_notifications,
|
||||
"page_size": (int) number_of_notifications_per_page,
|
||||
|
||||
}
|
||||
|
||||
Response Error Codes:
|
||||
- 403: The requester cannot access resource.
|
||||
"""
|
||||
|
||||
serializer_class = NotificationSerializer
|
||||
permission_classes = (permissions.IsAuthenticated,)
|
||||
|
||||
def get_queryset(self):
|
||||
"""
|
||||
Override the get_queryset method to filter the queryset by app name and request.user.
|
||||
"""
|
||||
app_name = self.request.query_params.get('app_name')
|
||||
if app_name:
|
||||
return Notification.objects.filter(user=self.request.user, app_name=app_name)
|
||||
else:
|
||||
return Notification.objects.filter(user=self.request.user)
|
||||
|
||||
|
||||
class NotificationCountView(APIView):
|
||||
"""
|
||||
API view for getting the unseen notifications count for a user.
|
||||
"""
|
||||
|
||||
permission_classes = (permissions.IsAuthenticated,)
|
||||
|
||||
def get(self, request):
|
||||
"""
|
||||
Get the unseen notifications count for a user.
|
||||
|
||||
**Permissions**: User must be authenticated.
|
||||
**Response Format**:
|
||||
```json
|
||||
{
|
||||
"count": (int) total_number_of_unseen_notifications,
|
||||
"count_by_app_name": {
|
||||
(str) app_name: (int) number_of_unseen_notifications,
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
**Response Error Codes**:
|
||||
- 403: The requester cannot access resource.
|
||||
"""
|
||||
# Get the unseen notifications count for each app name.
|
||||
count_by_app_name = (
|
||||
Notification.objects
|
||||
.filter(user_id=request.user, last_seen__isnull=True)
|
||||
.values('app_name')
|
||||
.annotate(count=Count('*'))
|
||||
)
|
||||
count_total = 0
|
||||
count_by_app_name_dict = {}
|
||||
|
||||
for item in count_by_app_name:
|
||||
app_name = item['app_name']
|
||||
count = item['count']
|
||||
|
||||
count_total += count
|
||||
count_by_app_name_dict[app_name] = count
|
||||
# Return the unseen notifications count for the user and the unseen notifications count for each app name.
|
||||
|
||||
return Response({
|
||||
"count": count_total,
|
||||
"count_by_app_name": count_by_app_name_dict,
|
||||
})
|
||||
|
||||
|
||||
class MarkNotificationsUnseenAPIView(UpdateAPIView):
|
||||
"""
|
||||
API view for marking user's all notifications unseen for a provided app_name.
|
||||
"""
|
||||
|
||||
permission_classes = (permissions.IsAuthenticated,)
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
"""
|
||||
Marks all notifications for the given app name unseen for the authenticated user.
|
||||
|
||||
**Args:**
|
||||
app_name: The name of the app to mark notifications unseen for.
|
||||
**Response Format:**
|
||||
A `Response` object with a 200 OK status code if the notifications were successfully marked unseen.
|
||||
**Response Error Codes**:
|
||||
- 400: Bad Request status code if the app name is invalid.
|
||||
"""
|
||||
app_name = self.kwargs.get('app_name')
|
||||
|
||||
if not app_name:
|
||||
return Response({'message': 'Invalid app name.'}, status=400)
|
||||
|
||||
notifications = Notification.objects.filter(
|
||||
user=request.user,
|
||||
app_name=app_name,
|
||||
last_seen__isnull=True,
|
||||
)
|
||||
|
||||
notifications.update(last_seen=datetime.now())
|
||||
|
||||
return Response({'message': 'Notifications marked unseen.'}, status=200)
|
||||
|
||||
Reference in New Issue
Block a user