Skip to content

Commit 4df4322

Browse files
Merge branch 'master' into arslan/fix-validation-api
2 parents 2179109 + 7f0375b commit 4df4322

10 files changed

Lines changed: 173 additions & 47 deletions

File tree

lms/djangoapps/course_api/blocks/api.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""
22
API function for retrieving course blocks data
33
"""
4-
4+
from edx_django_utils.cache import RequestCache
55

66
import lms.djangoapps.course_blocks.api as course_blocks_api
77
from lms.djangoapps.course_blocks.transformers.access_denied_filter import AccessDeniedMessageFilterTransformer
@@ -14,6 +14,7 @@
1414
from .toggles import HIDE_ACCESS_DENIALS_FLAG
1515
from .transformers.blocks_api import BlocksAPITransformer
1616
from .transformers.milestones import MilestonesAndSpecialExamsTransformer
17+
from .utils import COURSE_API_REQUEST_CACHE_NAMESPACE, REUSABLE_BLOCKS_CACHE_KEY
1718

1819

1920
def get_blocks(
@@ -29,6 +30,7 @@ def get_blocks(
2930
block_types_filter=None,
3031
hide_access_denials=False,
3132
allow_start_dates_in_future=False,
33+
cache_with_future_dates=False,
3234
):
3335
"""
3436
Return a serialized representation of the course blocks.
@@ -61,6 +63,7 @@ def get_blocks(
6163
allow_start_dates_in_future (bool): When True, will allow blocks to be
6264
returned that can bypass the StartDateTransformer's filter to show
6365
blocks with start dates in the future.
66+
cache_with_future_dates (bool): When True, will use the block caching logic using RequestCache
6467
"""
6568

6669
if HIDE_ACCESS_DENIALS_FLAG.is_enabled():
@@ -118,6 +121,10 @@ def get_blocks(
118121
),
119122
]
120123

124+
if cache_with_future_dates:
125+
# Include future dates such that get_course_assignments can reuse the block structure from RequestCache
126+
allow_start_dates_in_future = True
127+
121128
# transform
122129
blocks = course_blocks_api.get_course_blocks(
123130
user,
@@ -128,6 +135,19 @@ def get_blocks(
128135
include_has_scheduled_content=include_has_scheduled_content
129136
)
130137

138+
if cache_with_future_dates:
139+
# Store a copy of the transformed, but still unfiltered, course blocks in RequestCache to be reused
140+
# wherever possible for optimization. Copying is required to make sure the cached structure is not mutated
141+
# by the filtering below.
142+
request_cache = RequestCache(COURSE_API_REQUEST_CACHE_NAMESPACE)
143+
request_cache.set(REUSABLE_BLOCKS_CACHE_KEY, blocks.copy())
144+
145+
# Since we included blocks with future start dates in our block structure,
146+
# we need to include the 'start' field to filter out such blocks before returning the response.
147+
# If 'start' field is not requested, it will be removed from the response.
148+
requested_fields = set(requested_fields)
149+
requested_fields.add('start')
150+
131151
# filter blocks by types
132152
if block_types_filter:
133153
block_keys_to_remove = []
@@ -142,7 +162,7 @@ def get_blocks(
142162
serializer_context = {
143163
'request': request,
144164
'block_structure': blocks,
145-
'requested_fields': requested_fields or [],
165+
'requested_fields': requested_fields,
146166
}
147167

148168
if return_type == 'dict':

lms/djangoapps/course_api/blocks/utils.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""
22
Utils for Blocks
33
"""
4+
from edx_django_utils.cache import RequestCache
45
from rest_framework.utils.serializer_helpers import ReturnList
56

67
from openedx.core.djangoapps.discussions.models import (
@@ -9,6 +10,10 @@
910
)
1011

1112

13+
COURSE_API_REQUEST_CACHE_NAMESPACE = "course_api"
14+
REUSABLE_BLOCKS_CACHE_KEY = "reusable_transformed_blocks"
15+
16+
1217
def filter_discussion_xblocks_from_response(response, course_key):
1318
"""
1419
Removes discussion xblocks if discussion provider is openedx.
@@ -63,3 +68,18 @@ def filter_discussion_xblocks_from_response(response, course_key):
6368
response.data['blocks'] = filtered_blocks
6469

6570
return response
71+
72+
73+
def get_cached_transformed_blocks():
74+
"""
75+
Helper function to get an unfiltered course structure from RequestCache,
76+
including blocks with start dates in the future.
77+
78+
Caution: For performance reasons, the function returns the structure object itself, not its copy.
79+
This means the retrieved structure is supposed to be read-only and should not be mutated by consumers.
80+
"""
81+
request_cache = RequestCache(COURSE_API_REQUEST_CACHE_NAMESPACE)
82+
cached_response = request_cache.get_cached_response(REUSABLE_BLOCKS_CACHE_KEY)
83+
reusable_transformed_blocks = cached_response.value if cached_response.is_found else None
84+
85+
return reusable_transformed_blocks

lms/djangoapps/course_api/blocks/views.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
CourseBlocks API views
33
"""
44

5+
from datetime import datetime, timezone
56

67
from django.core.exceptions import ValidationError
78
from django.db import transaction
@@ -237,6 +238,7 @@ def list(self, request, usage_key_string, hide_access_denials=False): # pylint:
237238
params.cleaned_data['return_type'],
238239
params.cleaned_data.get('block_types_filter', None),
239240
hide_access_denials=hide_access_denials,
241+
cache_with_future_dates=True
240242
)
241243
)
242244
# If the username is an empty string, and not None, then we are requesting
@@ -339,9 +341,50 @@ def list(self, request, hide_access_denials=False): # pylint: disable=arguments
339341
if not root:
340342
raise ValidationError(f"Unable to find course block in '{course_key_string}'")
341343

344+
# Earlier we included blocks with future start dates in the collected/cached block structure.
345+
# Now we need to emulate allow_start_dates_in_future=False by removing any such blocks.
346+
include_start = "start" in request.query_params['requested_fields']
347+
self.remove_future_blocks(course_blocks, include_start)
348+
342349
recurse_mark_complete(root, course_blocks)
343350
return response
344351

352+
@staticmethod
353+
def remove_future_blocks(course_blocks, include_start: bool):
354+
"""
355+
Mutates course_blocks in place:
356+
- removes blocks whose 'start' is in the future
357+
- also removes references to them from parents' 'children' lists
358+
- removes 'start' key from all blocks if it wasn't requested
359+
"""
360+
if not course_blocks:
361+
return course_blocks
362+
363+
now = datetime.now(timezone.utc)
364+
365+
# 1. Collect IDs of blocks to remove
366+
to_remove = set()
367+
for block_id, block in course_blocks.items():
368+
get_field = block.get if include_start else block.pop
369+
start = get_field("start")
370+
if start and start > now:
371+
to_remove.add(block_id)
372+
373+
if not to_remove:
374+
return course_blocks
375+
376+
# 2. Remove the blocks themselves
377+
for block_id in to_remove:
378+
course_blocks.pop(block_id, None)
379+
380+
# 3. Clean up children lists
381+
for block in course_blocks.values():
382+
children = block.get("children")
383+
if children:
384+
block["children"] = [cid for cid in children if cid not in to_remove]
385+
386+
return course_blocks
387+
345388

346389
@method_decorator(transaction.non_atomic_requests, name='dispatch')
347390
@view_auth_classes(is_authenticated=False)

lms/djangoapps/courseware/courses.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from common.djangoapps.static_replace import replace_static_urls
2727
from common.djangoapps.util.date_utils import strftime_localized
2828
from lms.djangoapps import branding
29+
from lms.djangoapps.course_api.blocks.utils import get_cached_transformed_blocks
2930
from lms.djangoapps.course_blocks.api import get_course_blocks
3031
from lms.djangoapps.courseware.access import has_access
3132
from lms.djangoapps.courseware.access_response import (
@@ -636,7 +637,10 @@ def get_course_assignments(course_key, user, include_access=False, include_witho
636637

637638
store = modulestore()
638639
course_usage_key = store.make_course_usage_key(course_key)
639-
block_data = get_course_blocks(user, course_usage_key, allow_start_dates_in_future=True, include_completion=True)
640+
641+
block_data = get_cached_transformed_blocks() or get_course_blocks(
642+
user, course_usage_key, allow_start_dates_in_future=True, include_completion=True
643+
)
640644

641645
now = datetime.now(pytz.UTC)
642646
assignments = []

lms/djangoapps/courseware/tests/test_video_mongo.py

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,6 @@ def test_video_constructor(self):
8888

8989
expected_context = {
9090
'autoadvance_enabled': False,
91-
'branding_info': None,
9291
'license': None,
9392
'bumper_metadata': 'null',
9493
'block_id': str(self.block.location),
@@ -177,7 +176,6 @@ def test_video_constructor(self):
177176

178177
expected_context = {
179178
'autoadvance_enabled': False,
180-
'branding_info': None,
181179
'license': None,
182180
'bumper_metadata': 'null',
183181
'block_id': str(self.block.location),
@@ -454,7 +452,6 @@ def test_get_html_track(self):
454452

455453
expected_context = {
456454
'autoadvance_enabled': False,
457-
'branding_info': None,
458455
'license': None,
459456
'bumper_metadata': 'null',
460457
'block_id': str(self.block.location),
@@ -587,7 +584,6 @@ def test_get_html_source(self):
587584

588585
initial_context = {
589586
'autoadvance_enabled': False,
590-
'branding_info': None,
591587
'license': None,
592588
'bumper_metadata': 'null',
593589
'block_id': str(self.block.location),
@@ -726,7 +722,6 @@ def test_get_html_with_mocked_edx_video_id(self):
726722

727723
initial_context = {
728724
'autoadvance_enabled': False,
729-
'branding_info': None,
730725
'license': None,
731726
'bumper_metadata': 'null',
732727
'block_id': str(self.block.location),
@@ -914,7 +909,6 @@ def helper_get_html_with_edx_video_id(self, data):
914909

915910
initial_context = {
916911
'autoadvance_enabled': False,
917-
'branding_info': None,
918912
'license': None,
919913
'bumper_metadata': 'null',
920914
'block_id': str(self.block.location),
@@ -971,21 +965,12 @@ def helper_get_html_with_edx_video_id(self, data):
971965
return context, expected_context
972966

973967
# pylint: disable=invalid-name
974-
@patch('xmodule.video_block.video_block.BrandingInfoConfig')
975968
@patch('xmodule.video_block.video_block.rewrite_video_url')
976-
def test_get_html_cdn_source(self, mocked_get_video, mock_BrandingInfoConfig):
969+
def test_get_html_cdn_source(self, mocked_get_video):
977970
"""
978971
Test if sources got from CDN
979972
"""
980973

981-
mock_BrandingInfoConfig.get_config.return_value = {
982-
"CN": {
983-
'url': 'http://www.xuetangx.com',
984-
'logo_src': 'http://www.xuetangx.com/static/images/logo.png',
985-
'logo_tag': 'Video hosted by XuetangX.com'
986-
}
987-
}
988-
989974
def side_effect(*args, **kwargs): # lint-amnesty, pylint: disable=unused-argument
990975
cdn = {
991976
'http://example.com/example.mp4': 'http://cdn-example.com/example.mp4',
@@ -1031,11 +1016,6 @@ def side_effect(*args, **kwargs): # lint-amnesty, pylint: disable=unused-argume
10311016

10321017
initial_context = {
10331018
'autoadvance_enabled': False,
1034-
'branding_info': {
1035-
'logo_src': 'http://www.xuetangx.com/static/images/logo.png',
1036-
'logo_tag': 'Video hosted by XuetangX.com',
1037-
'url': 'http://www.xuetangx.com'
1038-
},
10391019
'license': None,
10401020
'bumper_metadata': 'null',
10411021
'block_id': str(self.block.location),
@@ -1138,7 +1118,6 @@ def test_get_html_cdn_source_external_video(self):
11381118

11391119
initial_context = {
11401120
'autoadvance_enabled': False,
1141-
'branding_info': None,
11421121
'license': None,
11431122
'bumper_metadata': 'null',
11441123
'cdn_eval': False,
@@ -2380,7 +2359,6 @@ def test_bumper_metadata(self, get_url_for_profiles, get_bumper_settings, is_bum
23802359

23812360
expected_context = {
23822361
'autoadvance_enabled': False,
2383-
'branding_info': None,
23842362
'license': None,
23852363
'bumper_metadata': json.dumps(OrderedDict({
23862364
'saveStateUrl': self.block.ajax_url + '/save_user_state',
@@ -2480,7 +2458,6 @@ def prepare_expected_context(self, autoadvanceenabled_flag, autoadvance_flag):
24802458

24812459
context = {
24822460
'autoadvance_enabled': autoadvanceenabled_flag,
2483-
'branding_info': None,
24842461
'block_id': str(self.block.location),
24852462
'course_id': str(self.block.location.course_key),
24862463
'license': None,

lms/djangoapps/grades/course_data.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22
Code used to get and cache the requested course-data
33
"""
44

5-
65
from lms.djangoapps.course_blocks.api import get_course_blocks
76
from openedx.core.djangoapps.content.block_structure.api import get_block_structure_manager
87
from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order
98

109
from .transformer import GradesTransformer
10+
from ..course_api.blocks.utils import get_cached_transformed_blocks
1111

1212

1313
class CourseData:
@@ -56,7 +56,11 @@ def location(self): # lint-amnesty, pylint: disable=missing-function-docstring
5656
@property
5757
def structure(self): # lint-amnesty, pylint: disable=missing-function-docstring
5858
if self._structure is None:
59-
self._structure = get_course_blocks(
59+
# The get_course_blocks function proved to be a major time sink during a request at "blocks/".
60+
# This caching logic helps improve the response time by getting a copy of the already transformed, but still
61+
# unfiltered, course blocks from RequestCache and thus reducing the number of times that
62+
# the get_course_blocks function is called.
63+
self._structure = get_cached_transformed_blocks() or get_course_blocks(
6064
self.user,
6165
self.location,
6266
collected_block_structure=self._collected_block_structure,

0 commit comments

Comments
 (0)