From 9da8ff0f0ba646edf0c3947d2442989c5697f91e Mon Sep 17 00:00:00 2001 From: "Dave St.Germain" Date: Mon, 3 Feb 2020 13:51:18 -0500 Subject: [PATCH] Allow anonymous access to courseware API, and return error message if user is unenrolled. --- .../content/course_overviews/models.py | 7 +++ .../djangoapps/courseware_api/serializers.py | 14 +----- .../courseware_api/tests/test_views.py | 50 +++++++++++-------- .../core/djangoapps/courseware_api/views.py | 30 +++++++++-- 4 files changed, 63 insertions(+), 38 deletions(-) diff --git a/openedx/core/djangoapps/content/course_overviews/models.py b/openedx/core/djangoapps/content/course_overviews/models.py index 0453dd539c..bcb7d42609 100644 --- a/openedx/core/djangoapps/content/course_overviews/models.py +++ b/openedx/core/djangoapps/content/course_overviews/models.py @@ -796,6 +796,13 @@ class CourseOverview(TimeStampedModel): """ return self._original_course.enable_ccx + @property + def course_visibility(self): + """ + TODO: move this to the model. + """ + return self._original_course.course_visibility + def __str__(self): """Represent ourselves with the course key.""" return six.text_type(self.id) diff --git a/openedx/core/djangoapps/courseware_api/serializers.py b/openedx/core/djangoapps/courseware_api/serializers.py index ea983641ab..46e0698811 100644 --- a/openedx/core/djangoapps/courseware_api/serializers.py +++ b/openedx/core/djangoapps/courseware_api/serializers.py @@ -7,7 +7,6 @@ from rest_framework import serializers from lms.djangoapps.courseware.tabs import get_course_tab_list from openedx.core.lib.api.fields import AbsoluteURLField -from student.models import CourseEnrollment class _MediaSerializer(serializers.Serializer): # pylint: disable=abstract-method @@ -78,7 +77,8 @@ class CourseInfoSerializer(serializers.Serializer): # pylint: disable=abstract- start_display = serializers.CharField() start_type = serializers.CharField() pacing = serializers.CharField() - enrollment = serializers.SerializerMethodField() + enrollment = serializers.DictField() + user_has_access = serializers.BooleanField() tabs = serializers.SerializerMethodField() def __init__(self, *args, **kwargs): @@ -108,13 +108,3 @@ class CourseInfoSerializer(serializers.Serializer): # pylint: disable=abstract- 'url': tab.link_func(course_overview, reverse), }) return tabs - - def get_enrollment(self, course_overview): - """ - Return the enrollment for the logged in user. - """ - mode, is_active = CourseEnrollment.enrollment_mode_for_user( - course_overview.effective_user, - course_overview.id - ) - return {'mode': mode, 'is_active': is_active} diff --git a/openedx/core/djangoapps/courseware_api/tests/test_views.py b/openedx/core/djangoapps/courseware_api/tests/test_views.py index 4a93e52ff6..b8f2645f01 100644 --- a/openedx/core/djangoapps/courseware_api/tests/test_views.py +++ b/openedx/core/djangoapps/courseware_api/tests/test_views.py @@ -4,6 +4,7 @@ Tests for courseware API from datetime import datetime import unittest import ddt +import mock from django.conf import settings @@ -52,36 +53,43 @@ class BaseCoursewareTests(SharedModuleStoreTestCase): super().setUp() self.client.login(username=self.user.username, password='foo') - def test_unauth(self): - self.client.logout() - response = self.client.get(self.url) - assert response.status_code == 401 - -# pylint: disable=test-inherits-tests @ddt.ddt class CourseApiTestViews(BaseCoursewareTests): """ Tests for the courseware REST API """ - @ddt.data((None,), ('audit',), ('verified',)) + @ddt.data( + (True, None, False), + (True, 'audit', False), + (True, 'verified', False), + (False, None, False), + (False, None, True), + ) @ddt.unpack - def test_course_metadata(self, enrollment_mode): - if enrollment_mode: - CourseEnrollment.enroll(self.user, self.course.id, enrollment_mode) - response = self.client.get(self.url) - assert response.status_code == 200 - enrollment = response.data['enrollment'] - if enrollment_mode: - assert enrollment_mode == enrollment['mode'] - assert enrollment['is_active'] - assert len(response.data['tabs']) == 4 - else: - assert len(response.data['tabs']) == 2 - assert not enrollment['is_active'] + def test_course_metadata(self, logged_in, enrollment_mode, enable_anonymous): + allow_public_access = mock.Mock() + allow_public_access.return_value = enable_anonymous + with mock.patch('openedx.core.djangoapps.courseware_api.views.allow_public_access', allow_public_access): + if not logged_in: + self.client.logout() + if enrollment_mode: + CourseEnrollment.enroll(self.user, self.course.id, enrollment_mode) + response = self.client.get(self.url) + assert response.status_code == 200 + if enrollment_mode: + enrollment = response.data['enrollment'] + assert enrollment_mode == enrollment['mode'] + assert enrollment['is_active'] + assert len(response.data['tabs']) == 4 + elif enable_anonymous and not logged_in: + allow_public_access.assert_called_once() + assert response.data['enrollment']['mode'] is None + assert response.data['user_has_access'] + else: + assert not response.data['user_has_access'] -# pylint: disable=test-inherits-tests class SequenceApiTestViews(BaseCoursewareTests): """ Tests for the sequence REST API diff --git a/openedx/core/djangoapps/courseware_api/views.py b/openedx/core/djangoapps/courseware_api/views.py index c842f1be0c..59838012bc 100644 --- a/openedx/core/djangoapps/courseware_api/views.py +++ b/openedx/core/djangoapps/courseware_api/views.py @@ -10,14 +10,17 @@ from rest_framework.response import Response from rest_framework.views import APIView from lms.djangoapps.course_api.api import course_detail +from lms.djangoapps.courseware.courses import allow_public_access from lms.djangoapps.courseware.module_render import get_module_by_usage_id -from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, view_auth_classes +from student.models import CourseEnrollment + +from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin +from xmodule.course_module import COURSE_VISIBILITY_PUBLIC from .serializers import CourseInfoSerializer -@view_auth_classes(is_authenticated=True) -class CoursewareInformation(DeveloperErrorViewMixin, RetrieveAPIView): +class CoursewareInformation(RetrieveAPIView): """ **Use Cases** @@ -57,6 +60,7 @@ class CoursewareInformation(DeveloperErrorViewMixin, RetrieveAPIView): * enrollment: Enrollment status of authenticated user * mode: `audit`, `verified`, etc * is_active: boolean + * user_has_access: Whether the user can view the course **Parameters:** @@ -80,11 +84,28 @@ class CoursewareInformation(DeveloperErrorViewMixin, RetrieveAPIView): Return the requested course object, if the user has appropriate permissions. """ - return course_detail( + + overview = course_detail( self.request, self.request.user.username, CourseKey.from_string(self.kwargs['course_key_string']), ) + if self.request.user.is_anonymous: + mode = None + is_active = False + else: + mode, is_active = CourseEnrollment.enrollment_mode_for_user( + overview.effective_user, + overview.id + ) + + overview.enrollment = {'mode': mode, 'is_active': is_active} + if not is_active: + user_has_access = allow_public_access(overview, [COURSE_VISIBILITY_PUBLIC]) + else: + user_has_access = True + overview.user_has_access = user_has_access + return overview def get_serializer_context(self): """ @@ -95,7 +116,6 @@ class CoursewareInformation(DeveloperErrorViewMixin, RetrieveAPIView): return context -@view_auth_classes(is_authenticated=True) class SequenceMetadata(DeveloperErrorViewMixin, APIView): """ **Use Cases**