Skip to content
7 changes: 7 additions & 0 deletions lms/djangoapps/ccx/modulestore.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,3 +304,10 @@ def bulk_operations(self, course_id, emit_signals=True, ignore_case=False):
course_id, _ = strip_ccx(course_id)
with self._modulestore.bulk_operations(course_id, emit_signals=emit_signals, ignore_case=ignore_case):
yield

@contextmanager
def fresh_block_reads(self, course_key):
"""See the docs for xmodule.modulestore.mixed.MixedModuleStore"""
course_key, _ = strip_ccx(course_key)
with self._modulestore.fresh_block_reads(course_key):
yield
10 changes: 10 additions & 0 deletions lms/djangoapps/ccx/tests/test_ccx_modulestore.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,16 @@ def test_get_item(self):
assert expected.display_name == actual.display_name
assert expected.location == actual.location.to_block_locator()

def test_fresh_block_reads(self):
"""verify that fresh block reads by ccx key ignore blocks changed in memory"""
expected = self.blocks[0]
block_key = self.ccx_locator.make_usage_key(expected.location.block_type, expected.location.block_id)
with self.store.bulk_operations(self.ccx_locator):
self.store.get_item(block_key).display_name = 'Changed in memory'
with self.store.fresh_block_reads(self.ccx_locator):
assert self.store.get_item(block_key).display_name == expected.display_name
assert self.store.get_item(block_key).display_name == 'Changed in memory'

def test_publication_api(self):
"""verify that we can correctly discern a published item by ccx key"""
with self.store.bulk_operations(self.ccx_locator):
Expand Down
6 changes: 5 additions & 1 deletion lms/djangoapps/course_blocks/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from django.conf import settings
from edx_when import field_data
from openedx_filters.learning.filters import CourseBlocksTransformCompleted
Comment thread
asadali145 marked this conversation as resolved.

from lms.djangoapps.course_api.blocks.transformers.block_completion import BlockCompletionTransformer
from openedx.core.djangoapps.content.block_structure.api import get_block_structure_manager
Expand Down Expand Up @@ -104,9 +105,12 @@ def get_course_blocks(
include_has_scheduled_content
)

return get_block_structure_manager(starting_block_usage_key.course_key).get_transformed(
block_structure = get_block_structure_manager(starting_block_usage_key.course_key).get_transformed(
transformers,
starting_block_usage_key,
collected_block_structure,
user,
)
return CourseBlocksTransformCompleted.run_filter(
block_structure=block_structure, user=user, starting_block_usage_key=starting_block_usage_key
)
Comment thread
asadali145 marked this conversation as resolved.
50 changes: 50 additions & 0 deletions lms/djangoapps/course_blocks/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,33 @@

import ddt
from django.http.request import HttpRequest
from django.test import override_settings
from openedx_filters import PipelineStep

from common.djangoapps.student.tests.factories import UserFactory
from lms.djangoapps.course_blocks.api import get_course_blocks
from lms.djangoapps.course_blocks.transformers.load_override_data import OverrideDataTransformer
from lms.djangoapps.course_blocks.transformers.tests.helpers import CourseStructureTestCase
from lms.djangoapps.course_blocks.transformers.tests.test_user_partitions import UserPartitionTestMixin
from lms.djangoapps.courseware.block_render import make_track_function, prepare_runtime_for_user
from openedx.core.djangoapps.content.block_structure.api import get_block_structure_manager
from openedx.core.djangoapps.content.block_structure.transformers import BlockStructureTransformers
from openedx.core.djangoapps.course_groups.cohorts import add_user_to_cohort
from xmodule.modulestore.django import modulestore

FILTERED_USERNAME = 'filtered_user'


class TestOverrideDisplayNameStep(PipelineStep):
"""
Pipeline step that overrides the course display name for a single user.
"""

def run_filter(self, block_structure, user, starting_block_usage_key): # pylint: disable=arguments-differ
if user.username == FILTERED_USERNAME:
block_structure.override_xblock_field(starting_block_usage_key, 'display_name', 'Changed')
return {'block_structure': block_structure}


def get_block_side_effect(block_locator, user_known):
"""
Expand Down Expand Up @@ -147,3 +164,36 @@ def test_get_course_blocks(self, group_id, expected_blocks, user_known):
set(block_structure.get_block_keys()),
self.get_block_key_set(self.blocks, *expected_blocks)
)

@override_settings(
OPEN_EDX_FILTERS_CONFIG={
'org.openedx.learning.course_blocks.transform.completed.v1': {
'pipeline': ['lms.djangoapps.course_blocks.tests.test_api.TestOverrideDisplayNameStep'],
'fail_silently': False,
},
},
)
# Register a transformer that collects display_name so the collected structure carries the authored value.
@patch(
'openedx.core.djangoapps.content.block_structure.transformer_registry.'
'TransformerRegistry.get_registered_transformers',
Mock(return_value={OverrideDataTransformer}),
)
def test_filter_overrides_do_not_leak_into_collected_structure(self):
"""
Tests that changes made by the transform completed filter are returned on cold and warm caches
without modifying the collected block structure or the blocks returned to other users.
"""
self.setup_partitions_and_course()
root_key = self.course.location
authored_name = self.course.display_name
filtered_user = UserFactory.create(username=FILTERED_USERNAME)

for _ in range(2): # the first call collects the structure, the second reads it from the cache
block_structure = get_course_blocks(filtered_user, root_key, BlockStructureTransformers([]))
assert block_structure.get_xblock_field(root_key, 'display_name') == 'Changed'

collected = get_block_structure_manager(self.course.id).get_collected()
assert collected.get_xblock_field(root_key, 'display_name') == authored_name
block_structure = get_course_blocks(self.user, root_key, BlockStructureTransformers([]))
assert block_structure.get_xblock_field(root_key, 'display_name') == authored_name
37 changes: 37 additions & 0 deletions lms/djangoapps/course_home_api/course_metadata/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from django.test.utils import override_settings
from django.urls import reverse
from edx_toggles.toggles.testutils import override_waffle_flag
from openedx_filters import PipelineStep
from openedx_filters.learning.filters import CoursewareAccessChecksRequested

from common.djangoapps.course_modes.models import CourseMode
Expand All @@ -28,6 +29,19 @@
)
from openedx.core.djangoapps.discussions.models import DiscussionsConfiguration

FILTERED_TABS = [{'tab_id': 'filtered', 'title': 'Filtered tab', 'url': 'https://example.com/filtered'}]
filter_users = []


class TestReplaceMetadataStep(PipelineStep):
"""
Pipeline step that records the user it receives and replaces the title and tabs.
"""

def run_filter(self, course_key, user, data): # pylint: disable=arguments-differ
filter_users.append(user)
return {'data': {**data, 'title': f'Filtered {course_key.course}', 'tabs': FILTERED_TABS}}


@ddt.ddt
@override_waffle_flag(COURSEWARE_MICROFRONTEND_PROGRESS_MILESTONES, active=True)
Expand Down Expand Up @@ -87,6 +101,29 @@ def test_get_masqueraded_user(self):
self.update_masquerade(username=self.user.username)
assert self.client.get(self.url).data['username'] == self.user.username

@override_settings(OPEN_EDX_FILTERS_CONFIG={
'org.openedx.learning.course_home_metadata.api.render.started.v1': {
'pipeline': [f'{__name__}.TestReplaceMetadataStep'],
'fail_silently': False,
},
})
def test_metadata_filter_replaces_data_for_masqueraded_user(self):
"""
Test that the response uses the data returned by the pipeline, which receives the masqueraded user.
"""
CourseEnrollment.enroll(self.user, self.course.id, CourseMode.VERIFIED)
self.client.logout()
self.client.login(username=self.staff_user.username, password='bar')
self.update_masquerade(username=self.user.username)
filter_users.clear()

response = self.client.get(self.url)

assert response.status_code == 200
assert response.data['title'] == f'Filtered {self.course.id.course}'
assert response.data['tabs'] == FILTERED_TABS
assert filter_users == [self.user]

def test_get_unknown_course(self):
self.client.logout()
url = reverse('course-home:course-metadata', args=['course-v1:unknown+course+2T2020'])
Expand Down
6 changes: 5 additions & 1 deletion lms/djangoapps/course_home_api/course_metadata/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
from edx_rest_framework_extensions.auth.session.authentication import SessionAuthenticationAllowInactiveUser
from opaque_keys.edx.keys import CourseKey
from openedx_filters.learning.filters import CourseHomeMetadataAPIRenderStarted
from rest_framework.exceptions import PermissionDenied
from rest_framework.generics import RetrieveAPIView
from rest_framework.response import Response
Expand Down Expand Up @@ -161,4 +162,7 @@ def get(self, request, *args, **kwargs):
context['course_overview'] = course
context['enrollment'] = enrollment
serializer = self.get_serializer_class()(data, context=context)
return Response(serializer.data)
data = CourseHomeMetadataAPIRenderStarted.run_filter(
course_key=course_key, user=request.user, data=serializer.data
)
Comment thread
asadali145 marked this conversation as resolved.
return Response(data)
37 changes: 37 additions & 0 deletions lms/djangoapps/course_home_api/dates/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,28 @@
from datetime import datetime

import ddt
from django.test import override_settings
from django.urls import reverse
from openedx_filters import PipelineStep

from common.djangoapps.course_modes.models import CourseMode
from common.djangoapps.student.models import CourseEnrollment
from common.djangoapps.student.tests.factories import UserFactory
from lms.djangoapps.course_home_api.tests.utils import BaseCourseHomeTests
from openedx.features.content_type_gating.models import ContentTypeGatingConfig

filter_users = []


class TestReplaceDatesDataStep(PipelineStep):
"""
Pipeline step that records the user and replaces the response data.
"""

def run_filter(self, course_key, user, data): # pylint: disable=arguments-differ
filter_users.append(user)
return {'data': {'course_key': str(course_key)}}


@ddt.ddt
class DatesTabTestViews(BaseCourseHomeTests):
Expand Down Expand Up @@ -69,3 +84,25 @@ def test_masquerade(self):

self.update_masquerade(role='student')
assert not self.client.get(self.url).data.get('learner_is_full_access')

@override_settings(OPEN_EDX_FILTERS_CONFIG={
'org.openedx.learning.course_home_dates.api.render.started.v1': {
'pipeline': [f'{__name__}.TestReplaceDatesDataStep'],
'fail_silently': False,
},
})
def test_render_filter_receives_masqueraded_user(self):
"""
Test that the render filter can replace the response and receives the masqueraded user.
"""
learner = UserFactory()
CourseEnrollment.enroll(learner, self.course.id)
self.switch_to_staff()
self.update_masquerade(username=learner.username)
filter_users.clear()

response = self.client.get(self.url)

assert response.status_code == 200
assert response.data == {'course_key': str(self.course.id)}
assert filter_users == [learner]
4 changes: 3 additions & 1 deletion lms/djangoapps/course_home_api/dates/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
from edx_rest_framework_extensions.auth.session.authentication import SessionAuthenticationAllowInactiveUser
from opaque_keys.edx.keys import CourseKey
from openedx_filters.learning.filters import CourseHomeDatesAPIRenderStarted
from rest_framework.generics import RetrieveAPIView
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
Expand Down Expand Up @@ -130,5 +131,6 @@ def get(self, request, *args, **kwargs):
context = self.get_serializer_context()
context['learner_is_full_access'] = learner_is_full_access
serializer = self.get_serializer_class()(data, context=context)
data = CourseHomeDatesAPIRenderStarted.run_filter(course_key=course_key, user=request.user, data=serializer.data)

return Response(serializer.data)
return Response(data)
Loading
Loading