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: [FC-0031] Add new endpoint CourseInfoDetailView #33297

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
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
Loading