Skip to content

Commit

Permalink
feat: add notifications mark as read API (openedx#32475)
Browse files Browse the repository at this point in the history
* feat: add notifications mark as read API

* chore: update description for mark as read notification URL

* refactor: resolve pylint issue

* refactor: notifications mark as read API and test cases

* feat: add translated messages in notificationsAPI response
  • Loading branch information
awais-ansari authored Jun 19, 2023
1 parent 7a217bf commit 8990035
Show file tree
Hide file tree
Showing 3 changed files with 161 additions and 3 deletions.
102 changes: 102 additions & 0 deletions openedx/core/djangoapps/notifications/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory

from ..base_notification import COURSE_NOTIFICATION_APPS


class CourseEnrollmentListViewTest(ModuleStoreTestCase):
"""
Expand Down Expand Up @@ -501,3 +503,103 @@ def test_mark_notifications_unseen(self):
# 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)


class NotificationReadAPIViewTestCase(APITestCase):
"""
Tests for the NotificationReadAPIView.
"""

def setUp(self):
self.user = UserFactory()
self.url = reverse('notifications-read')
self.client.login(username=self.user.username, password='test')

# Create some sample notifications for the user with already existing apps and with invalid app name
Notification.objects.create(user=self.user, app_name='app_name_2', notification_type='Type A')
for app_name in COURSE_NOTIFICATION_APPS:
Notification.objects.create(user=self.user, app_name=app_name, notification_type='Type A')
Notification.objects.create(user=self.user, app_name=app_name, notification_type='Type B')

def test_mark_all_notifications_read_with_app_name(self):
# Create a PATCH request to mark all notifications as read for already existing app e.g 'discussion'
app_name = next(iter(COURSE_NOTIFICATION_APPS))
data = {'app_name': app_name}

response = self.client.patch(self.url, data)

self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, {'message': 'Notifications marked read.'})
notifications = Notification.objects.filter(user=self.user, app_name=app_name, last_read__isnull=False)
self.assertEqual(notifications.count(), 2)

def test_mark_all_notifications_read_with_invalid_app_name(self):
# Create a PATCH request to mark all notifications as read for 'app_name_1'
app_name = 'app_name_1'
data = {'app_name': app_name}

response = self.client.patch(self.url, data)

self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.data, {'error': 'Invalid app_name or notification_id.'})

def test_mark_notification_read_with_notification_id(self):
# Create a PATCH request to mark notification as read for notification_id: 2
notification_id = 2
data = {'notification_id': notification_id}

response = self.client.patch(self.url, data)

self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, {'message': 'Notification marked read.'})
notifications = Notification.objects.filter(user=self.user, id=notification_id, last_read__isnull=False)
self.assertEqual(notifications.count(), 1)

def test_mark_notification_read_with_other_user_notification_id(self):
# Create a PATCH request to mark notification as read for notification_id: 2 through a different user
self.client.logout()
self.user = UserFactory()
self.client.login(username=self.user.username, password='test')

notification_id = 2
data = {'notification_id': notification_id}
response = self.client.patch(self.url, data)

self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
notifications = Notification.objects.filter(user=self.user, id=notification_id, last_read__isnull=False)
self.assertEqual(notifications.count(), 0)

def test_mark_notification_read_with_invalid_notification_id(self):
# Create a PATCH request to mark notification as read for notification_id: 23345
notification_id = 23345
data = {'notification_id': notification_id}

response = self.client.patch(self.url, data)

self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
self.assertEqual(response.data["detail"], 'Not found.')

def test_mark_notification_read_with_app_name_and_notification_id(self):
# Create a PATCH request to mark notification as read for existing app e.g 'discussion' and notification_id: 2
# notification_id has higher priority than app_name in this case app_name is ignored
app_name = next(iter(COURSE_NOTIFICATION_APPS))
notification_id = 2
data = {'app_name': app_name, 'notification_id': notification_id}

response = self.client.patch(self.url, data)

self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, {'message': 'Notification marked read.'})
notifications = Notification.objects.filter(
user=self.user,
id=notification_id,
last_read__isnull=False
)
self.assertEqual(notifications.count(), 1)

def test_mark_notification_read_without_app_name_and_notification_id(self):
# Create a PATCH request to mark notification as read without app_name and notification_id
response = self.client.patch(self.url, {})

self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.data, {'error': 'Invalid app_name or notification_id.'})
2 changes: 2 additions & 0 deletions openedx/core/djangoapps/notifications/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
MarkNotificationsUnseenAPIView,
NotificationCountView,
NotificationListAPIView,
NotificationReadAPIView,
UserNotificationPreferenceView
)

Expand All @@ -30,6 +31,7 @@
MarkNotificationsUnseenAPIView.as_view(),
name='mark-notifications-unseen'
),
path('read/', NotificationReadAPIView.as_view(), name='notifications-read'),

]

Expand Down
60 changes: 57 additions & 3 deletions openedx/core/djangoapps/notifications/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

from django.conf import settings
from django.db.models import Count
from django.shortcuts import get_object_or_404
from django.utils.translation import gettext as _
from opaque_keys.edx.keys import CourseKey
from pytz import UTC
from rest_framework import generics, permissions, status
Expand All @@ -18,6 +20,7 @@
get_course_notification_preference_config_version
)

from .base_notification import COURSE_NOTIFICATION_APPS
from .config.waffle import ENABLE_NOTIFICATIONS, SHOW_NOTIFICATIONS_TRAY
from .models import Notification
from .serializers import (
Expand Down Expand Up @@ -174,7 +177,7 @@ def patch(self, request, course_key_string):
)
if user_course_notification_preference.config_version != get_course_notification_preference_config_version():
return Response(
{'error': 'The notification preference config version is not up to date.'},
{'error': _('The notification preference config version is not up to date.')},
status=status.HTTP_409_CONFLICT,
)

Expand Down Expand Up @@ -323,7 +326,7 @@ def update(self, request, *args, **kwargs):
app_name = self.kwargs.get('app_name')

if not app_name:
return Response({'message': 'Invalid app name.'}, status=400)
return Response({'error': _('Invalid app name.')}, status=400)

notifications = Notification.objects.filter(
user=request.user,
Expand All @@ -333,4 +336,55 @@ def update(self, request, *args, **kwargs):

notifications.update(last_seen=datetime.now())

return Response({'message': 'Notifications marked unseen.'}, status=200)
return Response({'message': _('Notifications marked unseen.')}, status=200)


class NotificationReadAPIView(APIView):
"""
API view for marking user notifications as read, either all notifications or a single notification
"""

permission_classes = (permissions.IsAuthenticated,)

def patch(self, request, *args, **kwargs):
"""
Marks all notifications or single notification read for the given
app name or notification id for the authenticated user.
Requests:
PATCH /api/notifications/read/
Parameters:
request (Request): The request object containing the app name or notification id.
{
"app_name": (str) app_name,
"notification_id": (int) notification_id
}
Returns:
- 200: OK status code if the notification or notifications were successfully marked read.
- 400: Bad Request status code if the app name is invalid.
- 403: Forbidden status code if the user is not authenticated.
- 404: Not Found status code if the notification was not found.
"""
notification_id = request.data.get('notification_id', None)
read_at = datetime.now(UTC)

if notification_id:
notification = get_object_or_404(Notification, pk=notification_id, user=request.user)
notification.last_read = read_at
notification.save()
return Response({'message': _('Notification marked read.')}, status=status.HTTP_200_OK)

app_name = request.data.get('app_name', '')

if app_name and app_name in COURSE_NOTIFICATION_APPS:
notifications = Notification.objects.filter(
user=request.user,
app_name=app_name,
last_read__isnull=True,
)
notifications.update(last_read=read_at)
return Response({'message': _('Notifications marked read.')}, status=status.HTTP_200_OK)

return Response({'error': _('Invalid app_name or notification_id.')}, status=status.HTTP_400_BAD_REQUEST)

0 comments on commit 8990035

Please sign in to comment.