feat: make notification channel headings clickable (#34194)
* feat: make notification channel headings clickable in notification preferences * refactor: serializer code updated for better readability * test: added a test for UserNotificationChannelPreferenceView API * fix: updated the api test that was failing due to conflicts --------- Co-authored-by: eemaanamir <eemaan.amir@gmail.com>
This commit is contained in:
@@ -156,10 +156,65 @@ class UserNotificationPreferenceUpdateSerializer(serializers.Serializer):
|
||||
return instance
|
||||
|
||||
|
||||
class UserNotificationChannelPreferenceUpdateSerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer for user notification preferences update for an entire channel.
|
||||
"""
|
||||
|
||||
notification_app = serializers.CharField()
|
||||
value = serializers.BooleanField()
|
||||
notification_channel = serializers.CharField(required=False)
|
||||
|
||||
def validate(self, attrs):
|
||||
"""
|
||||
Validation for notification preference update form
|
||||
"""
|
||||
notification_app = attrs.get('notification_app')
|
||||
notification_channel = attrs.get('notification_channel')
|
||||
|
||||
notification_app_config = self.instance.notification_preference_config
|
||||
|
||||
if not notification_channel:
|
||||
raise ValidationError(
|
||||
'notification_channel is required for notification_type.'
|
||||
)
|
||||
|
||||
if not notification_app_config.get(notification_app, None):
|
||||
raise ValidationError(
|
||||
f'{notification_app} is not a valid notification app.'
|
||||
)
|
||||
|
||||
if notification_channel and notification_channel not in get_notification_channels():
|
||||
raise ValidationError(
|
||||
f'{notification_channel} is not a valid notification channel.'
|
||||
)
|
||||
|
||||
return attrs
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
"""
|
||||
Update notification preference config.
|
||||
"""
|
||||
notification_app = validated_data.get('notification_app')
|
||||
notification_channel = validated_data.get('notification_channel')
|
||||
value = validated_data.get('value')
|
||||
user_notification_preference_config = instance.notification_preference_config
|
||||
|
||||
app_prefs = user_notification_preference_config[notification_app]
|
||||
for notification_type_name, notification_type_preferences in app_prefs['notification_types'].items():
|
||||
non_editable_channels = app_prefs['non_editable'].get(notification_type_name, [])
|
||||
if notification_channel not in non_editable_channels:
|
||||
app_prefs['notification_types'][notification_type_name][notification_channel] = value
|
||||
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
|
||||
class NotificationSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Serializer for the Notification model.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = Notification
|
||||
fields = (
|
||||
|
||||
@@ -384,6 +384,144 @@ class UserNotificationPreferenceAPITest(ModuleStoreTestCase):
|
||||
assert 'info' not in type_prefs.keys()
|
||||
|
||||
|
||||
@override_waffle_flag(ENABLE_NOTIFICATIONS, active=True)
|
||||
@override_waffle_flag(ENABLE_REPORTED_CONTENT_NOTIFICATIONS, active=True)
|
||||
@ddt.ddt
|
||||
class UserNotificationChannelPreferenceAPITest(ModuleStoreTestCase):
|
||||
"""
|
||||
Test for user notification channel 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.client = APIClient()
|
||||
self.path = reverse('notification-channel-preferences', kwargs={'course_key_string': self.course.id})
|
||||
|
||||
enrollment_data = CourseEnrollmentData(
|
||||
user=UserData(
|
||||
pii=UserPersonalData(
|
||||
username=self.user.username,
|
||||
email=self.user.email,
|
||||
name=self.user.profile.name,
|
||||
),
|
||||
id=self.user.id,
|
||||
is_active=self.user.is_active,
|
||||
),
|
||||
course=CourseData(
|
||||
course_key=self.course.id,
|
||||
display_name=self.course.display_name,
|
||||
),
|
||||
mode=self.course_enrollment.mode,
|
||||
is_active=self.course_enrollment.is_active,
|
||||
creation_date=self.course_enrollment.created,
|
||||
)
|
||||
COURSE_ENROLLMENT_CREATED.send_event(
|
||||
enrollment=enrollment_data
|
||||
)
|
||||
|
||||
def _expected_api_response(self, course=None):
|
||||
"""
|
||||
Helper method to return expected API response.
|
||||
"""
|
||||
if course is None:
|
||||
course = self.course
|
||||
response = {
|
||||
'id': 1,
|
||||
'course_name': 'course-v1:testorg+testcourse+testrun Course',
|
||||
'course_id': 'course-v1:testorg+testcourse+testrun',
|
||||
'notification_preference_config': {
|
||||
'discussion': {
|
||||
'enabled': True,
|
||||
'core_notification_types': [
|
||||
'new_comment_on_response',
|
||||
'new_comment',
|
||||
'new_response',
|
||||
'response_on_followed_post',
|
||||
'comment_on_followed_post',
|
||||
'response_endorsed_on_thread',
|
||||
'response_endorsed'
|
||||
],
|
||||
'notification_types': {
|
||||
'core': {
|
||||
'web': True,
|
||||
'email': True,
|
||||
'push': True,
|
||||
'info': 'Notifications for responses and comments on your posts, and the ones you’re '
|
||||
'following, including endorsements to your responses and on your posts.'
|
||||
},
|
||||
'new_discussion_post': {'web': False, 'email': False, 'push': False, 'info': ''},
|
||||
'new_question_post': {'web': False, 'email': False, 'push': False, 'info': ''},
|
||||
'content_reported': {'web': True, 'email': True, 'push': True, 'info': ''},
|
||||
},
|
||||
'non_editable': {
|
||||
'core': ['web']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if not ENABLE_COURSEWIDE_NOTIFICATIONS.is_enabled(course.id):
|
||||
app_prefs = response['notification_preference_config']['discussion']
|
||||
notification_types = app_prefs['notification_types']
|
||||
for notification_type in ['new_discussion_post', 'new_question_post']:
|
||||
notification_types.pop(notification_type)
|
||||
return response
|
||||
|
||||
@ddt.data(
|
||||
('discussion', 'web', True, status.HTTP_200_OK),
|
||||
('discussion', 'web', False, status.HTTP_200_OK),
|
||||
|
||||
('invalid_notification_app', 'web', False, status.HTTP_400_BAD_REQUEST),
|
||||
('discussion', 'invalid_notification_channel', False, status.HTTP_400_BAD_REQUEST),
|
||||
)
|
||||
@ddt.unpack
|
||||
@mock.patch("eventtracking.tracker.emit")
|
||||
def test_patch_user_notification_preference(
|
||||
self, notification_app, notification_channel, value, expected_status, mock_emit,
|
||||
):
|
||||
"""
|
||||
Test update of user notification channel preference.
|
||||
"""
|
||||
self.client.login(username=self.user.username, password=self.TEST_PASSWORD)
|
||||
payload = {
|
||||
'notification_app': notification_app,
|
||||
'value': value,
|
||||
}
|
||||
if notification_channel:
|
||||
payload['notification_channel'] = notification_channel
|
||||
|
||||
response = self.client.patch(self.path, json.dumps(payload), content_type='application/json')
|
||||
self.assertEqual(response.status_code, expected_status)
|
||||
|
||||
if expected_status == status.HTTP_200_OK:
|
||||
expected_data = self._expected_api_response()
|
||||
expected_app_prefs = expected_data['notification_preference_config'][notification_app]
|
||||
for notification_type_name, notification_type_preferences in expected_app_prefs[
|
||||
'notification_types'].items():
|
||||
non_editable_channels = expected_app_prefs['non_editable'].get(notification_type_name, [])
|
||||
if notification_channel not in non_editable_channels:
|
||||
expected_app_prefs['notification_types'][notification_type_name][notification_channel] = value
|
||||
self.assertEqual(response.data, expected_data)
|
||||
event_name, event_data = mock_emit.call_args[0]
|
||||
self.assertEqual(event_name, 'edx.notifications.preferences.updated')
|
||||
self.assertEqual(event_data['notification_app'], notification_app)
|
||||
self.assertEqual(event_data['notification_channel'], notification_channel)
|
||||
self.assertEqual(event_data['value'], value)
|
||||
|
||||
|
||||
class NotificationListAPIViewTest(APITestCase):
|
||||
"""
|
||||
Tests suit for the NotificationListAPIView.
|
||||
|
||||
@@ -11,7 +11,8 @@ from .views import (
|
||||
NotificationCountView,
|
||||
NotificationListAPIView,
|
||||
NotificationReadAPIView,
|
||||
UserNotificationPreferenceView
|
||||
UserNotificationPreferenceView,
|
||||
UserNotificationChannelPreferenceView
|
||||
)
|
||||
|
||||
router = routers.DefaultRouter()
|
||||
@@ -24,6 +25,11 @@ urlpatterns = [
|
||||
UserNotificationPreferenceView.as_view(),
|
||||
name='notification-preferences'
|
||||
),
|
||||
re_path(
|
||||
fr'^channel/configurations/{settings.COURSE_KEY_PATTERN}$',
|
||||
UserNotificationChannelPreferenceView.as_view(),
|
||||
name='notification-channel-preferences'
|
||||
),
|
||||
path('', NotificationListAPIView.as_view(), name='notifications-list'),
|
||||
path('count/', NotificationCountView.as_view(), name='notifications-count'),
|
||||
path(
|
||||
|
||||
@@ -35,7 +35,8 @@ from .serializers import (
|
||||
NotificationCourseEnrollmentSerializer,
|
||||
NotificationSerializer,
|
||||
UserCourseNotificationPreferenceSerializer,
|
||||
UserNotificationPreferenceUpdateSerializer
|
||||
UserNotificationPreferenceUpdateSerializer,
|
||||
UserNotificationChannelPreferenceUpdateSerializer,
|
||||
)
|
||||
from .utils import get_show_notifications_tray
|
||||
|
||||
@@ -232,6 +233,58 @@ class UserNotificationPreferenceView(APIView):
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
@allow_any_authenticated_user()
|
||||
class UserNotificationChannelPreferenceView(APIView):
|
||||
"""
|
||||
Supports retrieving and patching the UserNotificationPreference
|
||||
model.
|
||||
|
||||
**Example Requests**
|
||||
PATCH /api/notifications/configurations/{course_id}
|
||||
"""
|
||||
|
||||
def patch(self, request, course_key_string):
|
||||
"""
|
||||
Update an existing user notification preference for an entire channel 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_course_notification_preference = CourseNotificationPreference.objects.get(
|
||||
user=request.user,
|
||||
course_id=course_id,
|
||||
is_active=True,
|
||||
)
|
||||
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.')},
|
||||
status=status.HTTP_409_CONFLICT,
|
||||
)
|
||||
|
||||
preference_update = UserNotificationChannelPreferenceUpdateSerializer(
|
||||
user_course_notification_preference, data=request.data, partial=True
|
||||
)
|
||||
preference_update.is_valid(raise_exception=True)
|
||||
updated_notification_preferences = preference_update.save()
|
||||
notification_preference_update_event(request.user, course_id, preference_update.validated_data)
|
||||
|
||||
serializer_context = {
|
||||
'course_id': course_id,
|
||||
'user': request.user
|
||||
}
|
||||
serializer = UserCourseNotificationPreferenceSerializer(updated_notification_preferences,
|
||||
context=serializer_context)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
@allow_any_authenticated_user()
|
||||
class NotificationListAPIView(generics.ListAPIView):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user