Skip to content

Commit

Permalink
feat: [AXIM-38] Add is_enrolled to Course detail response
Browse files Browse the repository at this point in the history
  • Loading branch information
KyryloKireiev authored and oksana-slu committed Sep 25, 2023
1 parent 8c9232a commit 118a200
Show file tree
Hide file tree
Showing 6 changed files with 198 additions and 2 deletions.
22 changes: 22 additions & 0 deletions lms/djangoapps/mobile_api/course_info/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""
Serializer for course_info API
"""


from common.djangoapps.student.models import CourseEnrollment
from lms.djangoapps.course_api.serializers import CourseDetailSerializer


class CourseInfoDetailSerializer(CourseDetailSerializer):
"""
Serializer for Course objects providing additional details about the
course.
This serializer returns more data - 'is_enrolled' user's status.
"""
def to_representation(self, instance):
response = super().to_representation(instance)

if self.context['request'].user.is_authenticated:
response['is_enrolled'] = CourseEnrollment.is_enrolled(self.context['request'].user, instance.id)
return response
61 changes: 61 additions & 0 deletions lms/djangoapps/mobile_api/course_info/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,18 @@
from edx_toggles.toggles.testutils import override_waffle_flag
from milestones.tests.utils import MilestonesTestCaseMixin
from mock import patch
from rest_framework.request import Request
from rest_framework.test import APIClient # pylint: disable=unused-import

from common.djangoapps.student.models import CourseEnrollment # pylint: disable=unused-import
from common.djangoapps.student.tests.factories import UserFactory # pylint: disable=unused-import
from lms.djangoapps.mobile_api.testutils import MobileAPITestCase, MobileAuthTestMixin, MobileCourseAccessTestMixin
from lms.djangoapps.mobile_api.utils import API_V1, API_V05
from lms.djangoapps.course_api.tests.test_views import CourseDetailViewTestCase
from lms.djangoapps.course_api.tests.test_serializers import TestCourseDetailSerializer
from openedx.features.course_experience import ENABLE_COURSE_GOALS
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from .serializers import CourseInfoDetailSerializer
from xmodule.html_block import CourseInfoBlock # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.modulestore import ModuleStoreEnum # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order
Expand Down Expand Up @@ -255,3 +260,59 @@ def test_flag_disabled(self, mock_logger):
'For this mobile request, user activity is not enabled for this user {} and course {}'.format(
str(self.user.id), str(self.course.id))
)


class CourseInfoDetailViewTestCase(CourseDetailViewTestCase): # lint-amnesty, pylint: disable=test-inherits-tests
"""
Test responses returned from CourseInfoDetailView.
"""

@classmethod
def setUpClass(cls):
super().setUpClass()
cls.url = reverse('course-info-detail', kwargs={
'course_key_string': cls.course.id,
'api_version': 'v3'
})
cls.hidden_url = reverse('course-info-detail', kwargs={
'course_key_string': cls.hidden_course.id,
'api_version': 'v3'
})
cls.nonexistent_url = reverse('course-info-detail', kwargs={
'course_key_string': 'edX/nope/Fall_2014',
'api_version': 'v3'
})


class TestCourseInfoDetailSerializer(TestCourseDetailSerializer): # lint-amnesty, pylint: disable=test-inherits-tests
"""
Test CourseInfoDetailSerializer by rerunning all the tests
in TestCourseDetailSerializer, but with the
CourseInfoDetailSerializer serializer class.
"""

serializer_class = CourseInfoDetailSerializer

def setUp(self):
super().setUp()
# by default, we do not have enrolled users
self.expected_data['is_enrolled'] = False

@patch('lms.djangoapps.mobile_api.course_info.serializers.CourseEnrollment.is_enrolled', return_value=True)
def test_is_enrolled_field_true(self, mock_is_enrolled):
course = self.create_course()
result = self._get_result(course)
assert result['is_enrolled'] is True
mock_is_enrolled.assert_called_once()

def test_is_enrolled_field_anonymous_user(self):
course = self.create_course()
result = self._get_anonymous_result(course)
self.assertNotIn('is_enrolled', result)

def _get_anonymous_request(self):
return Request(self.request_factory.get('/'))

def _get_anonymous_result(self, course):
course_overview = CourseOverview.get_from_id(course.id)
return self.serializer_class(course_overview, context={'request': self._get_anonymous_request()}).data
12 changes: 11 additions & 1 deletion lms/djangoapps/mobile_api/course_info/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,13 @@

from django.conf import settings
from django.urls import path, re_path
from .views import (
CourseHandoutsList,
CourseUpdatesList,
CourseGoalsRecordUserActivity,
CourseInfoDetailView,
)

from .views import CourseHandoutsList, CourseUpdatesList, CourseGoalsRecordUserActivity

urlpatterns = [
re_path(
Expand All @@ -19,5 +24,10 @@
CourseUpdatesList.as_view(),
name='course-updates-list'
),
re_path(
fr'^{settings.COURSE_KEY_PATTERN}/info$',
CourseInfoDetailView.as_view(),
name="course-info-detail"
),
path('record_user_activity', CourseGoalsRecordUserActivity.as_view(), name='record_user_activity'),
]
102 changes: 102 additions & 0 deletions lms/djangoapps/mobile_api/course_info/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,11 @@
from common.djangoapps.static_replace import make_static_urls_absolute
from lms.djangoapps.courseware.courses import get_course_info_section_block
from lms.djangoapps.course_goals.models import UserActivity
from lms.djangoapps.course_api.views import CourseDetailView
from openedx.core.lib.api.view_utils import view_auth_classes
from openedx.core.lib.xblock_utils import get_course_update_items
from openedx.features.course_experience import ENABLE_COURSE_GOALS
from .serializers import CourseInfoDetailSerializer
from ..decorators import mobile_course_access, mobile_view

User = get_user_model()
Expand Down Expand Up @@ -166,3 +169,102 @@ def post(self, request, *args, **kwargs):
# Populate user activity for tracking progress towards a user's course goals
UserActivity.record_user_activity(user, course_key)
return Response(status=(200))


@view_auth_classes(is_authenticated=False)
class CourseInfoDetailView(CourseDetailView):
"""
**Use Cases**
Request details for a course
**Example Requests**
GET /api/mobile/v3/course_info/{course_key}/info
**Response Values**
Body consists of the following fields:
* effort: A textual description of the weekly hours of effort expected
in the course.
* end: Date the course ends, in ISO 8601 notation
* enrollment_end: Date enrollment ends, in ISO 8601 notation
* enrollment_start: Date enrollment begins, in ISO 8601 notation
* id: A unique identifier of the course; a serialized representation
of the opaque key identifying the course.
* media: An object that contains named media items. Included here:
* course_image: An image to show for the course. Represented
as an object with the following fields:
* uri: The location of the image
* name: Name of the course
* number: Catalog number of the course
* org: Name of the organization that owns the course
* overview: A possibly verbose HTML textual description of the course.
Note: this field is only included in the Course Detail view, not
the Course List view.
* short_description: A textual description of the course
* start: Date the course begins, in ISO 8601 notation
* start_display: Readably formatted start of the course
* start_type: Hint describing how `start_display` is set. One of:
* `"string"`: manually set by the course author
* `"timestamp"`: generated from the `start` timestamp
* `"empty"`: no start date is specified
* pacing: Course pacing. Possible values: instructor, self
* certificate_available_date (optional): Date the certificate will be available,
in ISO 8601 notation if the `certificates.auto_certificate_generation`
waffle switch is enabled
* is_enrolled: (bool) Optional field. This field is not available for an anonymous user.
Indicates if the user is enrolled in the course
Deprecated fields:
* blocks_url: Used to fetch the course blocks
* course_id: Course key (use 'id' instead)
**Parameters:**
username (optional):
The username of the specified user for whom the course data
is being accessed. The username is not only required if the API is
requested by an Anonymous user.
**Returns**
* 200 on success with above fields.
* 400 if an invalid parameter was sent or the username was not provided
for an authenticated request.
* 401 unauthorized
* 403 if a user who does not have permission to masquerade as
another user specifies a username other than their own.
* 404 if the course is not available or cannot be seen.
Example response:
{
"blocks_url": "/api/courses/v1/blocks/?course_id=edX%2Fexample%2F2012_Fall",
"media": {
"course_image": {
"uri": "/c4x/edX/example/asset/just_a_test.jpg",
"name": "Course Image"
}
},
"description": "An example course.",
"end": "2015-09-19T18:00:00Z",
"enrollment_end": "2015-07-15T00:00:00Z",
"enrollment_start": "2015-06-15T00:00:00Z",
"course_id": "edX/example/2012_Fall",
"name": "Example Course",
"number": "example",
"org": "edX",
"overview: "<p>A verbose description of the course.</p>"
"start": "2015-07-17T12:00:00Z",
"start_display": "July 17, 2015",
"start_type": "timestamp",
"pacing": "instructor",
"certificate_available_date": "2015-08-14T00:00:00Z",
"is_enrolled": true
}
"""

serializer_class = CourseInfoDetailSerializer
1 change: 1 addition & 0 deletions lms/djangoapps/mobile_api/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
API_V05 = 'v0.5'
API_V1 = 'v1'
API_V2 = 'v2'
API_V3 = 'v3'


def parsed_version(version):
Expand Down
2 changes: 1 addition & 1 deletion lms/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@

if settings.FEATURES.get('ENABLE_MOBILE_REST_API'):
urlpatterns += [
re_path(r'^api/mobile/(?P<api_version>v(2|1|0.5))/', include('lms.djangoapps.mobile_api.urls')),
re_path(r'^api/mobile/(?P<api_version>v(3|2|1|0.5))/', include('lms.djangoapps.mobile_api.urls')),
]

if settings.FEATURES.get('ENABLE_OPENBADGES'):
Expand Down

0 comments on commit 118a200

Please sign in to comment.