feat: added enrollment api for notification config (#32162)
* feat: added enrollment API for notification config * feat: added apps.py in notifications * feat: added waffle flag for notification app * feat: added proper docs for the API
This commit is contained in:
1
.github/workflows/unit-test-shards.json
vendored
1
.github/workflows/unit-test-shards.json
vendored
@@ -126,6 +126,7 @@
|
||||
"openedx/core/djangoapps/lang_pref/",
|
||||
"openedx/core/djangoapps/models/",
|
||||
"openedx/core/djangoapps/monkey_patch/",
|
||||
"openedx/core/djangoapps/notifications/",
|
||||
"openedx/core/djangoapps/oauth_dispatch/",
|
||||
"openedx/core/djangoapps/olx_rest_api/",
|
||||
"openedx/core/djangoapps/password_policy/",
|
||||
|
||||
@@ -332,3 +332,6 @@ urlpatterns.extend(get_plugin_url_patterns(ProjectType.CMS))
|
||||
urlpatterns += [
|
||||
path('api/contentstore/', include('cms.djangoapps.contentstore.rest_api.urls'))
|
||||
]
|
||||
urlpatterns += [
|
||||
path('api/notifications/', include('openedx.core.djangoapps.notifications.urls')),
|
||||
]
|
||||
|
||||
@@ -1049,3 +1049,7 @@ urlpatterns += [
|
||||
urlpatterns += [
|
||||
path('api/mfe_config/v1', include(('lms.djangoapps.mfe_config_api.urls', 'lms.djangoapps.mfe_config_api'), namespace='mfe_config_api'))
|
||||
]
|
||||
|
||||
urlpatterns += [
|
||||
path('api/notifications/', include('openedx.core.djangoapps.notifications.urls')),
|
||||
]
|
||||
|
||||
20
openedx/core/djangoapps/notifications/apps.py
Normal file
20
openedx/core/djangoapps/notifications/apps.py
Normal file
@@ -0,0 +1,20 @@
|
||||
"""
|
||||
Config for notifications app
|
||||
"""
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class NotificationsConfig(AppConfig):
|
||||
"""
|
||||
Config for notifications app
|
||||
"""
|
||||
name = 'openedx.core.djangoapps.notifications'
|
||||
verbose_name = 'Notifications'
|
||||
|
||||
def ready(self):
|
||||
"""
|
||||
Import signals
|
||||
"""
|
||||
# pylint: disable=unused-import
|
||||
from . import handlers
|
||||
19
openedx/core/djangoapps/notifications/config/waffle.py
Normal file
19
openedx/core/djangoapps/notifications/config/waffle.py
Normal file
@@ -0,0 +1,19 @@
|
||||
"""
|
||||
This module contains various configuration settings via
|
||||
waffle switches for the notifications app.
|
||||
"""
|
||||
|
||||
from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag
|
||||
|
||||
WAFFLE_NAMESPACE = 'notifications'
|
||||
|
||||
# .. toggle_name: notifications.enable_notifications
|
||||
# .. toggle_implementation: CourseWaffleFlag
|
||||
# .. toggle_default: False
|
||||
# .. toggle_description: Waffle flag to enable the Notifications feature
|
||||
# .. toggle_use_cases: temporary, open_edx
|
||||
# .. toggle_creation_date: 2023-05-05
|
||||
# .. toggle_target_removal_date: 2023-11-05
|
||||
# .. toggle_warning: When the flag is ON, Notifications feature is enabled.
|
||||
# .. toggle_tickets: INF-866
|
||||
ENABLE_NOTIFICATIONS = CourseWaffleFlag(f'{WAFFLE_NAMESPACE}.enable_notifications', __name__)
|
||||
26
openedx/core/djangoapps/notifications/handlers.py
Normal file
26
openedx/core/djangoapps/notifications/handlers.py
Normal file
@@ -0,0 +1,26 @@
|
||||
"""
|
||||
Handlers for notifications
|
||||
"""
|
||||
import logging
|
||||
|
||||
from django.db import IntegrityError
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
from openedx.core.djangoapps.notifications.config.waffle import ENABLE_NOTIFICATIONS
|
||||
from openedx.core.djangoapps.notifications.models import NotificationPreference
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@receiver(post_save, sender='student.CourseEnrollment')
|
||||
def course_enrollment_post_save(sender, instance, created, **kwargs):
|
||||
"""
|
||||
Watches for post_save signal for creates on the CourseEnrollment table.
|
||||
Generate a NotificationPreference if new Enrollment is created
|
||||
"""
|
||||
if created and ENABLE_NOTIFICATIONS.is_enabled(instance.course_id):
|
||||
try:
|
||||
NotificationPreference.objects.create(user=instance.user, course_id=instance.course_id)
|
||||
except IntegrityError:
|
||||
log.info(f'NotificationPreference already exists for user {instance.user} and course {instance.course_id}')
|
||||
@@ -4,10 +4,8 @@ Models for notifications
|
||||
from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user
|
||||
from django.db import models
|
||||
from model_utils.models import TimeStampedModel
|
||||
|
||||
from opaque_keys.edx.django.models import CourseKeyField
|
||||
|
||||
|
||||
# When notification preferences are updated, we need to update the CONFIG_VERSION.
|
||||
NOTIFICATION_PREFERENCE_CONFIG = {
|
||||
"discussion": {
|
||||
|
||||
28
openedx/core/djangoapps/notifications/serializers.py
Normal file
28
openedx/core/djangoapps/notifications/serializers.py
Normal file
@@ -0,0 +1,28 @@
|
||||
"""
|
||||
Serializers for the notifications API.
|
||||
"""
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.djangoapps.student.models import CourseEnrollment
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
|
||||
|
||||
class CourseOverviewSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Serializer for CourseOverview model.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = CourseOverview
|
||||
fields = ('id', 'display_name')
|
||||
|
||||
|
||||
class NotificationCourseEnrollmentSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Serializer for CourseEnrollment model.
|
||||
"""
|
||||
course = CourseOverviewSerializer()
|
||||
|
||||
class Meta:
|
||||
model = CourseEnrollment
|
||||
fields = ('course', 'is_active', 'mode')
|
||||
125
openedx/core/djangoapps/notifications/tests/test_views.py
Normal file
125
openedx/core/djangoapps/notifications/tests/test_views.py
Normal file
@@ -0,0 +1,125 @@
|
||||
"""
|
||||
Tests for the views in the notifications app.
|
||||
"""
|
||||
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 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.serializers import NotificationCourseEnrollmentSerializer
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
|
||||
class CourseEnrollmentListViewTest(ModuleStoreTestCase):
|
||||
"""
|
||||
Tests for the CourseEnrollmentListView.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Set up the test.
|
||||
"""
|
||||
super().setUp()
|
||||
self.client = APIClient()
|
||||
self.user = UserFactory()
|
||||
# self.client.force_authenticate(user=self.user)
|
||||
course_1 = CourseFactory.create(
|
||||
org='testorg',
|
||||
number='testcourse',
|
||||
run='testrun'
|
||||
)
|
||||
course_2 = CourseFactory.create(
|
||||
org='testorg',
|
||||
number='testcourse_two',
|
||||
run='testrun'
|
||||
)
|
||||
course_overview_1 = CourseOverviewFactory.create(id=course_1.id, org='AwesomeOrg')
|
||||
course_overview_2 = CourseOverviewFactory.create(id=course_2.id, org='AwesomeOrg')
|
||||
|
||||
self.enrollment1 = CourseEnrollment.objects.create(
|
||||
user=self.user,
|
||||
course=course_overview_1,
|
||||
is_active=True,
|
||||
mode='audit'
|
||||
)
|
||||
self.enrollment2 = CourseEnrollment.objects.create(
|
||||
user=self.user,
|
||||
course=course_overview_2,
|
||||
is_active=False,
|
||||
mode='honor'
|
||||
)
|
||||
|
||||
def test_course_enrollment_list_view(self):
|
||||
"""
|
||||
Test the CourseEnrollmentListView.
|
||||
"""
|
||||
self.client.login(username=self.user.username, password='test')
|
||||
url = reverse('enrollment-list')
|
||||
response = self.client.get(url)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
enrollments = CourseEnrollment.objects.filter(user=self.user, is_active=True)
|
||||
expected_data = NotificationCourseEnrollmentSerializer(enrollments, many=True).data
|
||||
self.assertEqual(response.data, expected_data)
|
||||
|
||||
def test_course_enrollment_api_permission(self):
|
||||
"""
|
||||
Calls api without login.
|
||||
Check is 403 is returned
|
||||
"""
|
||||
url = reverse('enrollment-list')
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||
|
||||
|
||||
@override_waffle_flag(ENABLE_NOTIFICATIONS, active=True)
|
||||
class CourseEnrollmentPostSaveTest(ModuleStoreTestCase):
|
||||
"""
|
||||
Tests for the post_save signal for CourseEnrollment.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Set up the test.
|
||||
"""
|
||||
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()
|
||||
|
||||
def test_course_enrollment_post_save(self):
|
||||
"""
|
||||
Test the post_save signal for CourseEnrollment.
|
||||
"""
|
||||
# Emit post_save signal
|
||||
|
||||
self.post_save_signal.send(
|
||||
sender=self.course_enrollment.__class__,
|
||||
instance=self.course_enrollment,
|
||||
created=True
|
||||
)
|
||||
|
||||
# Assert that NotificationPreference object was created with correct attributes
|
||||
notification_preferences = NotificationPreference.objects.all()
|
||||
|
||||
self.assertEqual(notification_preferences.count(), 1)
|
||||
self.assertEqual(notification_preferences[0].user, self.user)
|
||||
15
openedx/core/djangoapps/notifications/urls.py
Normal file
15
openedx/core/djangoapps/notifications/urls.py
Normal file
@@ -0,0 +1,15 @@
|
||||
"""
|
||||
URLs for the notifications API.
|
||||
"""
|
||||
from django.urls import path
|
||||
from rest_framework import routers
|
||||
|
||||
from .views import CourseEnrollmentListView
|
||||
|
||||
router = routers.DefaultRouter()
|
||||
|
||||
urlpatterns = [
|
||||
path('enrollments/', CourseEnrollmentListView.as_view(), name='enrollment-list'),
|
||||
]
|
||||
|
||||
urlpatterns += router.urls
|
||||
38
openedx/core/djangoapps/notifications/views.py
Normal file
38
openedx/core/djangoapps/notifications/views.py
Normal file
@@ -0,0 +1,38 @@
|
||||
"""
|
||||
Views for the notifications API.
|
||||
"""
|
||||
from rest_framework import generics, permissions
|
||||
|
||||
from common.djangoapps.student.models import CourseEnrollment
|
||||
|
||||
from .serializers import NotificationCourseEnrollmentSerializer
|
||||
|
||||
|
||||
class CourseEnrollmentListView(generics.ListAPIView):
|
||||
"""
|
||||
API endpoint to get active CourseEnrollments for requester.
|
||||
|
||||
**Permissions**: User must be authenticated.
|
||||
|
||||
**Response Format**:
|
||||
[
|
||||
{
|
||||
"course": {
|
||||
"id": (int) course_id,
|
||||
"display_name": (str) course_display_name
|
||||
},
|
||||
"is_active": (bool) is_enrollment_active,
|
||||
"mode": (str) enrollment_mode
|
||||
},
|
||||
...
|
||||
]
|
||||
**Response Error Codes**:
|
||||
- 403: The requester cannot access resource.
|
||||
"""
|
||||
serializer_class = NotificationCourseEnrollmentSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
pagination_class = None
|
||||
|
||||
def get_queryset(self):
|
||||
user = self.request.user
|
||||
return CourseEnrollment.objects.filter(user=user, is_active=True)
|
||||
Reference in New Issue
Block a user