Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: make notification channel headings clickable in notification pr… #34083

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions openedx/core/djangoapps/notifications/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,10 +156,65 @@ def update(self, instance, validated_data):
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 = (
Expand Down
138 changes: 138 additions & 0 deletions openedx/core/djangoapps/notifications/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,144 @@ def test_info_is_not_saved_in_json(self):
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.
Expand Down
8 changes: 7 additions & 1 deletion openedx/core/djangoapps/notifications/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
NotificationCountView,
NotificationListAPIView,
NotificationReadAPIView,
UserNotificationPreferenceView
UserNotificationPreferenceView,
UserNotificationChannelPreferenceView
)

router = routers.DefaultRouter()
Expand All @@ -24,6 +25,11 @@
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(
Expand Down
55 changes: 54 additions & 1 deletion openedx/core/djangoapps/notifications/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@
NotificationCourseEnrollmentSerializer,
NotificationSerializer,
UserCourseNotificationPreferenceSerializer,
UserNotificationPreferenceUpdateSerializer
UserNotificationPreferenceUpdateSerializer,
UserNotificationChannelPreferenceUpdateSerializer,
)
from .utils import get_show_notifications_tray

Expand Down Expand Up @@ -232,6 +233,58 @@ def patch(self, request, course_key_string):
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):
"""
Expand Down
Loading