diff --git a/lms/djangoapps/courseware/access.py b/lms/djangoapps/courseware/access.py index 0c554ca4f8..7e3393d510 100644 --- a/lms/djangoapps/courseware/access.py +++ b/lms/djangoapps/courseware/access.py @@ -33,9 +33,25 @@ from xmodule.x_module import XModule from xmodule.split_test_module import get_split_user_partitions from xmodule.partitions.partitions import NoSuchUserPartitionError, NoSuchUserPartitionGroupError -from openedx.core.djangoapps.external_auth.models import ExternalAuthMap +from courseware.access_response import ( + MilestoneError, + MobileAvailabilityError, + VisibilityError, +) +from courseware.access_utils import ( + ACCESS_DENIED, + ACCESS_GRANTED, + adjust_start_date, + check_start_date, + debug, + in_preview_mode +) from courseware.masquerade import get_masquerade_role, is_masquerading_as_student +from lms.djangoapps.ccx.custom_exception import CCXLocatorValidationException +from lms.djangoapps.ccx.models import CustomCourseForEdX +from mobile_api.models import IgnoreMobileAvailableFlagConfig from openedx.core.djangoapps.content.course_overviews.models import CourseOverview +from openedx.core.djangoapps.external_auth.models import ExternalAuthMap from student import auth from student.models import CourseEnrollmentAllowed from student.roles import ( @@ -55,19 +71,6 @@ from util.milestones_helpers import ( ) from ccx_keys.locator import CCXLocator -from courseware.access_response import ( - MilestoneError, - MobileAvailabilityError, - VisibilityError, -) -from courseware.access_utils import ( - adjust_start_date, check_start_date, debug, ACCESS_GRANTED, ACCESS_DENIED, - in_preview_mode -) - -from lms.djangoapps.ccx.custom_exception import CCXLocatorValidationException -from lms.djangoapps.ccx.models import CustomCourseForEdX - log = logging.getLogger(__name__) @@ -849,7 +852,10 @@ def _is_descriptor_mobile_available(descriptor): """ Returns if descriptor is available on mobile. """ - return ACCESS_GRANTED if descriptor.mobile_available else MobileAvailabilityError() + if IgnoreMobileAvailableFlagConfig.is_enabled() or descriptor.mobile_available: + return ACCESS_GRANTED + else: + return MobileAvailabilityError() def is_mobile_available_for_user(user, descriptor): diff --git a/lms/djangoapps/mobile_api/admin.py b/lms/djangoapps/mobile_api/admin.py index b73bd523a3..84ae1387a0 100644 --- a/lms/djangoapps/mobile_api/admin.py +++ b/lms/djangoapps/mobile_api/admin.py @@ -4,9 +4,14 @@ Django admin dashboard configuration for LMS XBlock infrastructure. from django.contrib import admin from config_models.admin import ConfigurationModelAdmin -from mobile_api.models import MobileApiConfig, AppVersionConfig +from .models import ( + AppVersionConfig, + MobileApiConfig, + IgnoreMobileAvailableFlagConfig +) admin.site.register(MobileApiConfig, ConfigurationModelAdmin) +admin.site.register(IgnoreMobileAvailableFlagConfig, ConfigurationModelAdmin) class AppVersionConfigAdmin(admin.ModelAdmin): diff --git a/lms/djangoapps/mobile_api/course_info/views.py b/lms/djangoapps/mobile_api/course_info/views.py index c5b06dc74c..13557db83b 100644 --- a/lms/djangoapps/mobile_api/course_info/views.py +++ b/lms/djangoapps/mobile_api/course_info/views.py @@ -9,7 +9,7 @@ from courseware.courses import get_course_info_section_module from static_replace import make_static_urls_absolute from openedx.core.lib.xblock_utils import get_course_update_items -from ..utils import mobile_view, mobile_course_access +from ..decorators import mobile_course_access, mobile_view @mobile_view() diff --git a/lms/djangoapps/mobile_api/decorators.py b/lms/djangoapps/mobile_api/decorators.py new file mode 100644 index 0000000000..58cb99d73e --- /dev/null +++ b/lms/djangoapps/mobile_api/decorators.py @@ -0,0 +1,52 @@ +""" +Decorators for Mobile APIs. +""" +import functools +from rest_framework import status +from rest_framework.response import Response + +from lms.djangoapps.courseware.courses import get_course_with_access +from lms.djangoapps.courseware.courseware_access_exception import CoursewareAccessException +from opaque_keys.edx.keys import CourseKey +from xmodule.modulestore.django import modulestore + +from openedx.core.lib.api.view_utils import view_auth_classes + + +def mobile_course_access(depth=0): + """ + Method decorator for a mobile API endpoint that verifies the user has access to the course in a mobile context. + """ + def _decorator(func): + """Outer method decorator.""" + + @functools.wraps(func) + def _wrapper(self, request, *args, **kwargs): + """ + Expects kwargs to contain 'course_id'. + Passes the course descriptor to the given decorated function. + Raises 404 if access to course is disallowed. + """ + course_id = CourseKey.from_string(kwargs.pop('course_id')) + with modulestore().bulk_operations(course_id): + try: + course = get_course_with_access( + request.user, + 'load_mobile', + course_id, + depth=depth, + check_if_enrolled=True, + ) + except CoursewareAccessException as error: + return Response(data=error.to_json(), status=status.HTTP_404_NOT_FOUND) + return func(self, request, course=course, *args, **kwargs) + + return _wrapper + return _decorator + + +def mobile_view(is_user=False): + """ + Function and class decorator that abstracts the authentication and permission checks for mobile api views. + """ + return view_auth_classes(is_user) diff --git a/lms/djangoapps/mobile_api/migrations/0003_ignore_mobile_available_flag.py b/lms/djangoapps/mobile_api/migrations/0003_ignore_mobile_available_flag.py new file mode 100644 index 0000000000..abfc30b8bc --- /dev/null +++ b/lms/djangoapps/mobile_api/migrations/0003_ignore_mobile_available_flag.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('mobile_api', '0002_auto_20160406_0904'), + ] + + operations = [ + migrations.CreateModel( + name='IgnoreMobileAvailableFlagConfig', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('change_date', models.DateTimeField(auto_now_add=True, verbose_name='Change date')), + ('enabled', models.BooleanField(default=False, verbose_name='Enabled')), + ('changed_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, editable=False, to=settings.AUTH_USER_MODEL, null=True, verbose_name='Changed by')), + ], + ), + migrations.AlterModelOptions( + name='mobileapiconfig', + options={}, + ), + ] diff --git a/lms/djangoapps/mobile_api/models.py b/lms/djangoapps/mobile_api/models.py index 2d30a9ce91..05744706d4 100644 --- a/lms/djangoapps/mobile_api/models.py +++ b/lms/djangoapps/mobile_api/models.py @@ -2,9 +2,10 @@ ConfigurationModel for the mobile_api djangoapp. """ from django.db import models -from mobile_api import utils + from config_models.models import ConfigurationModel -from mobile_api.mobile_platform import PLATFORM_CLASSES +from .mobile_platform import PLATFORM_CLASSES +from . import utils class MobileApiConfig(ConfigurationModel): @@ -19,6 +20,9 @@ class MobileApiConfig(ConfigurationModel): help_text="A comma-separated list of names of profiles to include for videos returned from the mobile API." ) + class Meta(object): + app_label = "mobile_api" + @classmethod def get_video_profiles(cls): """ @@ -50,6 +54,7 @@ class AppVersionConfig(models.Model): updated_at = models.DateTimeField(auto_now=True) class Meta: + app_label = "mobile_api" unique_together = ('platform', 'version',) ordering = ['-major_version', '-minor_version', '-patch_version'] @@ -76,3 +81,16 @@ class AppVersionConfig(models.Model): """ parses version into major, minor and patch versions before saving """ self.major_version, self.minor_version, self.patch_version = utils.parsed_version(self.version) super(AppVersionConfig, self).save(*args, **kwargs) + + +class IgnoreMobileAvailableFlagConfig(ConfigurationModel): # pylint: disable=W5101 + """ + Configuration for the mobile_available flag. Default is false. + + Enabling this configuration will cause the mobile_available flag check in + access.py._is_descriptor_mobile_available to ignore the mobile_available + flag. + """ + + class Meta(object): + app_label = "mobile_api" diff --git a/lms/djangoapps/mobile_api/tests/test_decorator.py b/lms/djangoapps/mobile_api/tests/test_decorator.py index 5b30f72a50..55ca77ff8a 100644 --- a/lms/djangoapps/mobile_api/tests/test_decorator.py +++ b/lms/djangoapps/mobile_api/tests/test_decorator.py @@ -6,7 +6,7 @@ Tests for mobile API utilities. import ddt from django.test import TestCase -from mobile_api.utils import mobile_course_access, mobile_view +from ..decorators import mobile_course_access, mobile_view @ddt.ddt diff --git a/lms/djangoapps/mobile_api/testutils.py b/lms/djangoapps/mobile_api/testutils.py index 6cf07841ea..35fc627cad 100644 --- a/lms/djangoapps/mobile_api/testutils.py +++ b/lms/djangoapps/mobile_api/testutils.py @@ -30,6 +30,7 @@ from courseware.access_response import ( from courseware.tests.factories import UserFactory from student import auth from student.models import CourseEnrollment +from mobile_api.models import IgnoreMobileAvailableFlagConfig from mobile_api.tests.test_milestones import MobileAPIMilestonesMixin @@ -46,6 +47,7 @@ class MobileAPITestCase(ModuleStoreTestCase, APITestCase): self.user = UserFactory.create() self.password = 'test' self.username = self.user.username + IgnoreMobileAvailableFlagConfig(enabled=False).save() def tearDown(self): super(MobileAPITestCase, self).tearDown() @@ -188,12 +190,21 @@ class MobileCourseAccessTestMixin(MobileAPIMilestonesMixin): @ddt.unpack @patch.dict(settings.FEATURES, {'ENABLE_MKTG_SITE': True}) def test_non_mobile_available(self, role, should_succeed): + """ + Tests that the MobileAvailabilityError() is raised for certain user + roles when trying to access course content. Also verifies that if + the IgnoreMobileAvailableFlagConfig is enabled, + MobileAvailabilityError() will not be raised for all user roles. + """ self.init_course_access() # set mobile_available to False for the test course self.course.mobile_available = False self.store.update_item(self.course, self.user.id) self._verify_response(should_succeed, MobileAvailabilityError(), role) + IgnoreMobileAvailableFlagConfig(enabled=True).save() + self._verify_response(True, MobileAvailabilityError(), role) + def test_unenrolled_user(self): self.login() self.unenroll() diff --git a/lms/djangoapps/mobile_api/users/serializers.py b/lms/djangoapps/mobile_api/users/serializers.py index e759f358e5..729637699e 100644 --- a/lms/djangoapps/mobile_api/users/serializers.py +++ b/lms/djangoapps/mobile_api/users/serializers.py @@ -5,9 +5,9 @@ from opaque_keys.edx.keys import CourseKey from rest_framework import serializers from rest_framework.reverse import reverse +from certificates.api import certificate_downloadable_status from courseware.access import has_access from student.models import CourseEnrollment, User -from certificates.api import certificate_downloadable_status from util.course import get_lms_link_for_about_page diff --git a/lms/djangoapps/mobile_api/users/tests.py b/lms/djangoapps/mobile_api/users/tests.py index da40f21ca0..b283b5c484 100644 --- a/lms/djangoapps/mobile_api/users/tests.py +++ b/lms/djangoapps/mobile_api/users/tests.py @@ -37,6 +37,7 @@ from mobile_api.testutils import ( MobileAuthUserTestMixin, MobileCourseAccessTestMixin, ) + from .serializers import CourseEnrollmentSerializer diff --git a/lms/djangoapps/mobile_api/users/views.py b/lms/djangoapps/mobile_api/users/views.py index 3d8b4fba51..b1aee073ba 100644 --- a/lms/djangoapps/mobile_api/users/views.py +++ b/lms/djangoapps/mobile_api/users/views.py @@ -26,7 +26,7 @@ from xmodule.modulestore.exceptions import ItemNotFoundError from .serializers import CourseEnrollmentSerializer, UserSerializer from .. import errors -from ..utils import mobile_view, mobile_course_access +from ..decorators import mobile_course_access, mobile_view @mobile_view(is_user=True) @@ -60,8 +60,7 @@ class UserDetail(generics.RetrieveAPIView): * username: The username of the currently signed in user. """ queryset = ( - User.objects.all() - .select_related('profile') + User.objects.all().select_related('profile') ) serializer_class = UserSerializer lookup_field = 'username' diff --git a/lms/djangoapps/mobile_api/utils.py b/lms/djangoapps/mobile_api/utils.py index a5b9ba4b8d..77d3415696 100644 --- a/lms/djangoapps/mobile_api/utils.py +++ b/lms/djangoapps/mobile_api/utils.py @@ -1,54 +1,6 @@ """ -Common utility methods and decorators for Mobile APIs. +Common utility methods for Mobile APIs. """ -import functools -from rest_framework import status -from rest_framework.response import Response - -from lms.djangoapps.courseware.courses import get_course_with_access -from lms.djangoapps.courseware.courseware_access_exception import CoursewareAccessException -from opaque_keys.edx.keys import CourseKey -from openedx.core.lib.api.view_utils import view_auth_classes -from xmodule.modulestore.django import modulestore - - -def mobile_course_access(depth=0): - """ - Method decorator for a mobile API endpoint that verifies the user has access to the course in a mobile context. - """ - def _decorator(func): - """Outer method decorator.""" - - @functools.wraps(func) - def _wrapper(self, request, *args, **kwargs): - """ - Expects kwargs to contain 'course_id'. - Passes the course descriptor to the given decorated function. - Raises 404 if access to course is disallowed. - """ - course_id = CourseKey.from_string(kwargs.pop('course_id')) - with modulestore().bulk_operations(course_id): - try: - course = get_course_with_access( - request.user, - 'load_mobile', - course_id, - depth=depth, - check_if_enrolled=True, - ) - except CoursewareAccessException as error: - return Response(data=error.to_json(), status=status.HTTP_404_NOT_FOUND) - return func(self, request, course=course, *args, **kwargs) - - return _wrapper - return _decorator - - -def mobile_view(is_user=False): - """ - Function and class decorator that abstracts the authentication and permission checks for mobile api views. - """ - return view_auth_classes(is_user) def parsed_version(version): diff --git a/lms/djangoapps/mobile_api/video_outlines/views.py b/lms/djangoapps/mobile_api/video_outlines/views.py index bfd27d0d8b..d471d40b21 100644 --- a/lms/djangoapps/mobile_api/video_outlines/views.py +++ b/lms/djangoapps/mobile_api/video_outlines/views.py @@ -18,7 +18,7 @@ from opaque_keys.edx.locator import BlockUsageLocator from xmodule.exceptions import NotFoundError from xmodule.modulestore.django import modulestore -from ..utils import mobile_view, mobile_course_access +from ..decorators import mobile_course_access, mobile_view from .serializers import BlockOutline, video_summary