From 9a43af848ca87807200013a3779ff7e067068d9b Mon Sep 17 00:00:00 2001 From: SaadYousaf Date: Tue, 9 May 2023 15:11:38 +0500 Subject: [PATCH] feat: add api for user notification preferences --- .../djangoapps/notifications/serializers.py | 26 +++++ .../notifications/tests/test_views.py | 94 ++++++++++++++++++ openedx/core/djangoapps/notifications/urls.py | 11 ++- .../core/djangoapps/notifications/views.py | 98 ++++++++++++++++++- 4 files changed, 225 insertions(+), 4 deletions(-) diff --git a/openedx/core/djangoapps/notifications/serializers.py b/openedx/core/djangoapps/notifications/serializers.py index 573111a0e7..9581982c92 100644 --- a/openedx/core/djangoapps/notifications/serializers.py +++ b/openedx/core/djangoapps/notifications/serializers.py @@ -5,6 +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 class CourseOverviewSerializer(serializers.ModelSerializer): @@ -26,3 +27,28 @@ class NotificationCourseEnrollmentSerializer(serializers.ModelSerializer): class Meta: model = CourseEnrollment fields = ('course', 'is_active', 'mode') + + +class UserNotificationPreferenceSerializer(serializers.ModelSerializer): + """ + Serializer for user notification preferences. + """ + course_name = serializers.SerializerMethodField(read_only=True) + + class Meta: + model = NotificationPreference + fields = ('id', 'course_name', 'course_id', 'notification_preference_config',) + read_only_fields = ('id', 'course_name', 'course_id',) + write_only_fields = ('notification_preference_config',) + + def get_course_name(self, obj): + """ + Returns course name from course id. + """ + return CourseOverview.get_from_id(obj.course_id).display_name + + def update(self, instance, validated_data): + for key, val in validated_data.items(): + setattr(instance, key, val) + instance.save() + return instance diff --git a/openedx/core/djangoapps/notifications/tests/test_views.py b/openedx/core/djangoapps/notifications/tests/test_views.py index 8b293b5bf1..b110613037 100644 --- a/openedx/core/djangoapps/notifications/tests/test_views.py +++ b/openedx/core/djangoapps/notifications/tests/test_views.py @@ -1,6 +1,8 @@ """ Tests for the views in the notifications app. """ +import json + from django.dispatch import Signal from django.urls import reverse from edx_toggles.toggles.testutils import override_waffle_flag @@ -123,3 +125,95 @@ class CourseEnrollmentPostSaveTest(ModuleStoreTestCase): self.assertEqual(notification_preferences.count(), 1) self.assertEqual(notification_preferences[0].user, self.user) + + +@override_waffle_flag(ENABLE_NOTIFICATIONS, active=True) +class UserNotificationPreferenceAPITest(ModuleStoreTestCase): + """ + Test for user notification preference API. + """ + def setUp(self): + super().setUp() + self.user = UserFactory() + self.course = CourseFactory.create( + org='testorg', + number='testcourse', + run='testrun' + ) + + course_overview = CourseOverviewFactory.create(id=self.course.id, org='AwesomeOrg') + self.course_enrollment = CourseEnrollment.objects.create( + user=self.user, + course=course_overview, + is_active=True, + mode='audit' + ) + self.post_save_signal = Signal() + self.client = APIClient() + self.path = reverse('notification-preferences', kwargs={'course_key_string': self.course.id}) + self.post_save_signal.send( + sender=self.course_enrollment.__class__, + instance=self.course_enrollment, + created=True + ) + + def _expected_api_response(self, overrides=None): + """ + Helper method to return expected API response. + """ + expected_response = { + 'id': 1, + 'course_name': 'course-v1:testorg+testcourse+testrun Course', + 'course_id': 'course-v1:testorg+testcourse+testrun', + 'notification_preference_config': { + 'discussion': { + 'new_post': { + 'web': False, + 'push': False, + 'email': False + } + } + } + } + if overrides: + expected_response.update(overrides) + return expected_response + + def test_get_user_notification_preference_without_login(self): + """ + Test get user notification preference without login. + """ + response = self.client.get(self.path) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_get_user_notification_preference(self): + """ + Test get user notification preference. + """ + self.client.login(username=self.user.username, password='test') + response = self.client.get(self.path) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, self._expected_api_response()) + + def test_patch_user_notification_preference(self): + """ + Test update of user notification preference. + """ + self.client.login(username=self.user.username, password='test') + updated_notification_config_data = { + "notification_preference_config": { + "discussion": { + "new_post": { + "web": True, + "push": False, + "email": False, + }, + }, + }, + } + response = self.client.patch( + self.path, json.dumps(updated_notification_config_data), content_type='application/json' + ) + 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) diff --git a/openedx/core/djangoapps/notifications/urls.py b/openedx/core/djangoapps/notifications/urls.py index eaa85ec148..e7b72d1750 100644 --- a/openedx/core/djangoapps/notifications/urls.py +++ b/openedx/core/djangoapps/notifications/urls.py @@ -2,14 +2,21 @@ URLs for the notifications API. """ from django.urls import path +from django.urls import re_path +from django.conf import settings from rest_framework import routers - -from .views import CourseEnrollmentListView +from .views import CourseEnrollmentListView, UserNotificationPreferenceView router = routers.DefaultRouter() + urlpatterns = [ path('enrollments/', CourseEnrollmentListView.as_view(), name='enrollment-list'), + re_path( + fr'^configurations/{settings.COURSE_KEY_PATTERN}$', + UserNotificationPreferenceView.as_view(), + name='notification-preferences' + ), ] urlpatterns += router.urls diff --git a/openedx/core/djangoapps/notifications/views.py b/openedx/core/djangoapps/notifications/views.py index 9aaa89550f..e901a888e5 100644 --- a/openedx/core/djangoapps/notifications/views.py +++ b/openedx/core/djangoapps/notifications/views.py @@ -1,11 +1,18 @@ """ Views for the notifications API. """ -from rest_framework import generics, permissions +from django.contrib.auth import get_user_model +from opaque_keys.edx.keys import CourseKey +from rest_framework import generics, permissions, status +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 +from .serializers import NotificationCourseEnrollmentSerializer, UserNotificationPreferenceSerializer + +User = get_user_model() class CourseEnrollmentListView(generics.ListAPIView): @@ -36,3 +43,90 @@ class CourseEnrollmentListView(generics.ListAPIView): def get_queryset(self): user = self.request.user return CourseEnrollment.objects.filter(user=user, is_active=True) + + +class UserNotificationPreferenceView(APIView): + """ + Supports retrieving and patching the UserNotificationPreference + model. + + **Example Requests** + GET /api/notifications/configurations/{course_id} + PATCH /api/notifications/configurations/{course_id} + + **Example Response**: + { + 'id': 1, + 'course_name': 'testcourse', + 'course_id': 'course-v1:testorg+testcourse+testrun', + 'notification_preference_config': { + 'discussion': { + 'new_post': { + 'web': False, + 'push': False, + 'email': False, + } + } + } + } + """ + permission_classes = (permissions.IsAuthenticated,) + + def get(self, request, course_key_string): + """ + Returns notification preference for user for a course. + + Parameters: + request (Request): The request object. + course_key_string (int): The ID of the course to retrieve notification preference. + + Returns: + { + 'id': 1, + 'course_name': 'testcourse', + 'course_id': 'course-v1:testorg+testcourse+testrun', + 'notification_preference_config': { + 'discussion': { + 'new_post': { + 'web': False, + 'push': False, + 'email': False, + } + } + } + } + """ + course_id = CourseKey.from_string(course_key_string) + user_notification_preference, _ = NotificationPreference.objects.get_or_create( + user=request.user, + course_id=course_id, + is_active=True, + ) + serializer = UserNotificationPreferenceSerializer(user_notification_preference) + return Response(serializer.data) + + def patch(self, request, course_key_string): + """ + Update an existing user notification preference with the data in the request body. + + Parameters: + request (Request): The request object + course_key_string (int): The ID of the course of the notification preference to be updated. + + Returns: + 200: The updated preference, serialized using the UserNotificationPreferenceSerializer + 404: If the preference does not exist + 403: If the user does not have permission to update the preference + 400: Validation error + """ + course_id = CourseKey.from_string(course_key_string) + user_notification_preference = NotificationPreference.objects.get( + user=request.user, + course_id=course_id, + is_active=True, + ) + serializer = UserNotificationPreferenceSerializer(user_notification_preference, data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)