From f758e26b8a018a365d33526968a6c9b3173c8095 Mon Sep 17 00:00:00 2001 From: mkurkar Date: Fri, 28 Nov 2025 23:02:29 +0300 Subject: [PATCH 1/2] feat: add management command to create and update ConfigAccessControl and ConfigMirror entries --- .../helpers/management/__init__.py | 1 + .../helpers/management/commands/__init__.py | 1 + .../management/commands/add_config_access.py | 259 ++++++++++++++++++ tests/test_helpers/test_add_config_access.py | 86 ++++++ 4 files changed, 347 insertions(+) create mode 100644 futurex_openedx_extensions/helpers/management/__init__.py create mode 100644 futurex_openedx_extensions/helpers/management/commands/__init__.py create mode 100644 futurex_openedx_extensions/helpers/management/commands/add_config_access.py create mode 100644 tests/test_helpers/test_add_config_access.py diff --git a/futurex_openedx_extensions/helpers/management/__init__.py b/futurex_openedx_extensions/helpers/management/__init__.py new file mode 100644 index 00000000..1e21ed88 --- /dev/null +++ b/futurex_openedx_extensions/helpers/management/__init__.py @@ -0,0 +1 @@ +"""Management commands for futurex_openedx_extensions helpers.""" diff --git a/futurex_openedx_extensions/helpers/management/commands/__init__.py b/futurex_openedx_extensions/helpers/management/commands/__init__.py new file mode 100644 index 00000000..2226fb73 --- /dev/null +++ b/futurex_openedx_extensions/helpers/management/commands/__init__.py @@ -0,0 +1 @@ +"""Management commands.""" diff --git a/futurex_openedx_extensions/helpers/management/commands/add_config_access.py b/futurex_openedx_extensions/helpers/management/commands/add_config_access.py new file mode 100644 index 00000000..0eaee8c1 --- /dev/null +++ b/futurex_openedx_extensions/helpers/management/commands/add_config_access.py @@ -0,0 +1,259 @@ +""" +Django management command to add ConfigAccessControl entries for dashboard theme editor. + +This command automates the creation of ConfigAccessControl entries that allow the +dashboard to read and write theme configuration values. + +Usage: + python manage.py lms add_config_access + +""" +from typing import Any + +from django.core.management.base import BaseCommand +from django.db import transaction + +from futurex_openedx_extensions.helpers.models import ConfigAccessControl, ConfigMirror + + +class Command(BaseCommand): + """Django management command to create ConfigAccessControl entries.""" + + help = 'Add ConfigAccessControl entries for dashboard theme editor access' + + CONFIG_ACCESS_DATA = { + 'custom_pages': { + 'key_type': 'list', + 'path': 'theme_v2.custom_pages', + 'writable': True + }, + 'favicon_url': { + 'key_type': 'string', + 'path': 'favicon_path', + 'writable': True + }, + 'footer': { + 'key_type': 'dict', + 'path': 'theme_v2.footer', + 'writable': True + }, + 'footer_social_media_links': { + 'key_type': 'list', + 'path': 'theme_v2.footer.social_media_links', + 'writable': True + }, + 'fx_css_override_asset_slug': { + 'key_type': 'string', + 'path': 'theme_v2.fx_css_override_asset_slug', + 'writable': True + }, + 'fx_dev_css_enabled': { + 'key_type': 'boolean', + 'path': 'theme_v2.fx_dev_css_enabled', + 'writable': True + }, + 'header': { + 'key_type': 'dict', + 'path': 'theme_v2.header', + 'writable': True + }, + 'header_combined_login': { + 'key_type': 'boolean', + 'path': 'theme_v2.header.combined_login', + 'writable': True + }, + 'header_sections': { + 'key_type': 'list', + 'path': 'theme_v2.header.sections', + 'writable': True + }, + 'logo_image_url': { + 'key_type': 'string', + 'path': 'logo_image_url', + 'writable': True + }, + 'pages_about_us': { + 'key_type': 'dict', + 'path': 'theme_v2.pages.about_us', + 'writable': True + }, + 'pages_contact_us': { + 'key_type': 'dict', + 'path': 'theme_v2.pages.contact_us', + 'writable': True + }, + 'pages_courses': { + 'key_type': 'dict', + 'path': 'theme_v2.pages.courses', + 'writable': True + }, + 'pages_custom_page_1': { + 'key_type': 'dict', + 'path': 'theme_v2.pages.custom_page_1', + 'writable': True + }, + 'pages_custom_page_2': { + 'key_type': 'dict', + 'path': 'theme_v2.pages.custom_page_2', + 'writable': True + }, + 'pages_custom_page_3': { + 'key_type': 'dict', + 'path': 'theme_v2.pages.custom_page_3', + 'writable': True + }, + 'pages_custom_page_4': { + 'key_type': 'dict', + 'path': 'theme_v2.pages.custom_page_4', + 'writable': True + }, + 'pages_custom_page_5': { + 'key_type': 'dict', + 'path': 'theme_v2.pages.custom_page_5', + 'writable': True + }, + 'pages_custom_page_6': { + 'key_type': 'dict', + 'path': 'theme_v2.pages.custom_page_6', + 'writable': True + }, + 'pages_custom_page_7': { + 'key_type': 'dict', + 'path': 'theme_v2.pages.custom_page_7', + 'writable': True + }, + 'pages_custom_page_8': { + 'key_type': 'dict', + 'path': 'theme_v2.pages.custom_page_8', + 'writable': True + }, + 'pages_home': { + 'key_type': 'dict', + 'path': 'theme_v2.pages.home', + 'writable': True + }, + 'pages_terms': { + 'key_type': 'dict', + 'path': 'theme_v2.pages.terms', + 'writable': True + }, + 'platform_settings': { + 'key_type': 'dict', + 'path': 'theme_v2.platform_settings', + 'writable': True + }, + 'platform_settings_language': { + 'key_type': 'dict', + 'path': 'theme_v2.platform_settings.language', + 'writable': True + }, + 'site_domain': { + 'key_type': 'string', + 'path': 'SITE_NAME', + 'writable': False + }, + 'visual_identity': { + 'key_type': 'dict', + 'path': 'theme_v2.visual_identity', + 'writable': True + } + } + + CONFIG_MIRROR_DATA = [ + { + 'source_path': 'theme_v2.platform_settings.site_name.en', + 'destination_path': 'PLATFORM_NAME', + 'priority': 20, + 'enabled': True, + }, + { + 'source_path': 'theme_v2.platform_settings.site_name.ar', + 'destination_path': 'PLATFORM_NAME', + 'priority': 10, + 'enabled': True, + }, + { + 'source_path': 'PLATFORM_NAME', + 'destination_path': 'platform_name', + 'priority': 0, + 'enabled': True, + }, + ] + + def handle(self, *args: Any, **options: Any) -> None: + """Execute the command to create ConfigAccessControl entries.""" + self.stdout.write(self.style.MIGRATE_HEADING('Creating ConfigAccessControl entries...')) + self.stdout.write('') + + created_count = 0 + updated_count = 0 + + with transaction.atomic(): + for key_name, fields in self.CONFIG_ACCESS_DATA.items(): + _, created = ConfigAccessControl.objects.update_or_create( + key_name=key_name, + defaults={ + 'key_type': fields['key_type'], + 'path': fields['path'], + 'writable': fields['writable'], + } + ) + + if created: + self.stdout.write( + self.style.SUCCESS(f'✓ Created: {key_name}') + ) + created_count += 1 + else: + self.stdout.write( + self.style.WARNING(f'↻ Updated: {key_name}') + ) + updated_count += 1 + + self.stdout.write('') + self.stdout.write(self.style.SUCCESS( + f'Successfully processed {len(self.CONFIG_ACCESS_DATA)} entries:' + )) + self.stdout.write(f' - Created: {created_count}') + self.stdout.write(f' - Updated: {updated_count}') + self.stdout.write('') + + self.stdout.write(self.style.MIGRATE_HEADING('Creating ConfigMirror entries...')) + self.stdout.write('') + + created_mirror_count = 0 + updated_mirror_count = 0 + + with transaction.atomic(): + for mirror_data in self.CONFIG_MIRROR_DATA: + _, created = ConfigMirror.objects.update_or_create( + source_path=mirror_data['source_path'], + destination_path=mirror_data['destination_path'], + defaults={ + 'priority': mirror_data['priority'], + 'enabled': mirror_data['enabled'], + } + ) + + if created: + self.stdout.write( + self.style.SUCCESS( + f"✓ Created Mirror: {mirror_data['source_path']} -> {mirror_data['destination_path']}" + ) + ) + created_mirror_count += 1 + else: + self.stdout.write( + self.style.WARNING( + f"↻ Updated Mirror: {mirror_data['source_path']} -> {mirror_data['destination_path']}" + ) + ) + updated_mirror_count += 1 + + self.stdout.write('') + self.stdout.write(self.style.SUCCESS( + f'Successfully processed {len(self.CONFIG_MIRROR_DATA)} mirror entries:' + )) + self.stdout.write(f' - Created: {created_mirror_count}') + self.stdout.write(f' - Updated: {updated_mirror_count}') + self.stdout.write('') diff --git a/tests/test_helpers/test_add_config_access.py b/tests/test_helpers/test_add_config_access.py new file mode 100644 index 00000000..13d1974c --- /dev/null +++ b/tests/test_helpers/test_add_config_access.py @@ -0,0 +1,86 @@ +"""Tests for add_config_access management command.""" +import pytest +from django.core.management import call_command + +from futurex_openedx_extensions.helpers.management.commands.add_config_access import Command +from futurex_openedx_extensions.helpers.models import ConfigAccessControl, ConfigMirror + + +@pytest.mark.django_db +class TestAddConfigAccessCommand: # pylint: disable=no-self-use + """Tests for add_config_access management command.""" + + def test_command_creates_entries(self): + """Test that the command creates ConfigAccessControl entries.""" + ConfigAccessControl.objects.all().delete() + + call_command('add_config_access') + + assert ConfigAccessControl.objects.count() > 0 + + entry = ConfigAccessControl.objects.get(key_name='custom_pages') + assert entry.key_type == 'list' + assert entry.path == 'theme_v2.custom_pages' + assert entry.writable is True + + entry = ConfigAccessControl.objects.get(key_name='site_domain') + assert entry.key_type == 'string' + assert entry.path == 'SITE_NAME' + assert entry.writable is False + + def test_command_updates_existing_entries(self): + """Test that the command updates existing entries.""" + ConfigAccessControl.objects.create( + key_name='custom_pages', + key_type='string', + path='wrong.path', + writable=False + ) + + call_command('add_config_access') + + entry = ConfigAccessControl.objects.get(key_name='custom_pages') + assert entry.key_type == 'list' + assert entry.path == 'theme_v2.custom_pages' + assert entry.writable is True + + def test_command_creates_mirror_entries(self): + """Test that the command creates ConfigMirror entries.""" + ConfigMirror.objects.all().delete() + + call_command('add_config_access') + + assert ConfigMirror.objects.count() == 3 + + entry = ConfigMirror.objects.get(source_path='theme_v2.platform_settings.site_name.en') + assert entry.destination_path == 'PLATFORM_NAME' + assert entry.priority == 20 + assert entry.enabled is True + + entry = ConfigMirror.objects.get(source_path='PLATFORM_NAME') + assert entry.destination_path == 'platform_name' + assert entry.priority == 0 + assert entry.enabled is True + + def test_command_updates_existing_mirror_entries(self): + """ + Test that the command updates existing ConfigMirror entries. + """ + mirror_data = Command.CONFIG_MIRROR_DATA[0] + ConfigMirror.objects.create( + source_path=mirror_data['source_path'], + destination_path=mirror_data['destination_path'], + priority=mirror_data['priority'] + 10, + enabled=not mirror_data['enabled'], + ) + + call_command('add_config_access') + + assert ConfigMirror.objects.count() == 3 + + updated_mirror = ConfigMirror.objects.get( + source_path=mirror_data['source_path'], + destination_path=mirror_data['destination_path'] + ) + assert updated_mirror.priority == mirror_data['priority'] + assert updated_mirror.enabled == mirror_data['enabled'] From c67a09b29b411a8194ffdcc5d764b5c31dddb19d Mon Sep 17 00:00:00 2001 From: shadinaif Date: Sun, 7 Dec 2025 16:15:21 +0300 Subject: [PATCH 2/2] feat: add_config_access - course_categories --- .../management/commands/add_config_access.py | 39 ++++++++++++------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/futurex_openedx_extensions/helpers/management/commands/add_config_access.py b/futurex_openedx_extensions/helpers/management/commands/add_config_access.py index 0eaee8c1..68573148 100644 --- a/futurex_openedx_extensions/helpers/management/commands/add_config_access.py +++ b/futurex_openedx_extensions/helpers/management/commands/add_config_access.py @@ -22,6 +22,11 @@ class Command(BaseCommand): help = 'Add ConfigAccessControl entries for dashboard theme editor access' CONFIG_ACCESS_DATA = { + 'course_categories': { + 'key_type': 'dict', + 'path': 'theme_v2.course_categories', + 'writable': True + }, 'custom_pages': { 'key_type': 'list', 'path': 'theme_v2.custom_pages', @@ -180,6 +185,16 @@ class Command(BaseCommand): }, ] + def log_success(self, processed: int, created: int, updated: int) -> None: + """Log the summary of processed entries.""" + self.stdout.write('') + self.stdout.write(self.style.SUCCESS( + f'Successfully processed {processed} entries:' + )) + self.stdout.write(f' - Created: {created}') + self.stdout.write(f' - Updated: {updated}') + self.stdout.write('') + def handle(self, *args: Any, **options: Any) -> None: """Execute the command to create ConfigAccessControl entries.""" self.stdout.write(self.style.MIGRATE_HEADING('Creating ConfigAccessControl entries...')) @@ -210,13 +225,11 @@ def handle(self, *args: Any, **options: Any) -> None: ) updated_count += 1 - self.stdout.write('') - self.stdout.write(self.style.SUCCESS( - f'Successfully processed {len(self.CONFIG_ACCESS_DATA)} entries:' - )) - self.stdout.write(f' - Created: {created_count}') - self.stdout.write(f' - Updated: {updated_count}') - self.stdout.write('') + self.log_success( + processed=len(self.CONFIG_ACCESS_DATA), + created=created_count, + updated=updated_count + ) self.stdout.write(self.style.MIGRATE_HEADING('Creating ConfigMirror entries...')) self.stdout.write('') @@ -250,10 +263,8 @@ def handle(self, *args: Any, **options: Any) -> None: ) updated_mirror_count += 1 - self.stdout.write('') - self.stdout.write(self.style.SUCCESS( - f'Successfully processed {len(self.CONFIG_MIRROR_DATA)} mirror entries:' - )) - self.stdout.write(f' - Created: {created_mirror_count}') - self.stdout.write(f' - Updated: {updated_mirror_count}') - self.stdout.write('') + self.log_success( + processed=len(self.CONFIG_MIRROR_DATA), + created=created_mirror_count, + updated=updated_mirror_count + )