diff --git a/openedx/features/content_type_gating/partitions.py b/openedx/features/content_type_gating/partitions.py index 4fea2b1631..717318994d 100644 --- a/openedx/features/content_type_gating/partitions.py +++ b/openedx/features/content_type_gating/partitions.py @@ -9,13 +9,19 @@ import logging import crum from django.apps import apps +from django.conf import settings from django.template.loader import render_to_string from django.utils.translation import ugettext_lazy as _ from web_fragments.fragment import Fragment from course_modes.models import CourseMode +from courseware.masquerade import ( + is_masquerading_as_specific_student, + is_masquerading_as_student, + get_course_masquerade, +) from lms.djangoapps.commerce.utils import EcommerceService -from xmodule.partitions.partitions import UserPartition, UserPartitionError +from xmodule.partitions.partitions import UserPartition, UserPartitionError, ENROLLMENT_TRACK_PARTITION_ID from openedx.core.lib.mobile_utils import is_request_from_mobile_app from openedx.features.content_type_gating.models import ContentTypeGatingConfig from openedx.features.content_type_gating.helpers import CONTENT_GATING_PARTITION_ID, FULL_ACCESS, LIMITED_ACCESS @@ -73,7 +79,8 @@ class ContentTypeGatingPartition(UserPartition): to gated content. """ def access_denied_fragment(self, block, user, user_group, allowed_groups): - modes = CourseMode.modes_for_course_dict(block.scope_ids.usage_id.course_key) + course_key = self._get_course_key_from_course_block(block) + modes = CourseMode.modes_for_course_dict(course_key) verified_mode = modes.get(CourseMode.VERIFIED) if verified_mode is None or not self._is_audit_enrollment(user, block): return None @@ -93,8 +100,41 @@ class ContentTypeGatingPartition(UserPartition): return None def _is_audit_enrollment(self, user, block): + """ + Checks if user is enrolled in `Audit` track of course or any staff member is + viewing course as in `Audit` enrollment. + """ + course_key = self._get_course_key_from_course_block(block) + + if self._is_masquerading_as_generic_student(user, course_key): + return self._is_masquerading_audit_enrollment(user, course_key) + return self._has_active_enrollment_in_audit_mode(user, course_key) + + def _is_masquerading_as_generic_student(self, user, course_key): + """ + Checks if user is masquerading as a generic student. + """ + return ( + is_masquerading_as_student(user, course_key) and + not is_masquerading_as_specific_student(user, course_key) + ) + + def _is_masquerading_audit_enrollment(self, user, course_key): + """ + Checks if user is masquerading as learners in `Audit` enrollment track. + """ + course_masquerade = get_course_masquerade(user, course_key) + if course_masquerade.user_partition_id == ENROLLMENT_TRACK_PARTITION_ID: + audit_mode_id = settings.COURSE_ENROLLMENT_MODES.get(CourseMode.AUDIT, {}).get('id') + return course_masquerade.group_id == audit_mode_id + return False + + def _has_active_enrollment_in_audit_mode(self, user, course_key): + """ + Checks if user has an audit and active enrollment in the given course. + """ course_enrollment = apps.get_model('student.CourseEnrollment') - mode_slug, is_active = course_enrollment.enrollment_mode_for_user(user, block.scope_ids.usage_id.course_key) + mode_slug, is_active = course_enrollment.enrollment_mode_for_user(user, course_key) return mode_slug == CourseMode.AUDIT and is_active def _get_checkout_link(self, user, sku): @@ -103,6 +143,12 @@ class ContentTypeGatingPartition(UserPartition): if ecommerce_checkout and sku: return ecomm_service.get_checkout_page_url(sku) or '' + def _get_course_key_from_course_block(self, block): + """ + Extracts and returns `course_key` from `block` + """ + return block.scope_ids.usage_id.course_key + class ContentTypeGatingPartitionScheme(object): """ diff --git a/openedx/features/content_type_gating/tests/test_partitions.py b/openedx/features/content_type_gating/tests/test_partitions.py index 9cbf4af8ff..9ab84073ad 100644 --- a/openedx/features/content_type_gating/tests/test_partitions.py +++ b/openedx/features/content_type_gating/tests/test_partitions.py @@ -1,12 +1,20 @@ from datetime import datetime from mock import Mock, patch +from django.conf import settings +from django.test import RequestFactory +from courseware.masquerade import CourseMasquerade +from course_modes.tests.factories import CourseModeFactory +from lms.djangoapps.courseware.tests.factories import GlobalStaffFactory from opaque_keys.edx.keys import CourseKey from openedx.core.djangolib.testing.utils import CacheIsolationTestCase from openedx.features.content_type_gating.helpers import CONTENT_GATING_PARTITION_ID -from openedx.features.content_type_gating.partitions import create_content_gating_partition +from openedx.features.content_type_gating.partitions import ( + create_content_gating_partition, + ContentTypeGatingPartition +) from openedx.features.content_type_gating.models import ContentTypeGatingConfig -from xmodule.partitions.partitions import UserPartitionError +from xmodule.partitions.partitions import UserPartitionError, ENROLLMENT_TRACK_PARTITION_ID class TestContentTypeGatingPartition(CacheIsolationTestCase): @@ -53,3 +61,38 @@ class TestContentTypeGatingPartition(CacheIsolationTestCase): partition = create_content_gating_partition(mock_course) mock_log.warning.assert_called() self.assertIsNone(partition) + + def test_access_denied_fragment_for_masquerading(self): + """ + Test that a global staff sees gated content flag when viewing course as `Learner in Audit` + Note: Global staff doesn't require to be enrolled in course. + """ + mock_request = RequestFactory().get('/') + mock_course = Mock(id=self.course_key, user_partitions={}) + mock_block = Mock(scope_ids=Mock(usage_id=Mock(course_key=mock_course.id))) + mock_course_masquerade = Mock( + role='student', + user_partition_id=ENROLLMENT_TRACK_PARTITION_ID, + group_id=settings.COURSE_ENROLLMENT_MODES['audit']['id'], + user_name=None + ) + CourseModeFactory.create(course_id=mock_course.id, mode_slug='verified') + + global_staff = GlobalStaffFactory.create() + ContentTypeGatingConfig.objects.create(enabled=False, studio_override_enabled=True) + + partition = create_content_gating_partition(mock_course) + + with patch( + 'courseware.masquerade.get_course_masquerade', + return_value=mock_course_masquerade + ), patch( + 'openedx.features.content_type_gating.partitions.get_course_masquerade', + return_value=mock_course_masquerade + ), patch( + 'crum.get_current_request', + return_value=mock_request + ): + fragment = partition.access_denied_fragment(mock_block, global_staff, 'test_group', 'test_allowed_group') + + self.assertIsNotNone(fragment)