AA-137: Support courseware celebrations
- Add a new CourseEnrollmentCelebration model, which ties a course enrollment to some booleans about progress celebrations - Add serialization of the new model to the existing courseware_api app's existing course info view - Add new API in courseware_api to update a celebration model
This commit is contained in:
@@ -87,6 +87,7 @@ class CourseInfoSerializer(serializers.Serializer): # pylint: disable=abstract-
|
||||
can_load_courseware = serializers.DictField()
|
||||
notes = serializers.DictField()
|
||||
marketing_url = serializers.CharField()
|
||||
celebrations = serializers.DictField()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
|
||||
@@ -11,8 +11,8 @@ from django.conf import settings
|
||||
|
||||
from lms.djangoapps.courseware.access_utils import ACCESS_DENIED, ACCESS_GRANTED
|
||||
from lms.djangoapps.courseware.tabs import ExternalLinkCourseTab
|
||||
from student.models import CourseEnrollment
|
||||
from student.tests.factories import UserFactory
|
||||
from student.models import CourseEnrollment, CourseEnrollmentCelebration
|
||||
from student.tests.factories import CourseEnrollmentCelebrationFactory, UserFactory
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.tests.django_utils import TEST_DATA_SPLIT_MODULESTORE, SharedModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import ItemFactory, ToyCourseFactory
|
||||
@@ -156,3 +156,54 @@ class ResumeApiTestViews(BaseCoursewareTests, CompletionWaffleTestMixin):
|
||||
assert response.data['block_id'] == str(self.unit.location)
|
||||
assert response.data['unit_id'] == str(self.unit.location)
|
||||
assert response.data['section_id'] == str(self.sequence.location)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class CelebrationApiTestViews(BaseCoursewareTests):
|
||||
"""
|
||||
Tests for the celebration API
|
||||
"""
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.url = '/api/courseware/celebration/{}'.format(cls.course.id)
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.enrollment = CourseEnrollment.enroll(self.user, self.course.id, 'verified')
|
||||
|
||||
@ddt.data(True, False)
|
||||
def test_happy_path(self, update):
|
||||
if update:
|
||||
CourseEnrollmentCelebrationFactory(enrollment=self.enrollment, celebrate_first_section=False)
|
||||
|
||||
response = self.client.post(self.url, {'first_section': True}, content_type='application/json')
|
||||
assert response.status_code == (200 if update else 201)
|
||||
|
||||
celebration = CourseEnrollmentCelebration.objects.first()
|
||||
assert celebration.celebrate_first_section
|
||||
assert celebration.enrollment.id == self.enrollment.id
|
||||
|
||||
def test_extra_data(self):
|
||||
response = self.client.post(self.url, {'extra': True}, content_type='application/json')
|
||||
assert response.status_code == 400
|
||||
|
||||
def test_no_data(self):
|
||||
response = self.client.post(self.url, {}, content_type='application/json')
|
||||
assert response.status_code == 200
|
||||
assert CourseEnrollmentCelebration.objects.count() == 0
|
||||
|
||||
def test_no_enrollment(self):
|
||||
self.enrollment.delete()
|
||||
response = self.client.post(self.url, {'first_section': True}, content_type='application/json')
|
||||
assert response.status_code == 404
|
||||
|
||||
def test_no_login(self):
|
||||
self.client.logout()
|
||||
response = self.client.post(self.url, {'first_section': True}, content_type='application/json')
|
||||
assert response.status_code == 401
|
||||
|
||||
def test_invalid_course(self):
|
||||
response = self.client.post('/api/courseware/celebration/course-v1:does+not+exist',
|
||||
{'first_section': True}, content_type='application/json')
|
||||
assert response.status_code == 404
|
||||
|
||||
@@ -18,4 +18,7 @@ urlpatterns = [
|
||||
url(r'^resume/{}'.format(settings.COURSE_KEY_PATTERN),
|
||||
views.Resume.as_view(),
|
||||
name="resume-api"),
|
||||
url(r'^celebration/{}'.format(settings.COURSE_KEY_PATTERN),
|
||||
views.Celebration.as_view(),
|
||||
name="celebration-api"),
|
||||
]
|
||||
|
||||
@@ -29,7 +29,7 @@ from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin
|
||||
from openedx.features.content_type_gating.models import ContentTypeGatingConfig
|
||||
from openedx.features.course_duration_limits.access import generate_course_expired_message
|
||||
from openedx.features.discounts.utils import generate_offer_html
|
||||
from student.models import CourseEnrollment
|
||||
from student.models import CourseEnrollment, CourseEnrollmentCelebration
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.search import path_to_location
|
||||
|
||||
@@ -48,6 +48,8 @@ class CoursewareMeta:
|
||||
)
|
||||
self.effective_user = self.overview.effective_user
|
||||
self.course_key = course_key
|
||||
self.enrollment_object = CourseEnrollment.get_enrollment(self.effective_user, self.course_key,
|
||||
select_related=['celebration'])
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self.overview, name)
|
||||
@@ -90,8 +92,7 @@ class CoursewareMeta:
|
||||
|
||||
@property
|
||||
def can_show_upgrade_sock(self):
|
||||
enrollment = CourseEnrollment.get_enrollment(self.effective_user, self.course_key)
|
||||
can_show = can_show_verified_upgrade(self.effective_user, enrollment)
|
||||
can_show = can_show_verified_upgrade(self.effective_user, self.enrollment_object)
|
||||
return can_show
|
||||
|
||||
@property
|
||||
@@ -146,6 +147,15 @@ class CoursewareMeta:
|
||||
'visible': self.overview.edxnotes_visibility,
|
||||
}
|
||||
|
||||
@property
|
||||
def celebrations(self):
|
||||
"""
|
||||
Returns a list of celebrations that should be performed.
|
||||
"""
|
||||
return {
|
||||
'first_section': CourseEnrollmentCelebration.should_celebrate_first_section(self.enrollment_object),
|
||||
}
|
||||
|
||||
|
||||
class CoursewareInformation(RetrieveAPIView):
|
||||
"""
|
||||
@@ -338,3 +348,59 @@ class Resume(DeveloperErrorViewMixin, APIView):
|
||||
pass
|
||||
|
||||
return Response(resp)
|
||||
|
||||
|
||||
class Celebration(DeveloperErrorViewMixin, APIView):
|
||||
"""
|
||||
**Use Cases**
|
||||
|
||||
Marks a particular celebration as complete
|
||||
|
||||
**Example Requests**
|
||||
|
||||
POST /api/courseware/celebration/{course_key}
|
||||
|
||||
**Request Parameters**
|
||||
|
||||
Body consists of the following fields:
|
||||
|
||||
* first_section (bool): whether we should celebrate when a user finishes their first section of a course
|
||||
|
||||
**Returns**
|
||||
|
||||
* 200 or 201 on success with above fields.
|
||||
* 400 if an invalid parameter was sent.
|
||||
* 404 if the course is not available or cannot be seen.
|
||||
"""
|
||||
|
||||
authentication_classes = (
|
||||
JwtAuthentication,
|
||||
SessionAuthenticationAllowInactiveUser,
|
||||
)
|
||||
permission_classes = (IsAuthenticated, )
|
||||
http_method_names = ['post']
|
||||
|
||||
def post(self, request, course_key_string, *args, **kwargs):
|
||||
"""
|
||||
Handle a POST request.
|
||||
"""
|
||||
course_key = CourseKey.from_string(course_key_string)
|
||||
|
||||
data = dict(request.data)
|
||||
first_section = data.pop('first_section', None)
|
||||
if data:
|
||||
return Response(status=400) # there were parameters we didn't recognize
|
||||
|
||||
enrollment = CourseEnrollment.get_enrollment(request.user, course_key)
|
||||
if not enrollment:
|
||||
return Response(status=404)
|
||||
|
||||
defaults = {}
|
||||
if first_section is not None:
|
||||
defaults['celebrate_first_section'] = first_section
|
||||
|
||||
if defaults:
|
||||
_, created = CourseEnrollmentCelebration.objects.update_or_create(enrollment=enrollment, defaults=defaults)
|
||||
return Response(status=201 if created else 200)
|
||||
else:
|
||||
return Response(status=200) # just silently allow it
|
||||
|
||||
Reference in New Issue
Block a user