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:
Ahtisham Shahid
2023-05-09 11:51:31 +05:00
committed by GitHub
parent 837bcdd8ed
commit 118ea3a024
13 changed files with 279 additions and 2 deletions

View File

@@ -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/",

View File

@@ -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')),
]

View File

@@ -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')),
]

View 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

View 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__)

View 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}')

View File

@@ -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": {

View 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')

View 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)

View 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

View 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)