Skip to content

Commit bf14dbc

Browse files
committed
feat: support config sub-keys
1 parent 3525b2a commit bf14dbc

File tree

4 files changed

+117
-30
lines changed

4 files changed

+117
-30
lines changed

futurex_openedx_extensions/dashboard/views.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1435,6 +1435,7 @@ def put(self, request: Any, tenant_id: int) -> Response:
14351435
data = request.data
14361436
try:
14371437
key = data['key']
1438+
sub_key = (data.get('sub_key') or '').strip()
14381439
if not isinstance(key, str):
14391440
raise FXCodedException(
14401441
code=FXExceptionCodes.INVALID_INPUT, message='Key name must be a string.'
@@ -1458,17 +1459,17 @@ def put(self, request: Any, tenant_id: int) -> Response:
14581459

14591460
update_draft_tenant_config(
14601461
tenant_id=int(tenant_id),
1461-
config_path=key_access_info.path,
1462+
config_path=f'{key_access_info.path}.{sub_key}' if sub_key else key_access_info.path,
14621463
current_revision_id=int(current_revision_id),
14631464
new_value=new_value,
14641465
reset=reset,
14651466
user=request.user,
14661467
)
14671468

1468-
data = get_tenant_config(tenant_id=int(tenant_id), keys=[key], published_only=False)
1469+
result_data = get_tenant_config(tenant_id=int(tenant_id), keys=[key], published_only=False)
14691470
return Response(
14701471
status=http_status.HTTP_200_OK,
1471-
data=serializers.TenantConfigSerializer(data, context={'request': request}).data,
1472+
data=serializers.TenantConfigSerializer(result_data, context={'request': request}).data,
14721473
)
14731474

14741475
except KeyError as exc:

futurex_openedx_extensions/helpers/tenants.py

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,24 @@ def get_tenant_readable_lms_config(tenant_id: int) -> dict:
499499
return result
500500

501501

502+
def _get_tenant_config_non_published(
503+
tenant_id: int, lms_configs: Any, config_access_control: Any, cleaned_keys: set[str], revision_ids: Dict[str, int],
504+
) -> Dict[str, Any]:
505+
"""Helper function to get non-published config values"""
506+
search_keys = list(cleaned_keys & set(config_access_control.keys()))
507+
config_paths = [config_access_control[key]['path'] for key in search_keys]
508+
config_paths_reverse = {
509+
value['path']: key for key, value in config_access_control.items() if key in search_keys
510+
}
511+
for _path in config_paths:
512+
for extra_config_path, revision_id in DraftConfig.objects.filter(
513+
config_path__startswith=f'{_path}.'
514+
).values_list('config_path', 'revision_id'):
515+
extra_config_path = config_paths_reverse[_path] + extra_config_path[len(_path):]
516+
revision_ids[extra_config_path] = revision_id
517+
return DraftConfig.loads_into(tenant_id=tenant_id, config_paths=config_paths, dest=lms_configs)
518+
519+
502520
def get_tenant_config(tenant_id: int, keys: List[str], published_only: bool = True) -> Dict[str, Any]:
503521
"""
504522
Retrieve tenant configuration details for the given tenant ID.
@@ -510,21 +528,24 @@ def get_tenant_config(tenant_id: int, keys: List[str], published_only: bool = Tr
510528
:raises FXCodedException: If the tenant is not found.
511529
"""
512530
lms_configs = get_tenant_readable_lms_config(tenant_id)
531+
513532
config_access_control = get_config_access_control()
514533

515534
cleaned_keys = {key.strip() for key in keys}
516535

517-
draft_configs = {}
536+
revision_ids: Dict[str, int] = {}
518537
if not published_only:
519-
search_keys = list(cleaned_keys & set(config_access_control.keys()))
520-
config_paths = [config_access_control[key]['path'] for key in search_keys]
521-
draft_configs = DraftConfig.loads_into(tenant_id=tenant_id, config_paths=config_paths, dest=lms_configs)
538+
draft_configs = _get_tenant_config_non_published(
539+
tenant_id, lms_configs, config_access_control, cleaned_keys, revision_ids,
540+
)
541+
else:
542+
draft_configs = {}
522543

523544
details: Dict[str, Any] = {
524545
'values': {},
525546
'not_permitted': [],
526547
'bad_keys': [],
527-
'revision_ids': {},
548+
'revision_ids': revision_ids,
528549
}
529550

530551
for key in list(cleaned_keys):
@@ -533,7 +554,7 @@ def get_tenant_config(tenant_id: int, keys: List[str], published_only: bool = Tr
533554
_, config_value = dot_separated_path_get_value(lms_configs, config['path'])
534555
details['values'][key] = config_value
535556
if not published_only:
536-
details['revision_ids'][key] = draft_configs[config['path']]['revision_id']
557+
revision_ids[key] = draft_configs[config['path']]['revision_id']
537558
else:
538559
details['bad_keys'].append(key)
539560

tests/test_dashboard/test_views.py

Lines changed: 43 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2501,24 +2501,42 @@ def _prepare_data(tenant_id, config_path):
25012501
assert tenant_config.lms_configs['platform_name'] == 's1 platform name'
25022502
assert DraftConfig.objects.filter(tenant_id=tenant_id).count() == 1, 'bad test data'
25032503

2504-
ConfigAccessControl.objects.create(key_name='platform_name', path=config_path, writable=True)
2504+
ConfigAccessControl.objects.create(key_name='platform_name_key', path=config_path, writable=True)
25052505
assert DraftConfig.objects.filter(tenant_id=tenant_id, config_path=config_path).count() == 0, 'bad test data'
25062506

25072507
@patch('futurex_openedx_extensions.dashboard.views.ThemeConfigDraftView.validate_input')
25082508
@patch('futurex_openedx_extensions.dashboard.views.update_draft_tenant_config')
2509-
def test_draft_config_update(self, mock_update_draft, mocked_validate_input):
2509+
@ddt.data(
2510+
(None, 'platform_name_key', 'platform_name'),
2511+
('sub-key', 'platform_name_key.sub-key', 'platform_name.sub-key'),
2512+
('sub-key.another_sub', 'platform_name_key.sub-key.another_sub', 'platform_name.sub-key.another_sub'),
2513+
)
2514+
@ddt.unpack
2515+
def test_draft_config_update(
2516+
self, sub_key, final_key, result_config_path, mock_update_draft, mock_validate_input,
2517+
): # pylint: disable=too-many-arguments
25102518
"""Verify that the view returns the correct response"""
25112519
def _update_draft(**kwargs):
25122520
"""mock update_draft_tenant_config effect"""
25132521
draft_config = DraftConfig.objects.create(
25142522
tenant_id=1,
25152523
config_path=config_path,
2516-
config_value=new_value,
2524+
config_value='get_tenant_config should fetch the value of the main key',
25172525
created_by_id=1,
25182526
updated_by_id=1,
25192527
)
2520-
draft_config.revision_id = 987
2528+
draft_config.revision_id = 123
25212529
draft_config.save()
2530+
if sub_key:
2531+
draft_config = DraftConfig.objects.create(
2532+
tenant_id=1,
2533+
config_path=result_config_path,
2534+
config_value='should not be fetched by get_tenant_config',
2535+
created_by_id=1,
2536+
updated_by_id=1,
2537+
)
2538+
draft_config.revision_id = 987
2539+
draft_config.save()
25222540

25232541
tenant_id = 1
25242542
config_path = 'platform_name'
@@ -2527,38 +2545,42 @@ def _update_draft(**kwargs):
25272545

25282546
self._prepare_data(tenant_id, config_path)
25292547

2530-
new_value = 's1 new name'
25312548
mock_update_draft.side_effect = _update_draft
25322549

2550+
sub_key_new_value = 'should not be fetched by get_tenant_config, because it fetches the value of the main key'
25332551
response = self.client.put(
25342552
self.url,
25352553
data={
2536-
'key': 'platform_name',
2537-
'new_value': new_value,
2554+
'key': 'platform_name_key',
2555+
'sub_key': sub_key,
2556+
'new_value': sub_key_new_value,
25382557
'current_revision_id': '456',
25392558
},
25402559
format='json'
25412560
)
25422561
self.assertEqual(response.status_code, http_status.HTTP_200_OK, response.data)
2543-
self.assertEqual(response.data, {
2544-
'bad_keys': [],
2545-
'not_permitted': [],
2546-
'revision_ids': {
2547-
'platform_name': '987',
2548-
},
2549-
'values': {
2550-
'platform_name': new_value,
2551-
},
2552-
})
25532562
mock_update_draft.assert_called_once_with(
25542563
tenant_id=tenant_id,
2555-
config_path='platform_name',
2564+
config_path=result_config_path,
25562565
current_revision_id=456,
2557-
new_value=new_value,
2566+
new_value=sub_key_new_value,
25582567
reset=False,
25592568
user=ANY,
25602569
)
2561-
mocked_validate_input.assert_called_once_with('456')
2570+
expected_response_data = {
2571+
'bad_keys': [],
2572+
'not_permitted': [],
2573+
'revision_ids': {
2574+
'platform_name_key': '123',
2575+
},
2576+
'values': {
2577+
'platform_name_key': 'get_tenant_config should fetch the value of the main key',
2578+
},
2579+
}
2580+
if sub_key:
2581+
expected_response_data['revision_ids'][final_key] = '987'
2582+
self.assertEqual(response.data, expected_response_data)
2583+
mock_validate_input.assert_called_once_with('456')
25622584

25632585
@patch('futurex_openedx_extensions.dashboard.views.ThemeConfigDraftView.validate_input')
25642586
@patch('futurex_openedx_extensions.dashboard.views.update_draft_tenant_config')
@@ -2582,7 +2604,7 @@ def test_draft_config_update_reset(self, reset_value, expected_passed_value, moc
25822604
self.client.put(
25832605
self.url,
25842606
data={
2585-
'key': 'platform_name',
2607+
'key': 'platform_name_key',
25862608
'new_value': 'anything',
25872609
'current_revision_id': '0',
25882610
'reset': reset_value,

tests/test_helpers/test_tenants.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -602,6 +602,49 @@ def test_get_tenant_config(
602602
f'FAILED: {usecase} - Expected {expected_bad_keys}, got {result["bad_keys"]}'
603603

604604

605+
@pytest.mark.django_db
606+
@pytest.mark.parametrize('published_only, expected_revision_ids', [
607+
(False, {
608+
'pages_key': 123, 'pages_key.draft_page2': 567, 'courses_key': 345, 'courses_key.course1.chapters': 789,
609+
}),
610+
(True, {}),
611+
])
612+
def test_get_tenant_config_draft_sub_keys(
613+
base_data, published_only, expected_revision_ids,
614+
): # pylint: disable=unused-argument
615+
"""Verify get_tenant_config returns the revision IDs for all draft sub-keys."""
616+
def _new_draft_config(config_path, config_value, revision_id):
617+
"""Helper to create a new draft config."""
618+
draft_config = DraftConfig.objects.create(
619+
tenant_id=1, config_path=config_path, config_value=config_value,
620+
created_by_id=1, updated_by_id=1,
621+
)
622+
draft_config.revision_id = revision_id
623+
draft_config.save()
624+
625+
draft_config_template = {
626+
'pages': {'draft_page1': {'value': 'test1'}, 'draft_page2': {'value': 'test2'}},
627+
'courses': {'course1': {'chapters': {'c1': 'chapter1'}}, 'course2': {'chapters': {'c2': 'chapter2'}}},
628+
'something': {'else': {'val': 'val'}},
629+
}
630+
assert DraftConfig.objects.count() == 0, 'bad test data, DraftConfig should be empty before the test'
631+
632+
_new_draft_config('pages', draft_config_template['pages'], 123)
633+
_new_draft_config('courses', draft_config_template['courses'], 345)
634+
_new_draft_config('something', draft_config_template['something'], 555)
635+
_new_draft_config('pages.draft_page2', draft_config_template['pages']['draft_page2'], 567)
636+
_new_draft_config('courses.course1.chapters', draft_config_template['courses']['course1']['chapters'], 789)
637+
_new_draft_config('something.else', draft_config_template['something']['else'], 888)
638+
639+
ConfigAccessControl.objects.create(key_name='pages_key', path='pages', key_type='dict')
640+
ConfigAccessControl.objects.create(key_name='courses_key', path='courses', key_type='dict')
641+
ConfigAccessControl.objects.create(key_name='something_key', path='something', key_type='dict')
642+
643+
assert tenants.get_tenant_config(
644+
1, ['pages_key', 'courses_key'], published_only,
645+
)['revision_ids'] == expected_revision_ids
646+
647+
605648
@pytest.mark.django_db
606649
def test_get_tenant_config_for_non_exist_tenant():
607650
"""Test the get_tenant_config for non exist tenant_id."""

0 commit comments

Comments
 (0)