-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add create_or_update_tenant_config management command (#1)
* feat: add create_or_update_tenant_config management command * refactor: rename cmd edit_tenant_values to edit_microsite_values * docs: update readme to add example of create_or_update_tenant_config command * refactor: update configs instead of overwriting * refactor: add override argument and update merge logic * refactor: use inbuilt open method
- Loading branch information
1 parent
e04fbb1
commit 0f95406
Showing
5 changed files
with
343 additions
and
19 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
145 changes: 145 additions & 0 deletions
145
eox_tenant/management/commands/create_or_update_tenant_config.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
""" | ||
Create or updates the TenantConfig for given routes. | ||
""" | ||
|
||
import json | ||
import logging | ||
|
||
from django.core.management import BaseCommand | ||
from jsonfield.fields import JSONField | ||
|
||
from eox_tenant.models import Route, TenantConfig | ||
|
||
LOG = logging.getLogger(__name__) | ||
|
||
|
||
def load_json_from_file(filename): | ||
""" | ||
Loads json content from file. | ||
""" | ||
with open(filename, encoding='utf-8') as file: | ||
return json.load(file) | ||
|
||
|
||
class Command(BaseCommand): | ||
""" | ||
Management command to create or update TenantConfig. | ||
Examples: | ||
# create/update tenant config and link 2 routes | ||
- python manage.py create_or_update_tenant_config --external-key lacolhost.com \ | ||
--config '{"lms_configs": {"PLATFORM_NAME": "Lacolhost", "CONTACT_EMAIL": "[email protected]"}}' \ | ||
lacolhost.com preview.lacolhost.com | ||
# Override existing lms_configs under a tenant, for example, below command will overwrite `lms_configs` | ||
# with given dictionary instead of updating it. | ||
- python manage.py create_or_update_tenant_config --external-key lacolhost.com \ | ||
--config '{"lms_configs": {"PLATFORM_NAME": "New name"}}' lacolhost.com preview.lacolhost.com | ||
# create/update tenant config using json file | ||
- python manage.py create_or_update_tenant_config --external-key lacolhost.com \ | ||
--config-file /tmp/lms.json lacolhost.com preview.lacolhost.com | ||
# Link studio.lacolhost.com route to existing/empty tenant config with given external key | ||
- python manage.py create_or_update_tenant_config --external-key lacolhost.com studio.lacolhost.com | ||
""" | ||
help = 'Create or update TenantConfig' | ||
|
||
def add_arguments(self, parser): | ||
parser.add_argument( | ||
'--external-key', | ||
dest='external_key', | ||
required=True, | ||
type=str, | ||
help='External key of the tenant config' | ||
) | ||
parser.add_argument('routes', nargs='+', help='Routes to link to this tenant config') | ||
|
||
parser.add_argument( | ||
'--config', | ||
type=json.loads, | ||
help="Enter JSON tenant configurations", | ||
required=False | ||
) | ||
parser.add_argument( | ||
'-f', | ||
'--config-file', | ||
type=load_json_from_file, | ||
dest='config_file_data', | ||
help="Enter the path to the JSON file containing the tenant configuration", | ||
required=False | ||
) | ||
parser.add_argument( | ||
'--override', | ||
dest='override', | ||
action='store_true', | ||
required=False | ||
) | ||
|
||
def merge_dict(self, base_dict, override): | ||
""" | ||
Merge two nested dicts. | ||
""" | ||
if isinstance(base_dict, dict) and isinstance(override, dict): | ||
for key, value in override.items(): | ||
base_dict[key] = self.merge_dict(base_dict.get(key, {}), value) | ||
return base_dict | ||
|
||
return override | ||
|
||
def handle(self, *args, **options): | ||
""" | ||
Create or update TenantConfig and link related routes. | ||
""" | ||
external_key = options['external_key'] | ||
routes = options['routes'] | ||
configuration = options.get('config') | ||
config_file_data = options.get('config_file_data') | ||
tenant_configuration_values = configuration or config_file_data | ||
override = options.get('override') | ||
# pylint: disable=no-member,protected-access | ||
external_key_length = TenantConfig._meta.get_field("external_key").max_length | ||
if external_key: | ||
if len(str(external_key)) > external_key_length: | ||
LOG.warning( | ||
"The external_key %s is too long, truncating to %s" | ||
" characters. Please update external_key in admin.", | ||
external_key, | ||
external_key_length | ||
) | ||
# trim name as the column has a limit of 63 characters | ||
external_key = external_key[:external_key_length] | ||
tenant, created = TenantConfig.objects.get_or_create( | ||
external_key=external_key, | ||
) | ||
if created: | ||
LOG.info("Tenant does not exist. Created new tenant: '%s'", tenant.external_key) | ||
else: | ||
LOG.info("Found existing tenant for: '%s'", tenant.external_key) | ||
|
||
# split out lms, studio, theme, meta from configuration json | ||
if tenant_configuration_values: | ||
for field in TenantConfig._meta.get_fields(): | ||
if isinstance(field, JSONField): | ||
name = field.name | ||
value = tenant_configuration_values.get(name) | ||
if value is not None: | ||
if override: | ||
setattr(tenant, name, value) | ||
else: | ||
base_value = getattr(tenant, name, {}) | ||
merged = self.merge_dict(base_value, value) | ||
setattr(tenant, name, merged) | ||
|
||
tenant.save() | ||
# next add routes and link them | ||
for route in routes: | ||
route, created = Route.objects.update_or_create( | ||
domain=route, | ||
defaults={"config": tenant} | ||
) | ||
if created: | ||
LOG.info("Route does not exist. Created new route: '%s'", route.domain) | ||
else: | ||
LOG.info("Found existing route for: '%s'", route.domain) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
"""This module include a class that checks the command create_or_update_tenant_config.py""" | ||
import json | ||
import tempfile | ||
|
||
from django.core.management import CommandError, call_command | ||
from django.test import TestCase | ||
|
||
from eox_tenant.models import Route, TenantConfig | ||
|
||
|
||
class CreateOrUpdateTenantConfigTestCase(TestCase): | ||
""" This class checks the command create_or_update_tenant_config.py""" | ||
|
||
def setUp(self): | ||
"""This method creates TenantConfig objects in database""" | ||
self.test_conf = { | ||
"lms_configs": {"NEW KEY": "value-updated", "NESTED_KEY": {"key": "value"}}, | ||
"studio_configs": {"STUDIO_KEY": "value", "STUDIO_NESTED_KEY": {"key": "value"}, } | ||
} | ||
self.external_key = "test" | ||
TenantConfig.objects.create( | ||
external_key=self.external_key, | ||
lms_configs={ | ||
"KEY": "value", | ||
}, | ||
) | ||
|
||
def test_command_happy_path_with_cmd_config(self): | ||
"""Tests that command runs successfully if config is passed via cmd""" | ||
self.assertFalse(TenantConfig.objects.filter(external_key="new.key").exists()) | ||
self.assertFalse(Route.objects.filter(domain__contains="test.domain").exists()) | ||
call_command( | ||
"create_or_update_tenant_config", | ||
"--external-key", | ||
"new.key", | ||
"--config", | ||
json.dumps(self.test_conf), | ||
"test.domain", | ||
"studio.test.domain" | ||
) | ||
created_config = TenantConfig.objects.get(external_key="new.key") | ||
self.assertTrue(created_config.lms_configs == self.test_conf["lms_configs"]) | ||
self.assertTrue(created_config.studio_configs == self.test_conf["studio_configs"]) | ||
created_routes = Route.objects.filter(domain__contains="test.domain").count() | ||
self.assertTrue(created_routes == 2) | ||
|
||
def test_command_happy_path_with_file_config(self): | ||
"""Tests that command runs successfully if config is passed via file""" | ||
self.assertFalse(TenantConfig.objects.filter(external_key="new.key").exists()) | ||
self.assertFalse(Route.objects.filter(domain__contains="test.domain").exists()) | ||
with tempfile.NamedTemporaryFile('w') as fp: | ||
fp.write(json.dumps(self.test_conf)) | ||
fp.seek(0) | ||
call_command( | ||
"create_or_update_tenant_config", | ||
"--external-key", | ||
"new.key", | ||
"--config-file", | ||
fp.name, | ||
"test.domain", | ||
"studio.test.domain" | ||
) | ||
created_config = TenantConfig.objects.get(external_key="new.key") | ||
self.assertTrue(created_config.lms_configs == self.test_conf["lms_configs"]) | ||
created_routes = Route.objects.filter(domain__contains="test.domain").count() | ||
self.assertTrue(created_routes == 2) | ||
|
||
def test_command_invalid_config(self): | ||
"""Tests that command raises if config is invalid""" | ||
self.assertFalse(TenantConfig.objects.filter(external_key="new.key").exists()) | ||
self.assertFalse(Route.objects.filter(domain__contains="test.domain").exists()) | ||
with self.assertRaises(CommandError): | ||
call_command( | ||
"create_or_update_tenant_config", | ||
"--external-key", | ||
"new.key", | ||
"--config", | ||
'{"KEY": "value, "NESTED_KEY": {"key": "value"}}', | ||
"test.domain", | ||
"studio.test.domain" | ||
) | ||
self.assertFalse(TenantConfig.objects.filter(external_key="new.key").exists()) | ||
self.assertFalse(Route.objects.filter(domain__contains="test.domain").exists()) | ||
|
||
def test_command_with_no_config(self): | ||
""" | ||
Tests that command works even if config is not passed, | ||
i.e. it adds/updates an entry with external_key and links given routes. | ||
""" | ||
self.assertFalse(TenantConfig.objects.filter(external_key="new.key").exists()) | ||
self.assertFalse(Route.objects.filter(domain__contains="test.domain").exists()) | ||
call_command( | ||
"create_or_update_tenant_config", | ||
"--external-key", | ||
"new.key", | ||
"test.domain", | ||
"studio.test.domain" | ||
) | ||
self.assertTrue(TenantConfig.objects.filter(external_key="new.key").exists()) | ||
self.assertTrue(Route.objects.filter(domain__contains="test.domain").exists()) | ||
|
||
def test_command_with_long_external_key(self): | ||
"""Tests that command runs successfully even if external key is longer than limit.""" | ||
long_external_key = "areallyreallyreallyreallyreallyreallylongexternalkey" | ||
self.assertFalse( | ||
TenantConfig.objects.filter(external_key__in=[long_external_key, long_external_key[:63]]).exists() | ||
) | ||
self.assertFalse(Route.objects.filter(domain__contains="test.domain").exists()) | ||
call_command( | ||
"create_or_update_tenant_config", | ||
"--external-key", | ||
long_external_key, | ||
"--config", | ||
json.dumps(self.test_conf), | ||
"test.domain", | ||
"studio.test.domain" | ||
) | ||
created_config = TenantConfig.objects.get(external_key=long_external_key[:63]) | ||
self.assertTrue(created_config.lms_configs == self.test_conf["lms_configs"]) | ||
created_routes = Route.objects.filter(domain__contains="test.domain").count() | ||
self.assertTrue(created_routes == 2) | ||
|
||
def test_command_update_existing_tenant(self): | ||
"""Tests that command successfully updates existing TenantConfig.""" | ||
config = TenantConfig.objects.get(external_key=self.external_key) | ||
self.assertTrue(config.lms_configs == {"KEY": "value"}) | ||
self.assertTrue(config.studio_configs == {}) | ||
call_command( | ||
"create_or_update_tenant_config", | ||
"--external-key", | ||
self.external_key, | ||
"--config", | ||
json.dumps(self.test_conf), | ||
"test.domain", | ||
"studio.test.domain" | ||
) | ||
updated_config = TenantConfig.objects.get(external_key=self.external_key) | ||
for key, value in self.test_conf["lms_configs"].items(): | ||
self.assertTrue(updated_config.lms_configs[key] == value) | ||
self.assertTrue(updated_config.lms_configs["KEY"] == "value") | ||
self.assertTrue(updated_config.studio_configs == self.test_conf["studio_configs"]) | ||
created_routes = Route.objects.filter(domain__contains="test.domain").count() | ||
self.assertTrue(created_routes == 2) | ||
|
||
def test_command_update_existing_tenant_override(self): | ||
"""Tests that command successfully replaces existing TenantConfig with override param.""" | ||
config = TenantConfig.objects.get(external_key=self.external_key) | ||
self.assertTrue(config.studio_configs == {}) | ||
new_conf = {"lms_configs": {"NEW_KEY": "new value"}} | ||
call_command( | ||
"create_or_update_tenant_config", | ||
"--external-key", | ||
self.external_key, | ||
"--config", | ||
json.dumps(new_conf), | ||
"test.domain", | ||
"studio.test.domain", | ||
"--override", | ||
) | ||
updated_config = TenantConfig.objects.get(external_key=self.external_key) | ||
self.assertTrue(updated_config.lms_configs == new_conf["lms_configs"]) | ||
created_routes = Route.objects.filter(domain__contains="test.domain").count() | ||
self.assertTrue(created_routes == 2) |
Oops, something went wrong.