@@ -1163,3 +1163,207 @@ def test_key_properties_when_usage_is_not_an_opaque_key(self):
11631163 block = XBlock (Mock (spec = Runtime ), scope_ids = scope_ids )
11641164 self .assertEqual (block .usage_key , "myWeirdOldUsageId" )
11651165 self .assertIsNone (block .context_key )
1166+
1167+
1168+ class TestGetExplicitlySetFieldsByScope (unittest .TestCase ):
1169+ """
1170+ Tests for ``Blocklike.get_explicitly_set_fields_by_scope``.
1171+ """
1172+
1173+ class FieldBlock (XBlock ):
1174+ """XBlock with fields across multiple scopes for testing."""
1175+ content_field = String (scope = Scope .content , default = "default_content" )
1176+ settings_field = String (scope = Scope .settings , default = "default_settings" )
1177+ mutable_content = List (scope = Scope .content )
1178+ mutable_settings = List (scope = Scope .settings )
1179+
1180+ def _make_block (self , field_data_dict = None ):
1181+ field_data = DictFieldData (field_data_dict or {})
1182+ runtime = TestRuntime (services = {'field-data' : field_data })
1183+ return self .FieldBlock (runtime , scope_ids = Mock (spec = ScopeIds ))
1184+
1185+ def test_no_explicitly_set_fields (self ):
1186+ """Fields not explicitly set should not appear in the result."""
1187+ block = self ._make_block ()
1188+ assert not block .get_explicitly_set_fields_by_scope (Scope .content )
1189+ assert not block .get_explicitly_set_fields_by_scope (Scope .settings )
1190+
1191+ def test_explicitly_set_via_field_data (self ):
1192+ """Fields present in the field data store are considered explicitly set."""
1193+ block = self ._make_block ({
1194+ 'content_field' : 'custom_content' ,
1195+ 'settings_field' : 'custom_settings' ,
1196+ })
1197+ content = block .get_explicitly_set_fields_by_scope (Scope .content )
1198+ settings = block .get_explicitly_set_fields_by_scope (Scope .settings )
1199+
1200+ assert content == {'content_field' : 'custom_content' }
1201+ assert settings == {'settings_field' : 'custom_settings' }
1202+
1203+ def test_explicitly_set_via_assignment (self ):
1204+ """Fields set by attribute assignment should appear after save."""
1205+ block = self ._make_block ()
1206+ block .content_field = 'new_content'
1207+ block .settings_field = 'new_settings'
1208+ block .save ()
1209+
1210+ content = block .get_explicitly_set_fields_by_scope (Scope .content )
1211+ settings = block .get_explicitly_set_fields_by_scope (Scope .settings )
1212+
1213+ assert content == {'content_field' : 'new_content' }
1214+ assert settings == {'settings_field' : 'new_settings' }
1215+
1216+ def test_scope_filtering (self ):
1217+ """Only fields of the requested scope should be returned."""
1218+ block = self ._make_block ({
1219+ 'content_field' : 'some_content' ,
1220+ 'settings_field' : 'some_settings' ,
1221+ })
1222+ content = block .get_explicitly_set_fields_by_scope (Scope .content )
1223+ assert 'content_field' in content
1224+ assert 'settings_field' not in content
1225+
1226+ def test_mutable_fields (self ):
1227+ """Mutable field types (List, Dict) should work correctly."""
1228+ block = self ._make_block ({
1229+ 'mutable_content' : [1 , 2 , 3 ],
1230+ 'mutable_settings' : ['a' , 'b' ],
1231+ })
1232+ content = block .get_explicitly_set_fields_by_scope (Scope .content )
1233+ settings = block .get_explicitly_set_fields_by_scope (Scope .settings )
1234+
1235+ assert content == {'mutable_content' : [1 , 2 , 3 ]}
1236+ assert settings == {'mutable_settings' : ['a' , 'b' ]}
1237+
1238+ def test_field_set_to_none (self ):
1239+ """Fields explicitly set to None should still appear in the result."""
1240+ block = self ._make_block ({'content_field' : None })
1241+ content = block .get_explicitly_set_fields_by_scope (Scope .content )
1242+ assert 'content_field' in content
1243+ assert content ['content_field' ] is None
1244+
1245+ def test_default_scope_is_content (self ):
1246+ """The default scope parameter should be Scope.content."""
1247+ block = self ._make_block ({
1248+ 'content_field' : 'value' ,
1249+ 'settings_field' : 'value' ,
1250+ })
1251+ result = block .get_explicitly_set_fields_by_scope ()
1252+ assert 'content_field' in result
1253+ assert 'settings_field' not in result
1254+
1255+ def test_deleted_field_not_returned (self ):
1256+ """A field that was set and then deleted should no longer appear."""
1257+ block = self ._make_block ({'content_field' : 'will_delete' })
1258+ assert 'content_field' in block .get_explicitly_set_fields_by_scope (Scope .content )
1259+
1260+ del block .content_field
1261+ assert 'content_field' not in block .get_explicitly_set_fields_by_scope (Scope .content )
1262+
1263+
1264+ class TestGetIconClass (unittest .TestCase ):
1265+ """
1266+ Tests for ``XBlock.get_icon_class``.
1267+ """
1268+
1269+ def test_default_icon_class (self ):
1270+ """Block without icon_class attribute should return 'other'."""
1271+ class PlainBlock (XBlock ):
1272+ pass
1273+
1274+ runtime = TestRuntime (services = {'field-data' : DictFieldData ({})})
1275+ block = PlainBlock (runtime , scope_ids = Mock (spec = ScopeIds ))
1276+ assert block .get_icon_class () == 'other'
1277+
1278+ def test_custom_icon_class (self ):
1279+ """Block with icon_class attribute should return that value."""
1280+ class VideoLikeBlock (XBlock ):
1281+ icon_class = 'video'
1282+
1283+ runtime = TestRuntime (services = {'field-data' : DictFieldData ({})})
1284+ block = VideoLikeBlock (runtime , scope_ids = Mock (spec = ScopeIds ))
1285+ assert block .get_icon_class () == 'video'
1286+
1287+ def test_problem_icon_class (self ):
1288+ """Verify the 'problem' icon class."""
1289+ class ProblemLikeBlock (XBlock ):
1290+ icon_class = 'problem'
1291+
1292+ runtime = TestRuntime (services = {'field-data' : DictFieldData ({})})
1293+ block = ProblemLikeBlock (runtime , scope_ids = Mock (spec = ScopeIds ))
1294+ assert block .get_icon_class () == 'problem'
1295+
1296+
1297+ @ddt .ddt
1298+ class TestDisplayNameWithDefault (unittest .TestCase ):
1299+ """
1300+ Tests for ``XBlock.display_name_with_default``.
1301+ """
1302+
1303+ class BlockWithDisplayName (XBlock ):
1304+ display_name = String (default = "Default Name" , scope = Scope .settings )
1305+
1306+ def test_explicit_display_name (self ):
1307+ """When display_name is explicitly set, it should be returned."""
1308+ runtime = TestRuntime (services = {'field-data' : DictFieldData ({
1309+ 'display_name' : 'My Custom Name' ,
1310+ })})
1311+ block = self .BlockWithDisplayName (runtime , scope_ids = Mock (spec = ScopeIds ))
1312+ assert block .display_name_with_default == 'My Custom Name'
1313+
1314+ def test_field_default_used_when_not_set (self ):
1315+ """When display_name is not explicitly set, the field default is used."""
1316+ runtime = TestRuntime (services = {'field-data' : DictFieldData ({})})
1317+ block = self .BlockWithDisplayName (runtime , scope_ids = Mock (spec = ScopeIds ))
1318+ # Field has default="Default Name", so that's returned
1319+ assert block .display_name_with_default == "Default Name"
1320+
1321+ def test_empty_string_falls_back_to_usage_key (self ):
1322+ """When display_name is empty string (falsy), fall back to usage_key.block_id."""
1323+ usage_key = Mock ()
1324+ usage_key .block_id = "my_block_id"
1325+ scope_ids = Mock (spec = ScopeIds )
1326+ scope_ids .usage_id = usage_key
1327+
1328+ runtime = TestRuntime (services = {'field-data' : DictFieldData ({'display_name' : '' })})
1329+ block = self .BlockWithDisplayName (runtime , scope_ids = scope_ids )
1330+ assert block .display_name_with_default == "my block id"
1331+
1332+ def test_none_display_name_falls_back_to_usage_key (self ):
1333+ """When display_name is explicitly None (falsy), fall back to usage_key.block_id."""
1334+ usage_key = Mock ()
1335+ usage_key .block_id = "my_block_id"
1336+ scope_ids = Mock (spec = ScopeIds )
1337+ scope_ids .usage_id = usage_key
1338+
1339+ runtime = TestRuntime (services = {'field-data' : DictFieldData ({'display_name' : None })})
1340+ block = self .BlockWithDisplayName (runtime , scope_ids = scope_ids )
1341+ assert block .display_name_with_default == "my block id"
1342+
1343+ def test_no_display_name_field_falls_back_to_usage_key (self ):
1344+ """Block without display_name field at all should fall back to usage_key.block_id."""
1345+ class NoDisplayNameBlock (XBlock ):
1346+ pass
1347+
1348+ usage_key = Mock ()
1349+ usage_key .block_id = "some_block"
1350+ scope_ids = Mock (spec = ScopeIds )
1351+ scope_ids .usage_id = usage_key
1352+
1353+ runtime = TestRuntime (services = {'field-data' : DictFieldData ({})})
1354+ block = NoDisplayNameBlock (runtime , scope_ids = scope_ids )
1355+ assert block .display_name_with_default == "some block"
1356+
1357+ def test_underscores_replaced_with_spaces (self ):
1358+ """The fallback name should replace underscores with spaces."""
1359+ class NoDisplayNameBlock (XBlock ):
1360+ pass
1361+
1362+ usage_key = Mock ()
1363+ usage_key .block_id = "intro_to_python_101"
1364+ scope_ids = Mock (spec = ScopeIds )
1365+ scope_ids .usage_id = usage_key
1366+
1367+ runtime = TestRuntime (services = {'field-data' : DictFieldData ({})})
1368+ block = NoDisplayNameBlock (runtime , scope_ids = scope_ids )
1369+ assert block .display_name_with_default == "intro to python 101"
0 commit comments