mirror of
https://github.com/ansible/awx.git
synced 2026-04-08 19:49:22 -02:30
Merge pull request #4092 from cchurch/custom-logo-custom-login-info
Add support for CUSTOM_LOGO and CUSTOM_LOGIN_INFO settings
This commit is contained in:
@@ -103,8 +103,11 @@ class ApiRootView(APIView):
|
|||||||
current_version = current,
|
current_version = current,
|
||||||
available_versions = dict(
|
available_versions = dict(
|
||||||
v1 = current
|
v1 = current
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
|
if feature_enabled('rebranding'):
|
||||||
|
data['custom_logo'] = settings.CUSTOM_LOGO
|
||||||
|
data['custom_login_info'] = settings.CUSTOM_LOGIN_INFO
|
||||||
return Response(data)
|
return Response(data)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
# All Rights Reserved.
|
# All Rights Reserved.
|
||||||
|
|
||||||
# Python
|
# Python
|
||||||
|
import base64
|
||||||
import collections
|
import collections
|
||||||
import difflib
|
import difflib
|
||||||
import json
|
import json
|
||||||
@@ -141,12 +142,85 @@ class Command(BaseCommand):
|
|||||||
os.remove(license_file)
|
os.remove(license_file)
|
||||||
return diff_lines
|
return diff_lines
|
||||||
|
|
||||||
|
def _get_local_settings_file(self):
|
||||||
|
if MODE == 'development':
|
||||||
|
static_root = os.path.join(os.path.dirname(__file__), '..', '..', '..', 'ui', 'static')
|
||||||
|
else:
|
||||||
|
static_root = settings.STATIC_ROOT
|
||||||
|
return os.path.join(static_root, 'local_settings.json')
|
||||||
|
|
||||||
|
def _comment_local_settings_file(self, dry_run=True):
|
||||||
|
local_settings_file = self._get_local_settings_file()
|
||||||
|
diff_lines = []
|
||||||
|
if os.path.exists(local_settings_file):
|
||||||
|
try:
|
||||||
|
raw_local_settings_data = open(local_settings_file).read()
|
||||||
|
json.loads(raw_local_settings_data)
|
||||||
|
except Exception as e:
|
||||||
|
if not self.skip_errors:
|
||||||
|
raise CommandError('Error reading local settings from {0}: {1!r}'.format(local_settings_file, e))
|
||||||
|
return diff_lines
|
||||||
|
if self.backup_suffix:
|
||||||
|
backup_local_settings_file = '{}{}'.format(local_settings_file, self.backup_suffix)
|
||||||
|
else:
|
||||||
|
backup_local_settings_file = '{}.old'.format(local_settings_file)
|
||||||
|
diff_lines = list(difflib.unified_diff(
|
||||||
|
raw_local_settings_data.splitlines(),
|
||||||
|
[],
|
||||||
|
fromfile=backup_local_settings_file,
|
||||||
|
tofile=local_settings_file,
|
||||||
|
lineterm='',
|
||||||
|
))
|
||||||
|
if not dry_run:
|
||||||
|
if self.backup_suffix:
|
||||||
|
shutil.copy2(local_settings_file, backup_local_settings_file)
|
||||||
|
os.remove(local_settings_file)
|
||||||
|
return diff_lines
|
||||||
|
|
||||||
|
def _get_custom_logo_file(self):
|
||||||
|
if MODE == 'development':
|
||||||
|
static_root = os.path.join(os.path.dirname(__file__), '..', '..', '..', 'ui', 'static')
|
||||||
|
else:
|
||||||
|
static_root = settings.STATIC_ROOT
|
||||||
|
return os.path.join(static_root, 'assets', 'custom_console_logo.png')
|
||||||
|
|
||||||
|
def _comment_custom_logo_file(self, dry_run=True):
|
||||||
|
custom_logo_file = self._get_custom_logo_file()
|
||||||
|
diff_lines = []
|
||||||
|
if os.path.exists(custom_logo_file):
|
||||||
|
try:
|
||||||
|
raw_custom_logo_data = open(custom_logo_file).read()
|
||||||
|
except Exception as e:
|
||||||
|
if not self.skip_errors:
|
||||||
|
raise CommandError('Error reading custom logo from {0}: {1!r}'.format(custom_logo_file, e))
|
||||||
|
return diff_lines
|
||||||
|
if self.backup_suffix:
|
||||||
|
backup_custom_logo_file = '{}{}'.format(custom_logo_file, self.backup_suffix)
|
||||||
|
else:
|
||||||
|
backup_custom_logo_file = '{}.old'.format(custom_logo_file)
|
||||||
|
diff_lines = list(difflib.unified_diff(
|
||||||
|
['<PNG Image ({} bytes)>'.format(len(raw_custom_logo_data))],
|
||||||
|
[],
|
||||||
|
fromfile=backup_custom_logo_file,
|
||||||
|
tofile=custom_logo_file,
|
||||||
|
lineterm='',
|
||||||
|
))
|
||||||
|
if not dry_run:
|
||||||
|
if self.backup_suffix:
|
||||||
|
shutil.copy2(custom_logo_file, backup_custom_logo_file)
|
||||||
|
os.remove(custom_logo_file)
|
||||||
|
return diff_lines
|
||||||
|
|
||||||
def _check_if_needs_comment(self, patterns, setting):
|
def _check_if_needs_comment(self, patterns, setting):
|
||||||
files_to_comment = []
|
files_to_comment = []
|
||||||
# If any diffs are returned, this setting needs to be commented.
|
# If any diffs are returned, this setting needs to be commented.
|
||||||
diffs = comment_assignments(patterns, setting, dry_run=True)
|
diffs = comment_assignments(patterns, setting, dry_run=True)
|
||||||
if setting == 'LICENSE':
|
if setting == 'LICENSE':
|
||||||
diffs.extend(self._comment_license_file(dry_run=True))
|
diffs.extend(self._comment_license_file(dry_run=True))
|
||||||
|
elif setting == 'CUSTOM_LOGIN_INFO':
|
||||||
|
diffs.extend(self._comment_local_settings_file(dry_run=True))
|
||||||
|
elif setting == 'CUSTOM_LOGO':
|
||||||
|
diffs.extend(self._comment_custom_logo_file(dry_run=True))
|
||||||
for diff in diffs:
|
for diff in diffs:
|
||||||
for line in diff.splitlines():
|
for line in diff.splitlines():
|
||||||
if line.startswith('+++ '):
|
if line.startswith('+++ '):
|
||||||
@@ -163,6 +237,27 @@ class Command(BaseCommand):
|
|||||||
except SkipField:
|
except SkipField:
|
||||||
pass
|
pass
|
||||||
current_value = getattr(settings, setting, empty)
|
current_value = getattr(settings, setting, empty)
|
||||||
|
if setting == 'CUSTOM_LOGIN_INFO' and current_value in {empty, ''}:
|
||||||
|
local_settings_file = self._get_local_settings_file()
|
||||||
|
try:
|
||||||
|
if os.path.exists(local_settings_file):
|
||||||
|
local_settings = json.load(open(local_settings_file))
|
||||||
|
current_value = local_settings.get('custom_login_info', '')
|
||||||
|
except Exception as e:
|
||||||
|
if not self.skip_errors:
|
||||||
|
raise CommandError('Error reading custom login info from {0}: {1!r}'.format(local_settings_file, e))
|
||||||
|
if setting == 'CUSTOM_LOGO' and current_value in {empty, ''}:
|
||||||
|
custom_logo_file = self._get_custom_logo_file()
|
||||||
|
try:
|
||||||
|
if os.path.exists(custom_logo_file):
|
||||||
|
custom_logo_data = open(custom_logo_file).read()
|
||||||
|
if custom_logo_data:
|
||||||
|
current_value = 'data:image/png;base64,{}'.format(base64.b64encode(custom_logo_data))
|
||||||
|
else:
|
||||||
|
current_value = ''
|
||||||
|
except Exception as e:
|
||||||
|
if not self.skip_errors:
|
||||||
|
raise CommandError('Error reading custom logo from {0}: {1!r}'.format(custom_logo_file, e))
|
||||||
if current_value != default_value:
|
if current_value != default_value:
|
||||||
if current_value is empty:
|
if current_value is empty:
|
||||||
current_value = None
|
current_value = None
|
||||||
@@ -338,10 +433,16 @@ class Command(BaseCommand):
|
|||||||
if to_comment:
|
if to_comment:
|
||||||
to_comment_patterns = []
|
to_comment_patterns = []
|
||||||
license_file_to_comment = None
|
license_file_to_comment = None
|
||||||
|
local_settings_file_to_comment = None
|
||||||
|
custom_logo_file_to_comment = None
|
||||||
for files_to_comment in to_comment.values():
|
for files_to_comment in to_comment.values():
|
||||||
for file_to_comment in files_to_comment:
|
for file_to_comment in files_to_comment:
|
||||||
if file_to_comment == self._get_license_file():
|
if file_to_comment == self._get_license_file():
|
||||||
license_file_to_comment = file_to_comment
|
license_file_to_comment = file_to_comment
|
||||||
|
elif file_to_comment == self._get_local_settings_file():
|
||||||
|
local_settings_file_to_comment = file_to_comment
|
||||||
|
elif file_to_comment == self._get_custom_logo_file():
|
||||||
|
custom_logo_file_to_comment = file_to_comment
|
||||||
elif file_to_comment not in to_comment_patterns:
|
elif file_to_comment not in to_comment_patterns:
|
||||||
to_comment_patterns.append(file_to_comment)
|
to_comment_patterns.append(file_to_comment)
|
||||||
# Run once in dry-run mode to catch any errors from updating the files.
|
# Run once in dry-run mode to catch any errors from updating the files.
|
||||||
@@ -351,4 +452,8 @@ class Command(BaseCommand):
|
|||||||
diffs = comment_assignments(to_comment_patterns, to_comment.keys(), dry_run=False, backup_suffix=self.backup_suffix)
|
diffs = comment_assignments(to_comment_patterns, to_comment.keys(), dry_run=False, backup_suffix=self.backup_suffix)
|
||||||
if license_file_to_comment:
|
if license_file_to_comment:
|
||||||
diffs.extend(self._comment_license_file(dry_run=False))
|
diffs.extend(self._comment_license_file(dry_run=False))
|
||||||
|
if local_settings_file_to_comment:
|
||||||
|
diffs.extend(self._comment_local_settings_file(dry_run=False))
|
||||||
|
if custom_logo_file_to_comment:
|
||||||
|
diffs.extend(self._comment_custom_logo_file(dry_run=False))
|
||||||
self._display_comment(diffs)
|
self._display_comment(diffs)
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -5,16 +5,8 @@
|
|||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
# Tower
|
# Tower
|
||||||
from awx.conf import fields, register
|
from awx.conf import register, fields
|
||||||
|
from awx.ui.fields import * # noqa
|
||||||
|
|
||||||
class PendoTrackingStateField(fields.ChoiceField):
|
|
||||||
|
|
||||||
def to_internal_value(self, data):
|
|
||||||
# Any false/null values get converted to 'off'.
|
|
||||||
if data in fields.NullBooleanField.FALSE_VALUES or data in fields.NullBooleanField.NULL_VALUES:
|
|
||||||
return 'off'
|
|
||||||
return super(PendoTrackingStateField, self).to_internal_value(data)
|
|
||||||
|
|
||||||
|
|
||||||
register(
|
register(
|
||||||
@@ -30,3 +22,35 @@ register(
|
|||||||
category=_('UI'),
|
category=_('UI'),
|
||||||
category_slug='ui',
|
category_slug='ui',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
register(
|
||||||
|
'CUSTOM_LOGIN_INFO',
|
||||||
|
field_class=fields.CharField,
|
||||||
|
allow_blank=True,
|
||||||
|
default='',
|
||||||
|
label=_('Custom Login Info'),
|
||||||
|
help_text=_('If needed, you can add specific information (such as a legal '
|
||||||
|
'notice or a disclaimer) to a text box in the login modal using '
|
||||||
|
'this setting. Any content added must be in plain text, as '
|
||||||
|
'custom HTML or other markup languages are not supported. If '
|
||||||
|
'multiple paragraphs of text are needed, new lines (paragraphs) '
|
||||||
|
'must be escaped as `\\n` within the block of text.'),
|
||||||
|
category=_('UI'),
|
||||||
|
category_slug='ui',
|
||||||
|
feature_required='rebranding',
|
||||||
|
)
|
||||||
|
|
||||||
|
register(
|
||||||
|
'CUSTOM_LOGO',
|
||||||
|
field_class=CustomLogoField,
|
||||||
|
allow_blank=True,
|
||||||
|
default='',
|
||||||
|
label=_('Custom Logo'),
|
||||||
|
help_text=_('To set up a custom logo, provide a file that you create. For '
|
||||||
|
'the custom logo to look its best, use a `.png` file with a '
|
||||||
|
'transparent background. GIF, PNG and JPEG formats are supported.'),
|
||||||
|
placeholder='data:image/gif;base64,R0lGODlhAQABAAAAADs=',
|
||||||
|
category=_('UI'),
|
||||||
|
category_slug='ui',
|
||||||
|
feature_required='rebranding',
|
||||||
|
)
|
||||||
|
|||||||
43
awx/ui/fields.py
Normal file
43
awx/ui/fields.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# Copyright (c) 2016 Ansible, Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
|
||||||
|
# Python
|
||||||
|
import base64
|
||||||
|
import re
|
||||||
|
|
||||||
|
# Django
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
# Tower
|
||||||
|
from awx.conf import fields, register
|
||||||
|
|
||||||
|
|
||||||
|
class PendoTrackingStateField(fields.ChoiceField):
|
||||||
|
|
||||||
|
def to_internal_value(self, data):
|
||||||
|
# Any false/null values get converted to 'off'.
|
||||||
|
if data in fields.NullBooleanField.FALSE_VALUES or data in fields.NullBooleanField.NULL_VALUES:
|
||||||
|
return 'off'
|
||||||
|
return super(PendoTrackingStateField, self).to_internal_value(data)
|
||||||
|
|
||||||
|
|
||||||
|
class CustomLogoField(fields.CharField):
|
||||||
|
|
||||||
|
CUSTOM_LOGO_RE = re.compile(r'^data:image/(?:png|jpeg|gif);base64,([A-Za-z0-9+/=]+?)$')
|
||||||
|
|
||||||
|
default_error_messages = {
|
||||||
|
'invalid_format': _('Invalid format for custom logo. Must be a data URL with a base64-encoded GIF, PNG or JPEG image.'),
|
||||||
|
'invalid_data': _('Invalid base64-encoded data in data URL.'),
|
||||||
|
}
|
||||||
|
|
||||||
|
def to_internal_value(self, data):
|
||||||
|
data = super(CustomLogoField, self).to_internal_value(data)
|
||||||
|
match = self.CUSTOM_LOGO_RE.match(data)
|
||||||
|
if not match:
|
||||||
|
self.fail('invalid_format')
|
||||||
|
b64data = match.group(1)
|
||||||
|
try:
|
||||||
|
base64.b64decode(b64data)
|
||||||
|
except TypeError:
|
||||||
|
self.fail('invalid_data')
|
||||||
|
return data
|
||||||
Reference in New Issue
Block a user