Skip to content

Commit 34b532e

Browse files
authored
Merge branch 'master' into jci/issue35245
2 parents 60f6746 + 3847cec commit 34b532e

File tree

3 files changed

+143
-12
lines changed

3 files changed

+143
-12
lines changed

cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py

-1
Original file line numberDiff line numberDiff line change
@@ -555,7 +555,6 @@ def _copy_paste_and_assert_link(key_to_copy):
555555
assert new_block.upstream == str(self.lib_block_key)
556556
assert new_block.upstream_version == 3
557557
assert new_block.upstream_display_name == "MCQ-draft"
558-
assert new_block.upstream_max_attempts == 5
559558
return new_block_key
560559

561560
# first verify link for copied block from library

cms/lib/xblock/test/test_upstream_sync.py

+117-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
"""
22
Test CMS's upstream->downstream syncing system
33
"""
4+
import datetime
45
import ddt
6+
from pytz import utc
57

68
from organizations.api import ensure_organization
79
from organizations.models import Organization
@@ -42,13 +44,33 @@ def setUp(self):
4244
title="Test Upstream Library",
4345
)
4446
self.upstream_key = libs.create_library_block(self.library.key, "html", "test-upstream").usage_key
45-
libs.create_library_block(self.library.key, "video", "video-upstream")
4647

4748
upstream = xblock.load_block(self.upstream_key, self.user)
4849
upstream.display_name = "Upstream Title V2"
4950
upstream.data = "<html><body>Upstream content V2</body></html>"
5051
upstream.save()
5152

53+
self.upstream_problem_key = libs.create_library_block(self.library.key, "problem", "problem-upstream").usage_key
54+
libs.set_library_block_olx(self.upstream_problem_key, (
55+
'<problem'
56+
' attempts_before_showanswer_button="1"'
57+
' display_name="Upstream Problem Title V2"'
58+
' due="2024-01-01T00:00:00Z"'
59+
' force_save_button="false"'
60+
' graceperiod="1d"'
61+
' grading_method="last_attempt"'
62+
' matlab_api_key="abc"'
63+
' max_attempts="10"'
64+
' rerandomize="&quot;always&quot;"'
65+
' show_correctness="never"'
66+
' show_reset_button="false"'
67+
' showanswer="on_correct"'
68+
' submission_wait_seconds="10"'
69+
' use_latex_compiler="false"'
70+
' weight="1"'
71+
'/>\n'
72+
))
73+
5274
libs.publish_changes(self.library.key, self.user.id)
5375

5476
self.taxonomy_all_org = tagging_api.create_taxonomy(
@@ -179,6 +201,100 @@ def test_sync_updates_happy_path(self):
179201
for object_tag in object_tags:
180202
assert object_tag.value in new_upstream_tags
181203

204+
# pylint: disable=too-many-statements
205+
def test_sync_updates_to_downstream_only_fields(self):
206+
"""
207+
If we sync to modified content, will it preserve downstream-only fields, and overwrite the rest?
208+
"""
209+
downstream = BlockFactory.create(category='problem', parent=self.unit, upstream=str(self.upstream_problem_key))
210+
211+
# Initial sync
212+
sync_from_upstream(downstream, self.user)
213+
214+
# These fields are copied from upstream
215+
assert downstream.upstream_display_name == "Upstream Problem Title V2"
216+
assert downstream.display_name == "Upstream Problem Title V2"
217+
assert downstream.rerandomize == '"always"'
218+
assert downstream.matlab_api_key == 'abc'
219+
assert not downstream.use_latex_compiler
220+
221+
# These fields are "downstream only", so field defaults are preserved, and values are NOT copied from upstream
222+
assert downstream.attempts_before_showanswer_button == 0
223+
assert downstream.due is None
224+
assert not downstream.force_save_button
225+
assert downstream.graceperiod is None
226+
assert downstream.grading_method == 'last_score'
227+
assert downstream.max_attempts is None
228+
assert downstream.show_correctness == 'always'
229+
assert not downstream.show_reset_button
230+
assert downstream.showanswer == 'finished'
231+
assert downstream.submission_wait_seconds == 0
232+
assert downstream.weight is None
233+
234+
# Upstream updates
235+
libs.set_library_block_olx(self.upstream_problem_key, (
236+
'<problem'
237+
' attempts_before_showanswer_button="10"'
238+
' display_name="Upstream Problem Title V3"'
239+
' due="2024-02-02T00:00:00Z"'
240+
' force_save_button="false"'
241+
' graceperiod=""'
242+
' grading_method="final_attempt"'
243+
' matlab_api_key="def"'
244+
' max_attempts="11"'
245+
' rerandomize="&quot;per_student&quot;"'
246+
' show_correctness="past_due"'
247+
' show_reset_button="false"'
248+
' showanswer="attempted"'
249+
' submission_wait_seconds="11"'
250+
' use_latex_compiler="true"'
251+
' weight="2"'
252+
'/>\n'
253+
))
254+
libs.publish_changes(self.library.key, self.user.id)
255+
256+
# Modifing downstream-only fields are "safe" customizations
257+
downstream.display_name = "Downstream Title Override"
258+
downstream.attempts_before_showanswer_button = 2
259+
downstream.due = datetime.datetime(2025, 2, 2, tzinfo=utc)
260+
downstream.force_save_button = True
261+
downstream.graceperiod = '2d'
262+
downstream.grading_method = 'last_score'
263+
downstream.max_attempts = 100
264+
downstream.show_correctness = 'always'
265+
downstream.show_reset_button = True
266+
downstream.showanswer = 'on_expired'
267+
downstream.submission_wait_seconds = 100
268+
downstream.weight = 3
269+
270+
# Modifying synchronized fields are "unsafe" customizations
271+
downstream.rerandomize = '"onreset"'
272+
downstream.matlab_api_key = 'hij'
273+
downstream.save()
274+
275+
# Follow-up sync.
276+
sync_from_upstream(downstream, self.user)
277+
278+
# "unsafe" customizations are overridden by upstream
279+
assert downstream.upstream_display_name == "Upstream Problem Title V3"
280+
assert downstream.rerandomize == '"per_student"'
281+
assert downstream.matlab_api_key == 'def'
282+
assert downstream.use_latex_compiler
283+
284+
# but "safe" customizations survive
285+
assert downstream.display_name == "Downstream Title Override"
286+
assert downstream.attempts_before_showanswer_button == 2
287+
assert downstream.due == datetime.datetime(2025, 2, 2, tzinfo=utc)
288+
assert downstream.force_save_button
289+
assert downstream.graceperiod == '2d'
290+
assert downstream.grading_method == 'last_score'
291+
assert downstream.max_attempts == 100
292+
assert downstream.show_correctness == 'always'
293+
assert downstream.show_reset_button
294+
assert downstream.showanswer == 'on_expired'
295+
assert downstream.submission_wait_seconds == 100
296+
assert downstream.weight == 3
297+
182298
def test_sync_updates_to_modified_content(self):
183299
"""
184300
If we sync to modified content, will it preserve customizable fields, but overwrite the rest?

cms/lib/xblock/upstream_sync.py

+26-10
Original file line numberDiff line numberDiff line change
@@ -252,10 +252,6 @@ def _update_customizable_fields(*, upstream: XBlock, downstream: XBlock, only_fe
252252
* Set `course_problem.upstream_display_name = lib_problem.display_name` ("fetch").
253253
* If `not only_fetch`, and `course_problem.display_name` wasn't customized, then:
254254
* Set `course_problem.display_name = lib_problem.display_name` ("sync").
255-
256-
* Set `course_problem.upstream_max_attempts = lib_problem.max_attempts` ("fetch").
257-
* If `not only_fetch`, and `course_problem.max_attempts` wasn't customized, then:
258-
* Set `course_problem.max_attempts = lib_problem.max_attempts` ("sync").
259255
"""
260256
syncable_field_names = _get_synchronizable_fields(upstream, downstream)
261257

@@ -264,6 +260,10 @@ def _update_customizable_fields(*, upstream: XBlock, downstream: XBlock, only_fe
264260
if field_name not in syncable_field_names:
265261
continue
266262

263+
# Downstream-only fields don't have an upstream fetch field
264+
if fetch_field_name is None:
265+
continue
266+
267267
# FETCH the upstream's value and save it on the downstream (ie, `downstream.upstream_$FIELD`).
268268
old_upstream_value = getattr(downstream, fetch_field_name)
269269
new_upstream_value = getattr(upstream, field_name)
@@ -361,6 +361,9 @@ def sever_upstream_link(downstream: XBlock) -> None:
361361
downstream.upstream = None
362362
downstream.upstream_version = None
363363
for _, fetched_upstream_field in downstream.get_customizable_fields().items():
364+
# Downstream-only fields don't have an upstream fetch field
365+
if fetched_upstream_field is None:
366+
continue
364367
setattr(downstream, fetched_upstream_field, None) # Null out upstream_display_name, et al.
365368

366369

@@ -414,21 +417,30 @@ class UpstreamSyncMixin(XBlockMixin):
414417
help=("The value of display_name on the linked upstream block."),
415418
default=None, scope=Scope.settings, hidden=True, enforce_type=True,
416419
)
417-
upstream_max_attempts = Integer(
418-
help=("The value of max_attempts on the linked upstream block."),
419-
default=None, scope=Scope.settings, hidden=True, enforce_type=True,
420-
)
421420

422421
@classmethod
423-
def get_customizable_fields(cls) -> dict[str, str]:
422+
def get_customizable_fields(cls) -> dict[str, str | None]:
424423
"""
425424
Mapping from each customizable field to the field which can be used to restore its upstream value.
426425
426+
If the customizable field is mapped to None, then it is considered "downstream only", and cannot be restored
427+
from the upstream value.
428+
427429
XBlocks outside of edx-platform can override this in order to set up their own customizable fields.
428430
"""
429431
return {
430432
"display_name": "upstream_display_name",
431-
"max_attempts": "upstream_max_attempts",
433+
"attempts_before_showanswer_button": None,
434+
"due": None,
435+
"force_save_button": None,
436+
"graceperiod": None,
437+
"grading_method": None,
438+
"max_attempts": None,
439+
"show_correctness": None,
440+
"show_reset_button": None,
441+
"showanswer": None,
442+
"submission_wait_seconds": None,
443+
"weight": None,
432444
}
433445

434446
# PRESERVING DOWNSTREAM CUSTOMIZATIONS and RESTORING UPSTREAM VALUES
@@ -485,6 +497,10 @@ def get_customizable_fields(cls) -> dict[str, str]:
485497
# if field_name in self.downstream_customized:
486498
# continue
487499
#
500+
# # If there is no restore_field name, it's a downstream-only field
501+
# if restore_field_name is None:
502+
# continue
503+
#
488504
# # If this field's value doesn't match the synced upstream value, then mark the field
489505
# # as customized so that we don't clobber it later when syncing.
490506
# # NOTE: Need to consider the performance impact of all these field lookups.

0 commit comments

Comments
 (0)