Skip to content

Commit 51ac947

Browse files
fix: always generate block structure from "published-only" branch (#37335)
Block structures are meant to be an optimization for the LMS, meaning that they should always be collecting from the published branch of modulestore. This is what happens by default when it's run from the LMS celery process, but this code is sometimes invoked from a Studio worker (e.g. development mode celery, running in immediate in-proc mode). --------- Co-authored-by: Peter Pinch <[email protected]>
1 parent a277056 commit 51ac947

5 files changed

Lines changed: 65 additions & 13 deletions

File tree

lms/djangoapps/courseware/tests/test_courses.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ def test_get_course_func_with_access_error(self, course_access_func_name):
8888
assert not error.value.access_response.has_access
8989

9090
@ddt.data(
91-
(GET_COURSE_WITH_ACCESS, 2),
91+
(GET_COURSE_WITH_ACCESS, 1),
9292
(GET_COURSE_OVERVIEW_WITH_ACCESS, 0),
9393
)
9494
@ddt.unpack

lms/djangoapps/grades/tests/test_tasks.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -156,8 +156,8 @@ def test_block_structure_created_only_once(self):
156156
assert mock_block_structure_create.call_count == 1
157157

158158
@ddt.data(
159-
(ModuleStoreEnum.Type.split, 2, 42, True),
160-
(ModuleStoreEnum.Type.split, 2, 42, False),
159+
(ModuleStoreEnum.Type.split, 1, 42, True),
160+
(ModuleStoreEnum.Type.split, 1, 42, False),
161161
)
162162
@ddt.unpack
163163
def test_query_counts(self, default_store, num_mongo_calls, num_sql_calls, create_multiple_subsections):
@@ -168,7 +168,7 @@ def test_query_counts(self, default_store, num_mongo_calls, num_sql_calls, creat
168168
self._apply_recalculate_subsection_grade()
169169

170170
@ddt.data(
171-
(ModuleStoreEnum.Type.split, 2, 42),
171+
(ModuleStoreEnum.Type.split, 1, 42),
172172
)
173173
@ddt.unpack
174174
def test_query_counts_dont_change_with_more_content(self, default_store, num_mongo_calls, num_sql_calls):
@@ -200,16 +200,17 @@ def test_other_inaccessible_subsection(self, mock_subsection_signal):
200200
with self.store.branch_setting(ModuleStoreEnum.Branch.draft_preferred, self.course.id):
201201
self.store.update_item(sequential, self.user.id)
202202

203-
# Make sure the signal is sent for only the 2 accessible sequentials.
203+
# Make sure the signal is sent for only the 1 accessible sequentials.
204+
# Update: draft branch content shouldn't be accessible
204205
self._apply_recalculate_subsection_grade()
205-
assert mock_subsection_signal.call_count == 2
206+
assert mock_subsection_signal.call_count == 1
206207
sequentials_signalled = {
207208
args[1]['subsection_grade'].location
208209
for args in mock_subsection_signal.call_args_list
209210
}
210211
self.assertSetEqual(
211212
sequentials_signalled,
212-
{self.sequential.location, accessible_seq.location},
213+
{self.sequential.location},
213214
)
214215

215216
@patch('lms.djangoapps.grades.signals.signals.SUBSECTION_SCORE_CHANGED.send')
@@ -255,7 +256,7 @@ def test_problem_block_with_restricted_access(self, mock_subsection_signal):
255256
UserPartition.scheme_extensions = None
256257

257258
@ddt.data(
258-
(ModuleStoreEnum.Type.split, 2, 42),
259+
(ModuleStoreEnum.Type.split, 1, 42),
259260
)
260261
@ddt.unpack
261262
def test_persistent_grades_on_course(self, default_store, num_mongo_queries, num_sql_queries):

openedx/core/djangoapps/content/block_structure/manager.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
from contextlib import contextmanager
88

9+
from xmodule.modulestore import ModuleStoreEnum
10+
911
from .exceptions import BlockStructureNotFound, TransformerDataIncompatible, UsageKeyNotInBlockStructure
1012
from .factory import BlockStructureFactory
1113
from .store import BlockStructureStore
@@ -104,7 +106,6 @@ def get_collected(self, user=None):
104106
self.store,
105107
)
106108
BlockStructureTransformers.verify_versions(block_structure)
107-
108109
except (BlockStructureNotFound, TransformerDataIncompatible):
109110
if user and getattr(user, "known", True):
110111
# This bypasses the runtime access checks. When we are populating the course blocks cache,
@@ -133,10 +134,16 @@ def _update_collected(self):
133134
the modulestore.
134135
"""
135136
with self._bulk_operations():
136-
block_structure = BlockStructureFactory.create_from_modulestore(
137-
self.root_block_usage_key,
138-
self.modulestore,
139-
)
137+
# Always uses published-only branch regardless of CMS or LMS context.
138+
with self.modulestore.branch_setting(
139+
ModuleStoreEnum.Branch.published_only,
140+
self.root_block_usage_key.course_key
141+
):
142+
block_structure = BlockStructureFactory.create_from_modulestore(
143+
self.root_block_usage_key,
144+
self.modulestore,
145+
)
146+
140147
BlockStructureTransformers.collect(block_structure)
141148
self.store.add(block_structure)
142149
return block_structure

openedx/core/djangoapps/content/block_structure/tests/helpers.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,13 @@ def bulk_operations(self, ignore): # pylint: disable=unused-argument
110110
"""
111111
yield
112112

113+
@contextmanager
114+
def branch_setting(self, branch_settings, course_id=None): # pylint: disable=unused-argument
115+
"""
116+
A context manager for temporarily setting a store's branch value on the current thread.
117+
"""
118+
yield
119+
113120

114121
class MockCache:
115122
"""

openedx/core/djangoapps/content/block_structure/tests/test_manager.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@
44

55
import pytest
66
import ddt
7+
from unittest.mock import MagicMock
78
from django.test import TestCase
89

10+
from xmodule.modulestore import ModuleStoreEnum
11+
912
from ..block_structure import BlockStructureBlockData
1013
from ..exceptions import UsageKeyNotInBlockStructure
1114
from ..manager import BlockStructureManager
@@ -216,3 +219,37 @@ def test_clear(self):
216219
self.bs_manager.clear()
217220
self.collect_and_verify(expect_modulestore_called=True, expect_cache_updated=True)
218221
assert TestTransformer1.collect_call_count == 2
222+
223+
def test_update_collected_branch_context_integration(self):
224+
"""
225+
Integration test to verify the published-only branch context works end-to-end.
226+
"""
227+
# Track branch setting calls on our mock modulestore
228+
attr_name = 'branch_setting'
229+
original_branch_setting = getattr(self.modulestore, attr_name, None)
230+
branch_setting_calls = []
231+
232+
def mock_branch_setting(branch, course_key):
233+
branch_setting_calls.append((branch, course_key))
234+
# Return a proper context manager that does nothing
235+
return MagicMock(__enter__=MagicMock(), __exit__=MagicMock())
236+
237+
# Add the branch_setting method to our mock modulestore
238+
setattr(self.modulestore, attr_name, mock_branch_setting)
239+
240+
try:
241+
with mock_registered_transformers(self.registered_transformers):
242+
self.bs_manager.get_collected()
243+
244+
# Verify branch_setting was called with the correct parameters
245+
self.assertEqual(len(branch_setting_calls), 1)
246+
branch, course_key = branch_setting_calls[0]
247+
self.assertEqual(branch, ModuleStoreEnum.Branch.published_only)
248+
self.assertEqual(course_key, self.block_key_factory(0).course_key)
249+
250+
finally:
251+
# Restore original method if it existed
252+
if original_branch_setting is not None:
253+
setattr(self.modulestore, attr_name, original_branch_setting)
254+
elif hasattr(self.modulestore, attr_name):
255+
delattr(self.modulestore, attr_name)

0 commit comments

Comments
 (0)