diff --git a/lms/djangoapps/course_home_api/dates/v1/serializers.py b/lms/djangoapps/course_home_api/dates/v1/serializers.py index 9a8b6a37c6..0229c54009 100644 --- a/lms/djangoapps/course_home_api/dates/v1/serializers.py +++ b/lms/djangoapps/course_home_api/dates/v1/serializers.py @@ -27,10 +27,12 @@ class DateSummarySerializer(serializers.Serializer): first_component_block_id = serializers.SerializerMethodField() def get_learner_has_access(self, block): - learner_is_full_access = self.context.get('learner_is_full_access', False) - block_is_verified = (getattr(block, 'contains_gated_content', False) or - isinstance(block, VerificationDeadlineDate)) - return (not block_is_verified) or learner_is_full_access + """Whether the learner is blocked (gated) from this content or not""" + if isinstance(block, VerificationDeadlineDate): + # This date block isn't an assignment, so doesn't have contains_gated_content set for it + return self.context.get('learner_is_full_access', False) + + return not getattr(block, 'contains_gated_content', False) def get_link(self, block): if block.link: diff --git a/lms/djangoapps/course_home_api/outline/v1/tests/test_views.py b/lms/djangoapps/course_home_api/outline/v1/tests/test_views.py index 51053cfbcc..773956b9f2 100644 --- a/lms/djangoapps/course_home_api/outline/v1/tests/test_views.py +++ b/lms/djangoapps/course_home_api/outline/v1/tests/test_views.py @@ -17,7 +17,6 @@ from common.djangoapps.student.roles import CourseInstructorRole from common.djangoapps.student.tests.factories import UserFactory from lms.djangoapps.course_home_api.tests.utils import BaseCourseHomeTests from lms.djangoapps.course_home_api.toggles import COURSE_HOME_USE_LEGACY_FRONTEND -from lms.djangoapps.experiments.testutils import override_experiment_waffle_flag from openedx.core.djangoapps.content.learning_sequences.api import replace_course_outline from openedx.core.djangoapps.content.learning_sequences.data import CourseOutlineData, CourseVisibility from openedx.core.djangoapps.content.learning_sequences.toggles import USE_FOR_OUTLINES @@ -354,7 +353,6 @@ class OutlineTabTestViews(BaseCourseHomeTests): response = self.client.get(self.url) assert response.data['verified_mode'] == {'access_expiration_date': (enrollment.created + MIN_DURATION), 'currency': 'USD', 'currency_symbol': '$', 'price': 149, 'sku': 'ABCD1234', 'upgrade_url': '/dashboard'} - @override_experiment_waffle_flag(COURSE_HOME_MICROFRONTEND, active=True) def test_hide_learning_sequences(self): """ Check that Learning Sequences filters out sequences. diff --git a/lms/djangoapps/course_home_api/progress/v1/serializers.py b/lms/djangoapps/course_home_api/progress/v1/serializers.py index 6ad65767cc..59a5f66fb3 100644 --- a/lms/djangoapps/course_home_api/progress/v1/serializers.py +++ b/lms/djangoapps/course_home_api/progress/v1/serializers.py @@ -23,6 +23,7 @@ class SubsectionScoresSerializer(serializers.Serializer): display_name = serializers.CharField() block_key = serializers.SerializerMethodField() has_graded_assignment = serializers.BooleanField(source='graded') + learner_has_access = serializers.SerializerMethodField() num_points_earned = serializers.IntegerField(source='graded_total.earned') num_points_possible = serializers.IntegerField(source='graded_total.possible') percent_graded = serializers.FloatField() @@ -41,6 +42,10 @@ class SubsectionScoresSerializer(serializers.Serializer): def get_show_grades(self, subsection): return subsection.show_grades(self.context['staff_access']) + def get_learner_has_access(self, subsection): + course_blocks = self.context['course_blocks'] + return not course_blocks.get_xblock_field(subsection.location, 'contains_gated_content', False) + class SectionScoresSerializer(serializers.Serializer): """ diff --git a/lms/djangoapps/course_home_api/progress/v1/tests/test_views.py b/lms/djangoapps/course_home_api/progress/v1/tests/test_views.py index b8c14703e8..28f314783c 100644 --- a/lms/djangoapps/course_home_api/progress/v1/tests/test_views.py +++ b/lms/djangoapps/course_home_api/progress/v1/tests/test_views.py @@ -4,7 +4,6 @@ Tests for Progress Tab API in the Course Home API import dateutil import ddt -import mock from datetime import datetime, timedelta from pytz import UTC from unittest.mock import patch @@ -18,16 +17,14 @@ from common.djangoapps.student.tests.factories import UserFactory from lms.djangoapps.course_home_api.tests.utils import BaseCourseHomeTests from lms.djangoapps.course_home_api.models import DisableProgressPageStackedConfig from lms.djangoapps.course_home_api.toggles import COURSE_HOME_MICROFRONTEND_PROGRESS_TAB -from lms.djangoapps.experiments.testutils import override_experiment_waffle_flag from lms.djangoapps.verify_student.models import ManualVerification from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.djangoapps.course_date_signals.utils import MIN_DURATION -from openedx.core.djangoapps.user_api.preferences.api import set_user_preference +from openedx.features.content_type_gating.helpers import CONTENT_GATING_PARTITION_ID, CONTENT_TYPE_GATE_GROUP_IDS +from openedx.features.content_type_gating.models import ContentTypeGatingConfig from openedx.features.course_duration_limits.models import CourseDurationLimitConfig from xmodule.modulestore.tests.factories import ItemFactory -CREDIT_SUPPORT_URL = 'https://support.edx.org/hc/en-us/sections/115004154688-Purchasing-Academic-Credit' - @ddt.ddt class ProgressTabTestViews(BaseCourseHomeTests): @@ -159,3 +156,30 @@ class ProgressTabTestViews(BaseCourseHomeTests): response = self.client.get(self.url) assert response.status_code == 404 + + @override_waffle_flag(COURSE_HOME_MICROFRONTEND_PROGRESS_TAB, active=True) + def test_learner_has_access(self): + chapter = ItemFactory(parent=self.course, category='chapter') + gated = ItemFactory(parent=chapter, category='sequential') + ItemFactory.create(parent=gated, category='problem', graded=True, has_score=True) + ungated = ItemFactory(parent=chapter, category='sequential') + ItemFactory.create(parent=ungated, category='problem', graded=True, has_score=True, + group_access={ + CONTENT_GATING_PARTITION_ID: [CONTENT_TYPE_GATE_GROUP_IDS['full_access'], + CONTENT_TYPE_GATE_GROUP_IDS['limited_access']], + }) + + CourseEnrollment.enroll(self.user, self.course.id) + CourseDurationLimitConfig.objects.create(enabled=True, enabled_as_of=datetime(2018, 1, 1)) + ContentTypeGatingConfig.objects.create(enabled=True, enabled_as_of=datetime(2018, 1, 1)) + + response = self.client.get(self.url) + assert response.status_code == 200 + + sections = response.data['section_scores'] + ungraded_score = sections[0]['subsections'][0] # default sequence that parent class gives us + gated_score = sections[1]['subsections'][0] + ungated_score = sections[1]['subsections'][1] + assert ungraded_score['learner_has_access'] + assert not gated_score['learner_has_access'] + assert ungated_score['learner_has_access'] diff --git a/lms/djangoapps/course_home_api/progress/v1/views.py b/lms/djangoapps/course_home_api/progress/v1/views.py index db8f7649f2..46da3e9720 100644 --- a/lms/djangoapps/course_home_api/progress/v1/views.py +++ b/lms/djangoapps/course_home_api/progress/v1/views.py @@ -29,6 +29,7 @@ from openedx.core.djangoapps.content.block_structure.transformers import BlockSt from openedx.core.djangoapps.content.block_structure.api import get_block_structure_manager from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.lib.api.authentication import BearerAuthenticationAllowInactiveUser +from openedx.features.content_type_gating.block_transformers import ContentTypeGateTransformer class ProgressTabView(RetrieveAPIView): @@ -69,6 +70,7 @@ class ProgressTabView(RetrieveAPIView): block_key: (str) the key of the given subsection block display_name: (str) a str of what the name of the Subsection is for displaying on the site has_graded_assignment: (bool) whether or not the Subsection is a graded assignment + learner_has_access: (bool) whether the learner has access to the subsection (could be FBE gated) num_points_earned: (int) the amount of points the user has earned for the given subsection num_points_possible: (int) the total amount of points possible for the given subsection percent_graded: (float) the percentage of total points the user has received a grade for in a given subsection @@ -144,7 +146,7 @@ class ProgressTabView(RetrieveAPIView): # Get has_scheduled_content data transformers = BlockStructureTransformers() - transformers += [start_date.StartDateTransformer()] + transformers += [start_date.StartDateTransformer(), ContentTypeGateTransformer()] usage_key = collected_block_structure.root_block_usage_key course_blocks = get_course_blocks( request.user, @@ -190,6 +192,7 @@ class ProgressTabView(RetrieveAPIView): } context = self.get_serializer_context() context['staff_access'] = is_staff + context['course_blocks'] = course_blocks context['course_key'] = course_key # course_overview and enrollment will be used by VerifiedModeSerializerMixin context['course_overview'] = course_overview diff --git a/lms/envs/common.py b/lms/envs/common.py index 9205da22e1..f0051b8f39 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -2790,9 +2790,14 @@ YOUTUBE_API_KEY = 'PUT_YOUR_API_KEY_HERE' ################################### APPS ###################################### -# The order of INSTALLED_APPS is important, when adding new apps here -# remember to check that you are not creating new +# The order of INSTALLED_APPS is important, when adding new apps here remember to check that you are not creating new # RemovedInDjango19Warnings in the test logs. +# +# If you want to add a new djangoapp that isn't suitable for everyone, you have some options: +# - Add it to OPTIONAL_APPS below (registered if importable) +# - Add it to the ADDL_INSTALLED_APPS configuration variable (acts like EXTRA_APPS in other IDAs) +# - Make it a plugin (which are auto-registered) and add it to the EDXAPP_PRIVATE_REQUIREMENTS configuration variable +# (See https://github.com/edx/edx-django-utils/tree/master/edx_django_utils/plugins) INSTALLED_APPS = [ # Standard ones that are always installed... 'django.contrib.auth',