From 009c1f7901255e04dfa1eda3b290ccfcafeb9f0d Mon Sep 17 00:00:00 2001
From: Gabe Mulley <gabe@edx.org>
Date: Wed, 14 Nov 2018 16:35:34 -0500
Subject: [PATCH] REVE-81: fix errors in drag and drop and ORA2 xblocks

Also fixes REVE-82
---
 lms/djangoapps/courseware/field_overrides.py  |   5 +-
 .../content_type_gating/field_override.py     |  22 ++-
 .../content_type_gating/tests/test_access.py  | 126 +++++++++---------
 3 files changed, 83 insertions(+), 70 deletions(-)

diff --git a/lms/djangoapps/courseware/field_overrides.py b/lms/djangoapps/courseware/field_overrides.py
index 2340f01a5ec..d2c3d93ffa8 100644
--- a/lms/djangoapps/courseware/field_overrides.py
+++ b/lms/djangoapps/courseware/field_overrides.py
@@ -102,8 +102,9 @@ class FieldOverrideProvider(object):
     """
     __metaclass__ = ABCMeta
 
-    def __init__(self, user):
+    def __init__(self, user, fallback_field_data):
         self.user = user
+        self.fallback_field_data = fallback_field_data
 
     @abstractmethod
     def get(self, block, name, default):  # pragma no cover
@@ -196,7 +197,7 @@ class OverrideFieldData(FieldData):
 
     def __init__(self, user, fallback, providers):
         self.fallback = fallback
-        self.providers = tuple(provider(user) for provider in providers)
+        self.providers = tuple(provider(user, fallback) for provider in providers)
 
     def get_override(self, block, name):
         """
diff --git a/openedx/features/content_type_gating/field_override.py b/openedx/features/content_type_gating/field_override.py
index c01d0c13aa0..42d22a811a5 100644
--- a/openedx/features/content_type_gating/field_override.py
+++ b/openedx/features/content_type_gating/field_override.py
@@ -4,7 +4,7 @@ students in the Unlocked Group of the ContentTypeGating partition.
 """
 from django.conf import settings
 
-from lms.djangoapps.courseware.field_overrides import FieldOverrideProvider, disable_overrides
+from lms.djangoapps.courseware.field_overrides import FieldOverrideProvider
 from openedx.features.content_type_gating.partitions import CONTENT_GATING_PARTITION_ID
 from openedx.features.course_duration_limits.config import (
     CONTENT_TYPE_GATING_FLAG,
@@ -31,9 +31,23 @@ class ContentTypeGatingFieldOverride(FieldOverrideProvider):
         if not problem_eligible_for_content_gating:
             return default
 
-        # Read the group_access from the fallback field-data service
-        with disable_overrides():
-            original_group_access = block.group_access
+        # We want to fetch the value set by course authors since it should take precedence.
+        # We cannot simply call "block.group_access" to fetch that value even if we disable
+        # field overrides since it will set the group access field to "dirty" with
+        # the value read from the course content. Since most content does not have any
+        # value for this field it will usually be the default empty dict. This field
+        # override changes the value, however, resulting in the LMS thinking that the
+        # field data needs to be written back out to the store. This doesn't work,
+        # however, since this is a read-only setting in the LMS context. After this
+        # call to get() returns, the _dirty_fields dict will be set correctly to contain
+        # the value from this field override. This prevents the system from attempting
+        # to save the overridden value when it thinks it has changed when it hasn't.
+        original_group_access = None
+        if self.fallback_field_data.has(block, 'group_access'):
+            raw_value = self.fallback_field_data.get(block, 'group_access')
+            group_access_field = block.fields.get('group_access')
+            if group_access_field is not None:
+                original_group_access = group_access_field.from_json(raw_value)
 
         if original_group_access is None:
             original_group_access = {}
diff --git a/openedx/features/content_type_gating/tests/test_access.py b/openedx/features/content_type_gating/tests/test_access.py
index 2ad97ffa9ce..3fb1a8ea0f1 100644
--- a/openedx/features/content_type_gating/tests/test_access.py
+++ b/openedx/features/content_type_gating/tests/test_access.py
@@ -3,17 +3,17 @@ Test audit user's access to various content based on content-gating features.
 """
 
 import ddt
-from django.http import Http404
+from django.conf import settings
 from django.test.client import RequestFactory
 from django.test.utils import override_settings
 from django.urls import reverse
 from mock import patch
 
 from course_modes.tests.factories import CourseModeFactory
-from courseware.access_response import IncorrectPartitionGroupError
 from lms.djangoapps.courseware.module_render import load_single_xblock
 from openedx.core.djangoapps.waffle_utils.testutils import override_waffle_flag
 from openedx.core.lib.url_utils import quote_slashes
+from openedx.features.content_type_gating.partitions import CONTENT_GATING_PARTITION_ID
 from openedx.features.course_duration_limits.config import CONTENT_TYPE_GATING_FLAG
 from student.tests.factories import TEST_PASSWORD, AdminFactory, CourseEnrollmentFactory, UserFactory
 from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
@@ -82,14 +82,14 @@ class TestProblemTypeAccess(SharedModuleStoreTestCase):
             cls.graded_score_weight_blocks[(graded, has_score, weight)] = block
 
         # add LTI blocks to default course
-        cls.lti_block = ItemFactory.create(
+        cls.blocks_dict['lti_block'] = ItemFactory.create(
             parent=cls.blocks_dict['vertical'],
             category='lti_consumer',
             display_name='lti_consumer',
             has_score=True,
             graded=True,
         )
-        cls.lti_block_not_scored = ItemFactory.create(
+        cls.blocks_dict['lti_block_not_scored'] = ItemFactory.create(
             parent=cls.blocks_dict['vertical'],
             category='lti_consumer',
             display_name='lti_consumer_2',
@@ -97,19 +97,32 @@ class TestProblemTypeAccess(SharedModuleStoreTestCase):
         )
 
         # add ungraded problem for xblock_handler test
-        cls.graded_problem = ItemFactory.create(
+        cls.blocks_dict['graded_problem'] = ItemFactory.create(
             parent=cls.blocks_dict['vertical'],
             category='problem',
             display_name='graded_problem',
             graded=True,
         )
-        cls.ungraded_problem = ItemFactory.create(
+        cls.blocks_dict['ungraded_problem'] = ItemFactory.create(
             parent=cls.blocks_dict['vertical'],
             category='problem',
             display_name='ungraded_problem',
             graded=False,
         )
 
+        cls.blocks_dict['audit_visible_graded_problem'] = ItemFactory.create(
+            parent=cls.blocks_dict['vertical'],
+            category='problem',
+            display_name='audit_visible_graded_problem',
+            graded=True,
+            group_access={
+                CONTENT_GATING_PARTITION_ID: [
+                    settings.CONTENT_TYPE_GATE_GROUP_IDS['limited_access'],
+                    settings.CONTENT_TYPE_GATE_GROUP_IDS['full_access']
+                ]
+            },
+        )
+
         # audit_only course only has an audit track available
         cls.courses['audit_only'] = cls._create_course(
             run='audit_only_course_run_1',
@@ -205,14 +218,14 @@ class TestProblemTypeAccess(SharedModuleStoreTestCase):
                 display_name='Lesson 1 Vertical - Unit 1'
             )
 
-            for problem_type in component_types:
+            for component_type in component_types:
                 block = ItemFactory.create(
                     parent=blocks_dict['vertical'],
-                    category=problem_type,
-                    display_name=problem_type,
+                    category=component_type,
+                    display_name=component_type,
                     graded=True,
                 )
-                blocks_dict[problem_type] = block
+                blocks_dict[component_type] = block
 
             return {
                 'course': course,
@@ -224,14 +237,8 @@ class TestProblemTypeAccess(SharedModuleStoreTestCase):
         """
         Asserts that a block in a specific course is gated for a specific user
 
-        This functions asserts whether the passed in block is gated by content type gating.
-        This is determined by checking whether the has_access method called the IncorrectPartitionGroupError.
-        This error gets swallowed up and is raised as a 404, which is why we are checking for a 404 being raised.
-        However, the 404 could also be caused by other errors, which is why the actual assertion is checking
-        whether the IncorrectPartitionGroupError was called.
-
         Arguments:
-            block: some soft of xblock descriptor, must implement .scope_ids.usage_id
+            block: some sort of xblock descriptor, must implement .scope_ids.usage_id
             is_gated (bool): if True, this user is expected to be gated from this block
             user_id (int): id of user, if not set will be set to self.audit_user.id
             course_id (CourseLocator): id of course, if not set will be set to self.course.id
@@ -239,61 +246,52 @@ class TestProblemTypeAccess(SharedModuleStoreTestCase):
         fake_request = self.factory.get('')
         mock_get_current_request.return_value = fake_request
 
-        with patch.object(IncorrectPartitionGroupError, '__init__',
-                          wraps=IncorrectPartitionGroupError.__init__) as mock_access_error:
-            if is_gated:
-                with self.assertRaises(Http404):
-                    load_single_xblock(
-                        request=fake_request,
-                        user_id=user_id,
-                        course_id=unicode(course_id),
-                        usage_key_string=unicode(block.scope_ids.usage_id),
-                        course=None
-                    )
-                # check that has_access raised the IncorrectPartitionGroupError in order to gate the block
-                self.assertTrue(mock_access_error.called)
-            else:
-                load_single_xblock(
-                    request=fake_request,
-                    user_id=user_id,
-                    course_id=unicode(course_id),
-                    usage_key_string=unicode(block.scope_ids.usage_id),
-                    course=None
-                )
-                # check that has_access did not raise the IncorrectPartitionGroupError thereby not gating the block
-                self.assertFalse(mock_access_error.called)
-
-    def test_lti_audit_access(self):
-        """
-        LTI stands for learning tools interoperability and is a 3rd party iframe that pulls in learning content from
-        outside sources. This tests that audit users cannot see LTI components with graded content but can see the LTI
-        components which do not have graded content.
-        """
-        self._assert_block_is_gated(
-            block=self.lti_block,
-            user_id=self.audit_user.id,
-            course_id=self.course.id,
-            is_gated=True
-        )
-        self._assert_block_is_gated(
-            block=self.lti_block_not_scored,
-            user_id=self.audit_user.id,
-            course_id=self.course.id,
-            is_gated=False
+        # Load a block we know will pass access control checks
+        vertical_xblock = load_single_xblock(
+            request=fake_request,
+            user_id=user_id,
+            course_id=unicode(course_id),
+            usage_key_string=unicode(self.blocks_dict['vertical'].scope_ids.usage_id),
+            course=None
         )
 
+        runtime = vertical_xblock.runtime
+
+        # This method of fetching the block from the descriptor bypassess access checks
+        problem_block = runtime.get_module(block)
+
+        # Attempt to render the block, this should return different fragments if the content is gated or not.
+        frag = runtime.render(problem_block, 'student_view')
+        if is_gated:
+            assert 'content-paywall' in frag.content
+        else:
+            assert 'content-paywall' not in frag.content
+
     @ddt.data(
-        *PROBLEM_TYPES
+        ('problem', True),
+        ('openassessment', True),
+        ('drag-and-drop-v2', True),
+        ('done', True),
+        ('edx_sga', True),
+        ('lti_block', True),
+        ('ungraded_problem', False),
+        ('lti_block_not_scored', False),
+        ('audit_visible_graded_problem', False),
     )
-    def test_audit_fails_access_graded_problems(self, prob_type):
-        block = self.blocks_dict[prob_type]
-        is_gated = True
+    @ddt.unpack
+    def test_access_to_problems(self, prob_type, is_gated):
         self._assert_block_is_gated(
-            block=block,
-            user_id=self.audit_user.id,
+            block=self.blocks_dict[prob_type],
+            user_id=self.users['audit'].id,
             course_id=self.course.id,
             is_gated=is_gated
         )
+        self._assert_block_is_gated(
+            block=self.blocks_dict[prob_type],
+            user_id=self.users['verified'].id,
+            course_id=self.course.id,
+            is_gated=False
+        )
 
     @ddt.data(
         *GRADED_SCORE_WEIGHT_TEST_CASES
-- 
GitLab