Merge pull request #3729 from sundeep-co-in/STAGE

enable django i18n
This commit is contained in:
Matthew Jones 2016-11-04 15:52:33 -04:00 committed by GitHub
commit 88a8810815
35 changed files with 665 additions and 428 deletions

1
.gitignore vendored
View File

@ -106,6 +106,7 @@ reports
*.log.[0-9]
*.results
local/
*.mo
# AWX python libs populated by requirements.txt
awx/lib/.deps_built

View File

@ -505,9 +505,10 @@ test_tox:
# Alias existing make target so old versions run against Jekins the same way
test_jenkins : test_coverage
# UI TASKS
# l10n TASKS
# --------------------------------------
# check for UI po files
HAVE_PO := $(shell ls awx/ui/po/*.po 2>/dev/null)
check-po:
ifdef HAVE_PO
@ -537,14 +538,32 @@ else
@echo No PO files
endif
# generate l10n .json
languages: $(UI_DEPS_FLAG_FILE) check-po
$(NPM_BIN) --prefix awx/ui run languages
# generate .pot
# generate UI .pot
pot: $(UI_DEPS_FLAG_FILE)
$(NPM_BIN) --prefix awx/ui run pot
# generate django .pot .po
LANG = "en-us"
messages:
@if [ "$(VENV_BASE)" ]; then \
. $(VENV_BASE)/tower/bin/activate; \
fi; \
$(PYTHON) manage.py makemessages -l $(LANG) --keep-pot
# generate l10n .json .mo
languages: $(UI_DEPS_FLAG_FILE) check-po
$(NPM_BIN) --prefix awx/ui run languages
@if [ "$(VENV_BASE)" ]; then \
. $(VENV_BASE)/tower/bin/activate; \
fi; \
$(PYTHON) manage.py compilemessages
# End l10n TASKS
# --------------------------------------
# UI TASKS
# --------------------------------------
ui-deps: $(UI_DEPS_FLAG_FILE)
$(UI_DEPS_FLAG_FILE): awx/ui/package.json

View File

@ -9,6 +9,7 @@ import logging
from django.conf import settings
from django.utils.timezone import now as tz_now
from django.utils.encoding import smart_text
from django.utils.translation import ugettext_lazy as _
# Django REST Framework
from rest_framework import authentication
@ -62,10 +63,10 @@ class TokenAuthentication(authentication.TokenAuthentication):
return None
if len(auth) == 1:
msg = 'Invalid token header. No credentials provided.'
msg = _('Invalid token header. No credentials provided.')
raise exceptions.AuthenticationFailed(msg)
elif len(auth) > 2:
msg = 'Invalid token header. Token string should not contain spaces.'
msg = _('Invalid token header. Token string should not contain spaces.')
raise exceptions.AuthenticationFailed(msg)
return self.authenticate_credentials(auth[1])
@ -100,7 +101,7 @@ class TokenAuthentication(authentication.TokenAuthentication):
# If the user is inactive, then return an error.
if not token.user.is_active:
raise exceptions.AuthenticationFailed('User inactive or deleted')
raise exceptions.AuthenticationFailed(_('User inactive or deleted'))
# Refresh the token.
# The token is extended from "right now" + configurable setting amount.
@ -151,7 +152,7 @@ class TaskAuthentication(authentication.BaseAuthentication):
return None
token = unified_job.task_auth_token
if auth[1] != token:
raise exceptions.AuthenticationFailed('Invalid task token')
raise exceptions.AuthenticationFailed(_('Invalid task token'))
return (None, token)
def authenticate_header(self, request):

View File

@ -15,6 +15,7 @@ from django.template.loader import render_to_string
from django.utils.encoding import smart_text
from django.utils.safestring import mark_safe
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import ugettext_lazy as _
# Django REST Framework
from rest_framework.authentication import get_authorization_header
@ -432,7 +433,7 @@ class SubListCreateAttachDetachAPIView(SubListCreateAPIView):
sub_id = request.data.get('id', None)
res = None
if not sub_id:
data = dict(msg='"id" is required to disassociate')
data = dict(msg=_('"id" is required to disassociate'))
res = Response(data, status=status.HTTP_400_BAD_REQUEST)
return (sub_id, res)

View File

@ -7,6 +7,7 @@ from collections import OrderedDict
from django.core.exceptions import PermissionDenied
from django.http import Http404
from django.utils.encoding import force_text, smart_text
from django.utils.translation import ugettext_lazy as _
# Django REST Framework
from rest_framework import exceptions
@ -46,15 +47,15 @@ class Metadata(metadata.SimpleMetadata):
serializer = getattr(field, 'parent', None)
if serializer:
field_help_text = {
'id': 'Database ID for this {}.',
'name': 'Name of this {}.',
'description': 'Optional description of this {}.',
'type': 'Data type for this {}.',
'url': 'URL for this {}.',
'related': 'Data structure with URLs of related resources.',
'summary_fields': 'Data structure with name/description for related resources.',
'created': 'Timestamp when this {} was created.',
'modified': 'Timestamp when this {} was last modified.',
'id': _('Database ID for this {}.'),
'name': _('Name of this {}.'),
'description': _('Optional description of this {}.'),
'type': _('Data type for this {}.'),
'url': _('URL for this {}.'),
'related': _('Data structure with URLs of related resources.'),
'summary_fields': _('Data structure with name/description for related resources.'),
'created': _('Timestamp when this {} was created.'),
'modified': _('Timestamp when this {} was last modified.'),
}
if field.field_name in field_help_text:
if hasattr(serializer, 'Meta') and hasattr(serializer.Meta, 'model'):

View File

@ -5,6 +5,7 @@ import json
# Django
from django.conf import settings
from django.utils import six
from django.utils.translation import ugettext_lazy as _
# Django REST Framework
from rest_framework import parsers
@ -27,4 +28,4 @@ class JSONParser(parsers.JSONParser):
data = stream.read().decode(encoding)
return json.loads(data, object_pairs_hook=OrderedDict)
except ValueError as exc:
raise ParseError('JSON parse error - %s' % six.text_type(exc))
raise ParseError(_('JSON parse error - %s') % six.text_type(exc))

View File

@ -20,7 +20,7 @@ from django.contrib.contenttypes.models import ContentType
from django.core.urlresolvers import reverse
from django.core.exceptions import ObjectDoesNotExist, ValidationError as DjangoValidationError
from django.db import models
# from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import force_text
from django.utils.text import capfirst
@ -242,11 +242,11 @@ class BaseSerializer(serializers.ModelSerializer):
def get_type_choices(self):
type_name_map = {
'job': 'Playbook Run',
'ad_hoc_command': 'Command',
'project_update': 'SCM Update',
'inventory_update': 'Inventory Sync',
'system_job': 'Management Job',
'job': _('Playbook Run'),
'ad_hoc_command': _('Command'),
'project_update': _('SCM Update'),
'inventory_update': _('Inventory Sync'),
'system_job': _('Management Job'),
}
choices = []
for t in self.get_types():
@ -623,8 +623,9 @@ class UnifiedJobSerializer(BaseSerializer):
def get_result_stdout(self, obj):
obj_size = obj.result_stdout_size
if obj_size > settings.STDOUT_MAX_BYTES_DISPLAY:
return "Standard Output too large to display (%d bytes), only download supported for sizes over %d bytes" % (obj_size,
settings.STDOUT_MAX_BYTES_DISPLAY)
return _("Standard Output too large to display (%(text_size)d bytes), "
"only download supported for sizes over %(supported_size)d bytes") % {
'text_size': obj_size, 'supported_size': settings.STDOUT_MAX_BYTES_DISPLAY}
return obj.result_stdout
@ -680,8 +681,9 @@ class UnifiedJobStdoutSerializer(UnifiedJobSerializer):
def get_result_stdout(self, obj):
obj_size = obj.result_stdout_size
if obj_size > settings.STDOUT_MAX_BYTES_DISPLAY:
return "Standard Output too large to display (%d bytes), only download supported for sizes over %d bytes" % (obj_size,
settings.STDOUT_MAX_BYTES_DISPLAY)
return _("Standard Output too large to display (%(text_size)d bytes), "
"only download supported for sizes over %(supported_size)d bytes") % {
'text_size': obj_size, 'supported_size': settings.STDOUT_MAX_BYTES_DISPLAY}
return obj.result_stdout
def get_types(self):
@ -694,9 +696,9 @@ class UnifiedJobStdoutSerializer(UnifiedJobSerializer):
class UserSerializer(BaseSerializer):
password = serializers.CharField(required=False, default='', write_only=True,
help_text='Write-only field used to change the password.')
help_text=_('Write-only field used to change the password.'))
ldap_dn = serializers.CharField(source='profile.ldap_dn', read_only=True)
external_account = serializers.SerializerMethodField(help_text='Set if the account is managed by an external service')
external_account = serializers.SerializerMethodField(help_text=_('Set if the account is managed by an external service'))
is_system_auditor = serializers.BooleanField(default=False)
show_capabilities = ['edit', 'delete']
@ -720,7 +722,7 @@ class UserSerializer(BaseSerializer):
def validate_password(self, value):
if not self.instance and value in (None, ''):
raise serializers.ValidationError('Password required for new User.')
raise serializers.ValidationError(_('Password required for new User.'))
return value
def _update_password(self, obj, new_password):
@ -804,7 +806,7 @@ class UserSerializer(BaseSerializer):
ldap_managed_fields.extend(getattr(settings, 'AUTH_LDAP_USER_FLAGS_BY_GROUP', {}).keys())
if field_name in ldap_managed_fields:
if value != getattr(self.instance, field_name):
raise serializers.ValidationError('Unable to change %s on user managed by LDAP.' % field_name)
raise serializers.ValidationError(_('Unable to change %s on user managed by LDAP.') % field_name)
return value
def validate_username(self, value):
@ -955,13 +957,13 @@ class ProjectSerializer(UnifiedJobTemplateSerializer, ProjectOptionsSerializer):
view = self.context.get('view', None)
if not organization and not view.request.user.is_superuser:
# Only allow super users to create orgless projects
raise serializers.ValidationError('Organization is missing')
raise serializers.ValidationError(_('Organization is missing'))
return super(ProjectSerializer, self).validate(attrs)
class ProjectPlaybooksSerializer(ProjectSerializer):
playbooks = serializers.SerializerMethodField(help_text='Array of playbooks available within this project.')
playbooks = serializers.SerializerMethodField(help_text=_('Array of playbooks available within this project.'))
class Meta:
model = Project
@ -1143,7 +1145,7 @@ class HostSerializer(BaseSerializerWithVariables):
if port < 1 or port > 65535:
raise ValueError
except ValueError:
raise serializers.ValidationError(u'Invalid port specification: %s' % force_text(port))
raise serializers.ValidationError(_(u'Invalid port specification: %s') % force_text(port))
return name, port
def validate_name(self, value):
@ -1171,7 +1173,7 @@ class HostSerializer(BaseSerializerWithVariables):
vars_dict['ansible_ssh_port'] = port
attrs['variables'] = yaml.dump(vars_dict)
except (yaml.YAMLError, TypeError):
raise serializers.ValidationError('Must be valid JSON or YAML.')
raise serializers.ValidationError(_('Must be valid JSON or YAML.'))
return super(HostSerializer, self).validate(attrs)
@ -1228,7 +1230,7 @@ class GroupSerializer(BaseSerializerWithVariables):
def validate_name(self, value):
if value in ('all', '_meta'):
raise serializers.ValidationError('Invalid group name.')
raise serializers.ValidationError(_('Invalid group name.'))
return value
def to_representation(self, obj):
@ -1302,7 +1304,7 @@ class CustomInventoryScriptSerializer(BaseSerializer):
def validate_script(self, value):
if not value.startswith("#!"):
raise serializers.ValidationError('Script must begin with a hashbang sequence: i.e.... #!/usr/bin/env python')
raise serializers.ValidationError(_('Script must begin with a hashbang sequence: i.e.... #!/usr/bin/env python'))
return value
def to_representation(self, obj):
@ -1355,13 +1357,13 @@ class InventorySourceOptionsSerializer(BaseSerializer):
source_script = attrs.get('source_script', self.instance and self.instance.source_script or '')
if source == 'custom':
if source_script is None or source_script == '':
errors['source_script'] = "If 'source' is 'custom', 'source_script' must be provided."
errors['source_script'] = _("If 'source' is 'custom', 'source_script' must be provided.")
else:
try:
if source_script.organization != self.instance.inventory.organization:
errors['source_script'] = "The 'source_script' does not belong to the same organization as the inventory."
errors['source_script'] = _("The 'source_script' does not belong to the same organization as the inventory.")
except Exception as exc:
errors['source_script'] = "'source_script' doesn't exist."
errors['source_script'] = _("'source_script' doesn't exist.")
logger.error(str(exc))
if errors:
@ -1721,18 +1723,18 @@ class CredentialSerializerCreate(CredentialSerializer):
user = serializers.PrimaryKeyRelatedField(
queryset=User.objects.all(),
required=False, default=None, write_only=True, allow_null=True,
help_text='Write-only field used to add user to owner role. If provided, '
'do not give either team or organization. Only valid for creation.')
help_text=_('Write-only field used to add user to owner role. If provided, '
'do not give either team or organization. Only valid for creation.'))
team = serializers.PrimaryKeyRelatedField(
queryset=Team.objects.all(),
required=False, default=None, write_only=True, allow_null=True,
help_text='Write-only field used to add team to owner role. If provided, '
'do not give either user or organization. Only valid for creation.')
help_text=_('Write-only field used to add team to owner role. If provided, '
'do not give either user or organization. Only valid for creation.'))
organization = serializers.PrimaryKeyRelatedField(
queryset=Organization.objects.all(),
required=False, default=None, write_only=True, allow_null=True,
help_text='Write-only field used to add organization to owner role. If provided, '
'do not give either team or team. Only valid for creation.')
help_text=_('Write-only field used to add organization to owner role. If provided, '
'do not give either team or team. Only valid for creation.'))
class Meta:
model = Credential
@ -1747,7 +1749,7 @@ class CredentialSerializerCreate(CredentialSerializer):
else:
attrs.pop(field)
if not owner_fields:
raise serializers.ValidationError({"detail": "Missing 'user', 'team', or 'organization'."})
raise serializers.ValidationError({"detail": _("Missing 'user', 'team', or 'organization'.")})
return super(CredentialSerializerCreate, self).validate(attrs)
def create(self, validated_data):
@ -1760,7 +1762,7 @@ class CredentialSerializerCreate(CredentialSerializer):
credential.admin_role.members.add(user)
if team:
if not credential.organization or team.organization.id != credential.organization.id:
raise serializers.ValidationError({"detail": "Credential organization must be set and match before assigning to a team"})
raise serializers.ValidationError({"detail": _("Credential organization must be set and match before assigning to a team")})
credential.admin_role.parents.add(team.admin_role)
credential.use_role.parents.add(team.member_role)
return credential
@ -1846,11 +1848,11 @@ class JobOptionsSerializer(LabelsListMixin, BaseSerializer):
playbook = attrs.get('playbook', self.instance and self.instance.playbook or '')
job_type = attrs.get('job_type', self.instance and self.instance.job_type or None)
if not project and job_type != PERM_INVENTORY_SCAN:
raise serializers.ValidationError({'project': 'This field is required.'})
raise serializers.ValidationError({'project': _('This field is required.')})
if project and playbook and force_text(playbook) not in project.playbook_files:
raise serializers.ValidationError({'playbook': 'Playbook not found for project.'})
raise serializers.ValidationError({'playbook': _('Playbook not found for project.')})
if project and not playbook:
raise serializers.ValidationError({'playbook': 'Must select playbook for project.'})
raise serializers.ValidationError({'playbook': _('Must select playbook for project.')})
return super(JobOptionsSerializer, self).validate(attrs)
@ -1903,12 +1905,12 @@ class JobTemplateSerializer(UnifiedJobTemplateSerializer, JobOptionsSerializer):
if job_type == "scan":
if inventory is None or attrs.get('ask_inventory_on_launch', False):
raise serializers.ValidationError({'inventory': 'Scan jobs must be assigned a fixed inventory.'})
raise serializers.ValidationError({'inventory': _('Scan jobs must be assigned a fixed inventory.')})
elif project is None:
raise serializers.ValidationError({'project': "Job types 'run' and 'check' must have assigned a project."})
raise serializers.ValidationError({'project': _("Job types 'run' and 'check' must have assigned a project.")})
if survey_enabled and job_type == PERM_INVENTORY_SCAN:
raise serializers.ValidationError({'survey_enabled': 'Survey Enabled can not be used with scan jobs.'})
raise serializers.ValidationError({'survey_enabled': _('Survey Enabled can not be used with scan jobs.')})
return super(JobTemplateSerializer, self).validate(attrs)
@ -1968,7 +1970,7 @@ class JobSerializer(UnifiedJobSerializer, JobOptionsSerializer):
try:
job_template = JobTemplate.objects.get(pk=data['job_template'])
except JobTemplate.DoesNotExist:
raise serializers.ValidationError({'job_template': 'Invalid job template.'})
raise serializers.ValidationError({'job_template': _('Invalid job template.')})
data.setdefault('name', job_template.name)
data.setdefault('description', job_template.description)
data.setdefault('job_type', job_template.job_type)
@ -2053,11 +2055,11 @@ class JobRelaunchSerializer(JobSerializer):
def validate(self, attrs):
obj = self.context.get('obj')
if not obj.credential:
raise serializers.ValidationError(dict(credential=["Credential not found or deleted."]))
raise serializers.ValidationError(dict(credential=[_("Credential not found or deleted.")]))
if obj.job_type != PERM_INVENTORY_SCAN and obj.project is None:
raise serializers.ValidationError(dict(errors=["Job Template Project is missing or undefined."]))
raise serializers.ValidationError(dict(errors=[_("Job Template Project is missing or undefined.")]))
if obj.inventory is None:
raise serializers.ValidationError(dict(errors=["Job Template Inventory is missing or undefined."]))
raise serializers.ValidationError(dict(errors=[_("Job Template Inventory is missing or undefined.")]))
attrs = super(JobRelaunchSerializer, self).validate(attrs)
return attrs
@ -2315,15 +2317,17 @@ class WorkflowJobTemplateNodeSerializer(WorkflowNodeBaseSerializer):
job_types = [t for t, v in JOB_TYPE_CHOICES]
if attrs['char_prompts']['job_type'] not in job_types:
raise serializers.ValidationError({
"job_type": "%s is not a valid job type. The choices are %s." % (
attrs['char_prompts']['job_type'], job_types)})
"job_type": _("%(job_type)s is not a valid job type. The choices are %(choices)s.") % {
'job_type': attrs['char_prompts']['job_type'], 'choices': job_types}})
if self.instance is None and ('workflow_job_template' not in attrs or
attrs['workflow_job_template'] is None):
raise serializers.ValidationError({"workflow_job_template": "Workflow job template is missing during creation"})
raise serializers.ValidationError({
"workflow_job_template": _("Workflow job template is missing during creation")
})
ujt_obj = attrs.get('unified_job_template', None)
if isinstance(ujt_obj, (WorkflowJobTemplate, SystemJobTemplate)):
raise serializers.ValidationError({
"unified_job_template": "Can not nest a %s inside a WorkflowJobTemplate" % ujt_obj.__class__.__name__})
"unified_job_template": _("Can not nest a %s inside a WorkflowJobTemplate") % ujt_obj.__class__.__name__})
return super(WorkflowJobTemplateNodeSerializer, self).validate(attrs)
class WorkflowJobNodeSerializer(WorkflowNodeBaseSerializer):
@ -2531,7 +2535,7 @@ class JobLaunchSerializer(BaseSerializer):
for field in obj.resources_needed_to_start:
if not (attrs.get(field, False) and obj._ask_for_vars_dict().get(field, False)):
errors[field] = "Job Template '%s' is missing or undefined." % field
errors[field] = _("Job Template '%s' is missing or undefined.") % field
if (not obj.ask_credential_on_launch) or (not attrs.get('credential', None)):
credential = obj.credential
@ -2557,7 +2561,7 @@ class JobLaunchSerializer(BaseSerializer):
extra_vars = yaml.safe_load(extra_vars)
assert isinstance(extra_vars, dict)
except (yaml.YAMLError, TypeError, AttributeError, AssertionError):
errors['extra_vars'] = 'Must be a valid JSON or YAML dictionary.'
errors['extra_vars'] = _('Must be a valid JSON or YAML dictionary.')
if not isinstance(extra_vars, dict):
extra_vars = {}
@ -2641,7 +2645,7 @@ class NotificationTemplateSerializer(BaseSerializer):
else:
notification_type = None
if not notification_type:
raise serializers.ValidationError('Missing required fields for Notification Configuration: notification_type')
raise serializers.ValidationError(_('Missing required fields for Notification Configuration: notification_type'))
notification_class = NotificationTemplate.CLASS_FOR_NOTIFICATION_TYPE[notification_type]
missing_fields = []
@ -2664,16 +2668,16 @@ class NotificationTemplateSerializer(BaseSerializer):
incorrect_type_fields.append((field, field_type))
continue
if field_type == "list" and len(field_val) < 1:
error_list.append("No values specified for field '{}'".format(field))
error_list.append(_("No values specified for field '{}'").format(field))
continue
if field_type == "password" and field_val == "$encrypted$" and object_actual is not None:
attrs['notification_configuration'][field] = object_actual.notification_configuration[field]
if missing_fields:
error_list.append("Missing required fields for Notification Configuration: {}.".format(missing_fields))
error_list.append(_("Missing required fields for Notification Configuration: {}.").format(missing_fields))
if incorrect_type_fields:
for type_field_error in incorrect_type_fields:
error_list.append("Configuration field '{}' incorrect type, expected {}.".format(type_field_error[0],
type_field_error[1]))
error_list.append(_("Configuration field '{}' incorrect type, expected {}.").format(type_field_error[0],
type_field_error[1]))
if error_list:
raise serializers.ValidationError(error_list)
return attrs
@ -2722,7 +2726,7 @@ class ScheduleSerializer(BaseSerializer):
def validate_unified_job_template(self, value):
if type(value) == InventorySource and value.source not in SCHEDULEABLE_PROVIDERS:
raise serializers.ValidationError('Inventory Source must be a cloud resource.')
raise serializers.ValidationError(_('Inventory Source must be a cloud resource.'))
return value
# We reject rrules if:
@ -2744,37 +2748,37 @@ class ScheduleSerializer(BaseSerializer):
match_multiple_dtstart = re.findall(".*?(DTSTART\:[0-9]+T[0-9]+Z)", rrule_value)
match_multiple_rrule = re.findall(".*?(RRULE\:)", rrule_value)
if not len(match_multiple_dtstart):
raise serializers.ValidationError('DTSTART required in rrule. Value should match: DTSTART:YYYYMMDDTHHMMSSZ')
raise serializers.ValidationError(_('DTSTART required in rrule. Value should match: DTSTART:YYYYMMDDTHHMMSSZ'))
if len(match_multiple_dtstart) > 1:
raise serializers.ValidationError('Multiple DTSTART is not supported.')
raise serializers.ValidationError(_('Multiple DTSTART is not supported.'))
if not len(match_multiple_rrule):
raise serializers.ValidationError('RRULE require in rrule.')
raise serializers.ValidationError(_('RRULE require in rrule.'))
if len(match_multiple_rrule) > 1:
raise serializers.ValidationError('Multiple RRULE is not supported.')
raise serializers.ValidationError(_('Multiple RRULE is not supported.'))
if 'interval' not in rrule_value.lower():
raise serializers.ValidationError('INTERVAL required in rrule.')
raise serializers.ValidationError(_('INTERVAL required in rrule.'))
if 'tzid' in rrule_value.lower():
raise serializers.ValidationError('TZID is not supported.')
raise serializers.ValidationError(_('TZID is not supported.'))
if 'secondly' in rrule_value.lower():
raise serializers.ValidationError('SECONDLY is not supported.')
raise serializers.ValidationError(_('SECONDLY is not supported.'))
if re.match(multi_by_month_day, rrule_value):
raise serializers.ValidationError('Multiple BYMONTHDAYs not supported.')
raise serializers.ValidationError(_('Multiple BYMONTHDAYs not supported.'))
if re.match(multi_by_month, rrule_value):
raise serializers.ValidationError('Multiple BYMONTHs not supported.')
raise serializers.ValidationError(_('Multiple BYMONTHs not supported.'))
if re.match(by_day_with_numeric_prefix, rrule_value):
raise serializers.ValidationError("BYDAY with numeric prefix not supported.")
raise serializers.ValidationError(_("BYDAY with numeric prefix not supported."))
if 'byyearday' in rrule_value.lower():
raise serializers.ValidationError("BYYEARDAY not supported.")
raise serializers.ValidationError(_("BYYEARDAY not supported."))
if 'byweekno' in rrule_value.lower():
raise serializers.ValidationError("BYWEEKNO not supported.")
raise serializers.ValidationError(_("BYWEEKNO not supported."))
if match_count:
count_val = match_count.groups()[0].strip().split("=")
if int(count_val[1]) > 999:
raise serializers.ValidationError("COUNT > 999 is unsupported.")
raise serializers.ValidationError(_("COUNT > 999 is unsupported."))
try:
rrule.rrulestr(rrule_value)
except Exception:
raise serializers.ValidationError("rrule parsing failed validation.")
raise serializers.ValidationError(_("rrule parsing failed validation."))
return value
class ActivityStreamSerializer(BaseSerializer):
@ -2791,15 +2795,15 @@ class ActivityStreamSerializer(BaseSerializer):
ret = super(ActivityStreamSerializer, self).get_fields()
for key, field in ret.items():
if key == 'changes':
field.help_text = 'A summary of the new and changed values when an object is created, updated, or deleted'
field.help_text = _('A summary of the new and changed values when an object is created, updated, or deleted')
if key == 'object1':
field.help_text = ('For create, update, and delete events this is the object type that was affected. '
'For associate and disassociate events this is the object type associated or disassociated with object2.')
field.help_text = _('For create, update, and delete events this is the object type that was affected. '
'For associate and disassociate events this is the object type associated or disassociated with object2.')
if key == 'object2':
field.help_text = ('Unpopulated for create, update, and delete events. For associate and disassociate '
'events this is the object type that object1 is being associated with.')
field.help_text = _('Unpopulated for create, update, and delete events. For associate and disassociate '
'events this is the object type that object1 is being associated with.')
if key == 'operation':
field.help_text = 'The action taken with respect to the given object(s).'
field.help_text = _('The action taken with respect to the given object(s).')
return ret
def get_changes(self, obj):
@ -2822,7 +2826,7 @@ class ActivityStreamSerializer(BaseSerializer):
rel = {}
if obj.actor is not None:
rel['actor'] = reverse('api:user_detail', args=(obj.actor.pk,))
for fk, _ in SUMMARIZABLE_FK_FIELDS.items():
for fk, __ in SUMMARIZABLE_FK_FIELDS.items():
if not hasattr(obj, fk):
continue
allm2m = getattr(obj, fk).distinct()
@ -2899,9 +2903,9 @@ class AuthTokenSerializer(serializers.Serializer):
attrs['user'] = user
return attrs
else:
raise serializers.ValidationError('Unable to login with provided credentials.')
raise serializers.ValidationError(_('Unable to login with provided credentials.'))
else:
raise serializers.ValidationError('Must include "username" and "password".')
raise serializers.ValidationError(_('Must include "username" and "password".'))
class FactVersionSerializer(BaseFactSerializer):

View File

@ -30,6 +30,7 @@ from django.template.loader import render_to_string
from django.core.servers.basehttp import FileWrapper
from django.http import HttpResponse
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import ugettext_lazy as _
# Django REST Framework
@ -88,14 +89,14 @@ class ApiRootView(APIView):
authentication_classes = []
permission_classes = (AllowAny,)
view_name = 'REST API'
view_name = _('REST API')
def get(self, request, format=None):
''' list supported API versions '''
current = reverse('api:api_v1_root_view', args=[])
data = dict(
description = 'Ansible Tower REST API',
description = _('Ansible Tower REST API'),
current_version = current,
available_versions = dict(
v1 = current
@ -107,7 +108,7 @@ class ApiV1RootView(APIView):
authentication_classes = []
permission_classes = (AllowAny,)
view_name = 'Version 1'
view_name = _('Version 1')
def get(self, request, format=None):
''' list top level resources '''
@ -158,7 +159,7 @@ class ApiV1PingView(APIView):
"""
permission_classes = (AllowAny,)
authentication_classes = ()
view_name = 'Ping'
view_name = _('Ping')
new_in_210 = True
def get(self, request, format=None):
@ -185,7 +186,7 @@ class ApiV1PingView(APIView):
class ApiV1ConfigView(APIView):
permission_classes = (IsAuthenticated,)
view_name = 'Configuration'
view_name = _('Configuration')
def get(self, request, format=None):
'''Return various sitewide configuration settings.'''
@ -235,29 +236,29 @@ class ApiV1ConfigView(APIView):
if not request.user.is_superuser:
return Response(None, status=status.HTTP_404_NOT_FOUND)
if not isinstance(request.data, dict):
return Response({"error": "Invalid license data"}, status=status.HTTP_400_BAD_REQUEST)
return Response({"error": _("Invalid license data")}, status=status.HTTP_400_BAD_REQUEST)
if "eula_accepted" not in request.data:
return Response({"error": "Missing 'eula_accepted' property"}, status=status.HTTP_400_BAD_REQUEST)
return Response({"error": _("Missing 'eula_accepted' property")}, status=status.HTTP_400_BAD_REQUEST)
try:
eula_accepted = to_python_boolean(request.data["eula_accepted"])
except ValueError:
return Response({"error": "'eula_accepted' value is invalid"}, status=status.HTTP_400_BAD_REQUEST)
return Response({"error": _("'eula_accepted' value is invalid")}, status=status.HTTP_400_BAD_REQUEST)
if not eula_accepted:
return Response({"error": "'eula_accepted' must be True"}, status=status.HTTP_400_BAD_REQUEST)
return Response({"error": _("'eula_accepted' must be True")}, status=status.HTTP_400_BAD_REQUEST)
request.data.pop("eula_accepted")
try:
data_actual = json.dumps(request.data)
except Exception:
# FIX: Log
return Response({"error": "Invalid JSON"}, status=status.HTTP_400_BAD_REQUEST)
return Response({"error": _("Invalid JSON")}, status=status.HTTP_400_BAD_REQUEST)
try:
from awx.main.task_engine import TaskEnhancer
license_data = json.loads(data_actual)
license_data_validated = TaskEnhancer(**license_data).validate_enhancements()
except Exception:
# FIX: Log
return Response({"error": "Invalid License"}, status=status.HTTP_400_BAD_REQUEST)
return Response({"error": _("Invalid License")}, status=status.HTTP_400_BAD_REQUEST)
# If the license is valid, write it to the database.
if license_data_validated['valid_key']:
@ -265,7 +266,7 @@ class ApiV1ConfigView(APIView):
settings.TOWER_URL_BASE = "{}://{}".format(request.scheme, request.get_host())
return Response(license_data_validated)
return Response({"error": "Invalid license"}, status=status.HTTP_400_BAD_REQUEST)
return Response({"error": _("Invalid license")}, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request):
if not request.user.is_superuser:
@ -276,12 +277,12 @@ class ApiV1ConfigView(APIView):
return Response(status=status.HTTP_204_NO_CONTENT)
except:
# FIX: Log
return Response({"error": "Failed to remove license (%s)" % has_error}, status=status.HTTP_400_BAD_REQUEST)
return Response({"error": _("Failed to remove license (%s)") % has_error}, status=status.HTTP_400_BAD_REQUEST)
class DashboardView(APIView):
view_name = "Dashboard"
view_name = _("Dashboard")
new_in_14 = True
def get(self, request, format=None):
@ -386,7 +387,7 @@ class DashboardView(APIView):
class DashboardJobsGraphView(APIView):
view_name = "Dashboard Jobs Graphs"
view_name = _("Dashboard Jobs Graphs")
new_in_200 = True
def get(self, request, format=None):
@ -422,7 +423,7 @@ class DashboardJobsGraphView(APIView):
end_date = start_date - dateutil.relativedelta.relativedelta(days=1)
interval = 'hours'
else:
return Response({'error': 'Unknown period "%s"' % str(period)}, status=status.HTTP_400_BAD_REQUEST)
return Response({'error': _('Unknown period "%s"') % str(period)}, status=status.HTTP_400_BAD_REQUEST)
dashboard_data = {"jobs": {"successful": [], "failed": []}}
for element in success_qss.time_series(end_date, start_date, interval=interval):
@ -436,7 +437,7 @@ class DashboardJobsGraphView(APIView):
class ScheduleList(ListAPIView):
view_name = "Schedules"
view_name = _("Schedules")
model = Schedule
serializer_class = ScheduleSerializer
new_in_148 = True
@ -453,7 +454,7 @@ class ScheduleUnifiedJobsList(SubListAPIView):
serializer_class = UnifiedJobSerializer
parent_model = Schedule
relationship = 'unifiedjob_set'
view_name = 'Schedule Jobs List'
view_name = _('Schedule Jobs List')
new_in_148 = True
class AuthView(APIView):
@ -655,8 +656,8 @@ class OrganizationList(OrganizationCountsMixin, ListCreateAPIView):
# if no organizations exist in the system.
if (not feature_enabled('multiple_organizations') and
self.model.objects.exists()):
raise LicenseForbids('Your Tower license only permits a single '
'organization to exist.')
raise LicenseForbids(_('Your Tower license only permits a single '
'organization to exist.'))
# Okay, create the organization as usual.
return super(OrganizationList, self).create(request, *args, **kwargs)
@ -766,8 +767,8 @@ class OrganizationActivityStreamList(SubListAPIView):
# Sanity check: Does this license allow activity streams?
# If not, forbid this request.
if not feature_enabled('activity_streams'):
raise LicenseForbids('Your license does not allow use of '
'the activity stream.')
raise LicenseForbids(_('Your license does not allow use of '
'the activity stream.'))
# Okay, let it through.
return super(OrganizationActivityStreamList, self).get(request, *args, **kwargs)
@ -860,20 +861,20 @@ class TeamRolesList(SubListCreateAttachDetachAPIView):
# Forbid implicit role creation here
sub_id = request.data.get('id', None)
if not sub_id:
data = dict(msg="Role 'id' field is missing.")
data = dict(msg=_("Role 'id' field is missing."))
return Response(data, status=status.HTTP_400_BAD_REQUEST)
role = get_object_or_400(Role, pk=sub_id)
org_content_type = ContentType.objects.get_for_model(Organization)
if role.content_type == org_content_type:
data = dict(msg="You cannot assign an Organization role as a child role for a Team.")
data = dict(msg=_("You cannot assign an Organization role as a child role for a Team."))
return Response(data, status=status.HTTP_400_BAD_REQUEST)
team = get_object_or_404(Team, pk=self.kwargs['pk'])
credential_content_type = ContentType.objects.get_for_model(Credential)
if role.content_type == credential_content_type:
if not role.content_object.organization or role.content_object.organization.id != team.organization.id:
data = dict(msg="You cannot grant credential access to a team when the Organization field isn't set, or belongs to a different organization")
data = dict(msg=_("You cannot grant credential access to a team when the Organization field isn't set, or belongs to a different organization"))
return Response(data, status=status.HTTP_400_BAD_REQUEST)
return super(TeamRolesList, self).post(request, *args, **kwargs)
@ -919,8 +920,8 @@ class TeamActivityStreamList(SubListAPIView):
# Sanity check: Does this license allow activity streams?
# If not, forbid this request.
if not feature_enabled('activity_streams'):
raise LicenseForbids('Your license does not allow use of '
'the activity stream.')
raise LicenseForbids(_('Your license does not allow use of '
'the activity stream.'))
# Okay, let it through.
return super(TeamActivityStreamList, self).get(request, *args, **kwargs)
@ -966,7 +967,7 @@ class ProjectDetail(RetrieveUpdateDestroyAPIView):
obj = self.get_object()
can_delete = request.user.can_access(Project, 'delete', obj)
if not can_delete:
raise PermissionDenied("Cannot delete project.")
raise PermissionDenied(_("Cannot delete project."))
for pu in obj.project_updates.filter(status__in=['new', 'pending', 'waiting', 'running']):
pu.cancel()
return super(ProjectDetail, self).destroy(request, *args, **kwargs)
@ -992,7 +993,7 @@ class ProjectTeamsList(ListAPIView):
class ProjectSchedulesList(SubListCreateAttachDetachAPIView):
view_name = "Project Schedules"
view_name = _("Project Schedules")
model = Schedule
serializer_class = ScheduleSerializer
@ -1013,8 +1014,8 @@ class ProjectActivityStreamList(SubListAPIView):
# Sanity check: Does this license allow activity streams?
# If not, forbid this request.
if not feature_enabled('activity_streams'):
raise LicenseForbids('Your license does not allow use of '
'the activity stream.')
raise LicenseForbids(_('Your license does not allow use of '
'the activity stream.'))
# Okay, let it through.
return super(ProjectActivityStreamList, self).get(request, *args, **kwargs)
@ -1161,7 +1162,7 @@ class UserMeList(ListAPIView):
model = User
serializer_class = UserSerializer
view_name = 'Me'
view_name = _('Me')
def get_queryset(self):
return self.model.objects.filter(pk=self.request.user.pk)
@ -1199,26 +1200,26 @@ class UserRolesList(SubListCreateAttachDetachAPIView):
# Forbid implicit role creation here
sub_id = request.data.get('id', None)
if not sub_id:
data = dict(msg="Role 'id' field is missing.")
data = dict(msg=_("Role 'id' field is missing."))
return Response(data, status=status.HTTP_400_BAD_REQUEST)
if sub_id == self.request.user.admin_role.pk:
raise PermissionDenied('You may not perform any action with your own admin_role.')
raise PermissionDenied(_('You may not perform any action with your own admin_role.'))
user = get_object_or_400(User, pk=self.kwargs['pk'])
role = get_object_or_400(Role, pk=sub_id)
user_content_type = ContentType.objects.get_for_model(User)
if role.content_type == user_content_type:
raise PermissionDenied('You may not change the membership of a users admin_role')
raise PermissionDenied(_('You may not change the membership of a users admin_role'))
credential_content_type = ContentType.objects.get_for_model(Credential)
if role.content_type == credential_content_type:
if role.content_object.organization and user not in role.content_object.organization.member_role:
data = dict(msg="You cannot grant credential access to a user not in the credentials' organization")
data = dict(msg=_("You cannot grant credential access to a user not in the credentials' organization"))
return Response(data, status=status.HTTP_400_BAD_REQUEST)
if not role.content_object.organization and not request.user.is_superuser:
data = dict(msg="You cannot grant private credential access to another user")
data = dict(msg=_("You cannot grant private credential access to another user"))
return Response(data, status=status.HTTP_400_BAD_REQUEST)
@ -1281,8 +1282,8 @@ class UserActivityStreamList(SubListAPIView):
# Sanity check: Does this license allow activity streams?
# If not, forbid this request.
if not feature_enabled('activity_streams'):
raise LicenseForbids('Your license does not allow use of '
'the activity stream.')
raise LicenseForbids(_('Your license does not allow use of '
'the activity stream.'))
# Okay, let it through.
return super(UserActivityStreamList, self).get(request, *args, **kwargs)
@ -1322,13 +1323,13 @@ class UserDetail(RetrieveUpdateDestroyAPIView):
if left is not None and right is not None and left != right:
bad_changes[field] = (left, right)
if bad_changes:
raise PermissionDenied('Cannot change %s.' % ', '.join(bad_changes.keys()))
raise PermissionDenied(_('Cannot change %s.') % ', '.join(bad_changes.keys()))
def destroy(self, request, *args, **kwargs):
obj = self.get_object()
can_delete = request.user.can_access(User, 'delete', obj)
if not can_delete:
raise PermissionDenied('Cannot delete user.')
raise PermissionDenied(_('Cannot delete user.'))
return super(UserDetail, self).destroy(request, *args, **kwargs)
class UserAccessList(ResourceAccessList):
@ -1440,8 +1441,8 @@ class CredentialActivityStreamList(SubListAPIView):
# Sanity check: Does this license allow activity streams?
# If not, forbid this request.
if not feature_enabled('activity_streams'):
raise LicenseForbids('Your license does not allow use of '
'the activity stream.')
raise LicenseForbids(_('Your license does not allow use of '
'the activity stream.'))
# Okay, let it through.
return super(CredentialActivityStreamList, self).get(request, *args, **kwargs)
@ -1478,7 +1479,7 @@ class InventoryScriptDetail(RetrieveUpdateDestroyAPIView):
instance = self.get_object()
can_delete = request.user.can_access(self.model, 'delete', instance)
if not can_delete:
raise PermissionDenied("Cannot delete inventory script.")
raise PermissionDenied(_("Cannot delete inventory script."))
for inv_src in InventorySource.objects.filter(source_script=instance):
inv_src.source_script = None
inv_src.save()
@ -1529,8 +1530,8 @@ class InventoryActivityStreamList(SubListAPIView):
# Sanity check: Does this license allow activity streams?
# If not, forbid this request.
if not feature_enabled('activity_streams'):
raise LicenseForbids('Your license does not allow use of '
'the activity stream.')
raise LicenseForbids(_('Your license does not allow use of '
'the activity stream.'))
# Okay, let it through.
return super(InventoryActivityStreamList, self).get(request, *args, **kwargs)
@ -1662,8 +1663,8 @@ class HostActivityStreamList(SubListAPIView):
# Sanity check: Does this license allow activity streams?
# If not, forbid this request.
if not feature_enabled('activity_streams'):
raise LicenseForbids('Your license does not allow use of '
'the activity stream.')
raise LicenseForbids(_('Your license does not allow use of '
'the activity stream.'))
# Okay, let it through.
return super(HostActivityStreamList, self).get(request, *args, **kwargs)
@ -1680,8 +1681,8 @@ class SystemTrackingEnforcementMixin(APIView):
'''
def check_permissions(self, request):
if not feature_enabled("system_tracking"):
raise LicenseForbids("Your license does not permit use "
"of system tracking.")
raise LicenseForbids(_("Your license does not permit use "
"of system tracking."))
return super(SystemTrackingEnforcementMixin, self).check_permissions(request)
class HostFactVersionsList(ListAPIView, ParentMixin, SystemTrackingEnforcementMixin):
@ -1725,7 +1726,7 @@ class HostFactCompareView(SubDetailAPIView, SystemTrackingEnforcementMixin):
fact_entry = Fact.get_host_fact(host_obj.id, module_spec, datetime_actual)
if not fact_entry:
return Response({'detail': 'Fact not found.'}, status=status.HTTP_404_NOT_FOUND)
return Response({'detail': _('Fact not found.')}, status=status.HTTP_404_NOT_FOUND)
return Response(self.serializer_class(instance=fact_entry).data)
class GroupList(ListCreateAPIView):
@ -1847,8 +1848,8 @@ class GroupActivityStreamList(SubListAPIView):
# Sanity check: Does this license allow activity streams?
# If not, forbid this request.
if not feature_enabled('activity_streams'):
raise LicenseForbids('Your license does not allow use of '
'the activity stream.')
raise LicenseForbids(_('Your license does not allow use of '
'the activity stream.'))
# Okay, let it through.
return super(GroupActivityStreamList, self).get(request, *args, **kwargs)
@ -2037,7 +2038,7 @@ class InventoryInventorySourcesList(SubListAPIView):
serializer_class = InventorySourceSerializer
parent_model = Inventory
relationship = None # Not defined since using get_queryset().
view_name = 'Inventory Source List'
view_name = _('Inventory Source List')
new_in_14 = True
def get_queryset(self):
@ -2063,14 +2064,14 @@ class InventorySourceDetail(RetrieveUpdateAPIView):
obj = self.get_object()
can_delete = request.user.can_access(InventorySource, 'delete', obj)
if not can_delete:
raise PermissionDenied("Cannot delete inventory source.")
raise PermissionDenied(_("Cannot delete inventory source."))
for pu in obj.inventory_updates.filter(status__in=['new', 'pending', 'waiting', 'running']):
pu.cancel()
return super(InventorySourceDetail, self).destroy(request, *args, **kwargs)
class InventorySourceSchedulesList(SubListCreateAttachDetachAPIView):
view_name = "Inventory Source Schedules"
view_name = _("Inventory Source Schedules")
model = Schedule
serializer_class = ScheduleSerializer
@ -2091,8 +2092,8 @@ class InventorySourceActivityStreamList(SubListAPIView):
# Sanity check: Does this license allow activity streams?
# If not, forbid this request.
if not feature_enabled('activity_streams'):
raise LicenseForbids('Your license does not allow use of '
'the activity stream.')
raise LicenseForbids(_('Your license does not allow use of '
'the activity stream.'))
# Okay, let it through.
return super(InventorySourceActivityStreamList, self).get(request, *args, **kwargs)
@ -2107,7 +2108,7 @@ class InventorySourceNotificationTemplatesAnyList(SubListCreateAttachDetachAPIVi
def post(self, request, *args, **kwargs):
parent = self.get_parent_object()
if parent.source not in CLOUD_INVENTORY_SOURCES:
return Response(dict(msg="Notification Templates can only be assigned when source is one of {}."
return Response(dict(msg=_("Notification Templates can only be assigned when source is one of {}.")
.format(CLOUD_INVENTORY_SOURCES, parent.source)),
status=status.HTTP_400_BAD_REQUEST)
return super(InventorySourceNotificationTemplatesAnyList, self).post(request, *args, **kwargs)
@ -2297,7 +2298,7 @@ class JobTemplateLaunch(RetrieveAPIView, GenericAPIView):
class JobTemplateSchedulesList(SubListCreateAttachDetachAPIView):
view_name = "Job Template Schedules"
view_name = _("Job Template Schedules")
model = Schedule
serializer_class = ScheduleSerializer
@ -2315,8 +2316,8 @@ class JobTemplateSurveySpec(GenericAPIView):
def get(self, request, *args, **kwargs):
obj = self.get_object()
if not feature_enabled('surveys'):
raise LicenseForbids('Your license does not allow '
'adding surveys.')
raise LicenseForbids(_('Your license does not allow '
'adding surveys.'))
return Response(obj.survey_spec)
def post(self, request, *args, **kwargs):
@ -2325,42 +2326,43 @@ class JobTemplateSurveySpec(GenericAPIView):
# Sanity check: Are surveys available on this license?
# If not, do not allow them to be used.
if not feature_enabled('surveys'):
raise LicenseForbids('Your license does not allow '
'adding surveys.')
raise LicenseForbids(_('Your license does not allow '
'adding surveys.'))
if not request.user.can_access(self.model, 'change', obj, None):
raise PermissionDenied()
try:
obj.survey_spec = json.dumps(request.data)
except ValueError:
return Response(dict(error="Invalid JSON when parsing survey spec."), status=status.HTTP_400_BAD_REQUEST)
return Response(dict(error=_("Invalid JSON when parsing survey spec.")), status=status.HTTP_400_BAD_REQUEST)
if "name" not in obj.survey_spec:
return Response(dict(error="'name' missing from survey spec."), status=status.HTTP_400_BAD_REQUEST)
return Response(dict(error=_("'name' missing from survey spec.")), status=status.HTTP_400_BAD_REQUEST)
if "description" not in obj.survey_spec:
return Response(dict(error="'description' missing from survey spec."), status=status.HTTP_400_BAD_REQUEST)
return Response(dict(error=_("'description' missing from survey spec.")), status=status.HTTP_400_BAD_REQUEST)
if "spec" not in obj.survey_spec:
return Response(dict(error="'spec' missing from survey spec."), status=status.HTTP_400_BAD_REQUEST)
return Response(dict(error=_("'spec' missing from survey spec.")), status=status.HTTP_400_BAD_REQUEST)
if not isinstance(obj.survey_spec["spec"], list):
return Response(dict(error="'spec' must be a list of items."), status=status.HTTP_400_BAD_REQUEST)
return Response(dict(error=_("'spec' must be a list of items.")), status=status.HTTP_400_BAD_REQUEST)
if len(obj.survey_spec["spec"]) < 1:
return Response(dict(error="'spec' doesn't contain any items."), status=status.HTTP_400_BAD_REQUEST)
return Response(dict(error=_("'spec' doesn't contain any items.")), status=status.HTTP_400_BAD_REQUEST)
idx = 0
variable_set = set()
for survey_item in obj.survey_spec["spec"]:
if not isinstance(survey_item, dict):
return Response(dict(error="Survey question %s is not a json object." % str(idx)), status=status.HTTP_400_BAD_REQUEST)
return Response(dict(error=_("Survey question %s is not a json object.") % str(idx)), status=status.HTTP_400_BAD_REQUEST)
if "type" not in survey_item:
return Response(dict(error="'type' missing from survey question %s." % str(idx)), status=status.HTTP_400_BAD_REQUEST)
return Response(dict(error=_("'type' missing from survey question %s.") % str(idx)), status=status.HTTP_400_BAD_REQUEST)
if "question_name" not in survey_item:
return Response(dict(error="'question_name' missing from survey question %s." % str(idx)), status=status.HTTP_400_BAD_REQUEST)
return Response(dict(error=_("'question_name' missing from survey question %s.") % str(idx)), status=status.HTTP_400_BAD_REQUEST)
if "variable" not in survey_item:
return Response(dict(error="'variable' missing from survey question %s." % str(idx)), status=status.HTTP_400_BAD_REQUEST)
return Response(dict(error=_("'variable' missing from survey question %s.") % str(idx)), status=status.HTTP_400_BAD_REQUEST)
if survey_item['variable'] in variable_set:
return Response(dict(error="'variable' '%s' duplicated in survey question %s." % (survey_item['variable'], str(idx))), status=status.HTTP_400_BAD_REQUEST)
return Response(dict(error=_("'variable' '%(item)s' duplicated in survey question %(survey)s.") % {
'item': survey_item['variable'], 'survey': str(idx)}), status=status.HTTP_400_BAD_REQUEST)
else:
variable_set.add(survey_item['variable'])
if "required" not in survey_item:
return Response(dict(error="'required' missing from survey question %s." % str(idx)), status=status.HTTP_400_BAD_REQUEST)
return Response(dict(error=_("'required' missing from survey question %s.") % str(idx)), status=status.HTTP_400_BAD_REQUEST)
idx += 1
obj.save()
return Response()
@ -2385,8 +2387,8 @@ class JobTemplateActivityStreamList(SubListAPIView):
# Sanity check: Does this license allow activity streams?
# If not, forbid this request.
if not feature_enabled('activity_streams'):
raise LicenseForbids('Your license does not allow use of '
'the activity stream.')
raise LicenseForbids(_('Your license does not allow use of '
'the activity stream.'))
# Okay, let it through.
return super(JobTemplateActivityStreamList, self).get(request, *args, **kwargs)
@ -2554,22 +2556,22 @@ class JobTemplateCallback(GenericAPIView):
matching_hosts = self.find_matching_hosts()
# Check matching hosts.
if not matching_hosts:
data = dict(msg='No matching host could be found!')
data = dict(msg=_('No matching host could be found!'))
return Response(data, status=status.HTTP_400_BAD_REQUEST)
elif len(matching_hosts) > 1:
data = dict(msg='Multiple hosts matched the request!')
data = dict(msg=_('Multiple hosts matched the request!'))
return Response(data, status=status.HTTP_400_BAD_REQUEST)
else:
host = list(matching_hosts)[0]
if not job_template.can_start_without_user_input():
data = dict(msg='Cannot start automatically, user input required!')
data = dict(msg=_('Cannot start automatically, user input required!'))
return Response(data, status=status.HTTP_400_BAD_REQUEST)
limit = host.name
# NOTE: We limit this to one job waiting per host per callblack to keep them from stacking crazily
if Job.objects.filter(status__in=['pending', 'waiting', 'running'], job_template=job_template,
limit=limit).count() > 0:
data = dict(msg='Host callback job already pending.')
data = dict(msg=_('Host callback job already pending.'))
return Response(data, status=status.HTTP_400_BAD_REQUEST)
# Everything is fine; actually create the job.
@ -2582,7 +2584,7 @@ class JobTemplateCallback(GenericAPIView):
kv['extra_vars'] = extra_vars
result = job.signal_start(**kv)
if not result:
data = dict(msg='Error starting job!')
data = dict(msg=_('Error starting job!'))
return Response(data, status=status.HTTP_400_BAD_REQUEST)
# Return the location of the new job.
@ -2856,7 +2858,7 @@ class SystemJobTemplateList(ListAPIView):
def get(self, request, *args, **kwargs):
if not request.user.is_superuser and not request.user.is_system_auditor:
raise PermissionDenied("Superuser privileges needed.")
raise PermissionDenied(_("Superuser privileges needed."))
return super(SystemJobTemplateList, self).get(request, *args, **kwargs)
class SystemJobTemplateDetail(RetrieveAPIView):
@ -2883,7 +2885,7 @@ class SystemJobTemplateLaunch(GenericAPIView):
class SystemJobTemplateSchedulesList(SubListCreateAttachDetachAPIView):
view_name = "System Job Template Schedules"
view_name = _("System Job Template Schedules")
model = Schedule
serializer_class = ScheduleSerializer
@ -2966,8 +2968,8 @@ class JobActivityStreamList(SubListAPIView):
# Sanity check: Does this license allow activity streams?
# If not, forbid this request.
if not feature_enabled('activity_streams'):
raise LicenseForbids('Your license does not allow use of '
'the activity stream.')
raise LicenseForbids(_('Your license does not allow use of '
'the activity stream.'))
# Okay, let it through.
return super(JobActivityStreamList, self).get(request, *args, **kwargs)
@ -3060,7 +3062,7 @@ class BaseJobHostSummariesList(SubListAPIView):
serializer_class = JobHostSummarySerializer
parent_model = None # Subclasses must define this attribute.
relationship = 'job_host_summaries'
view_name = 'Job Host Summaries List'
view_name = _('Job Host Summaries List')
class HostJobHostSummariesList(BaseJobHostSummariesList):
@ -3095,7 +3097,7 @@ class JobEventChildrenList(SubListAPIView):
serializer_class = JobEventSerializer
parent_model = JobEvent
relationship = 'children'
view_name = 'Job Event Children List'
view_name = _('Job Event Children List')
class JobEventHostsList(SubListAPIView):
@ -3103,7 +3105,7 @@ class JobEventHostsList(SubListAPIView):
serializer_class = HostSerializer
parent_model = JobEvent
relationship = 'hosts'
view_name = 'Job Event Hosts List'
view_name = _('Job Event Hosts List')
class BaseJobEventsList(SubListAPIView):
@ -3111,7 +3113,7 @@ class BaseJobEventsList(SubListAPIView):
serializer_class = JobEventSerializer
parent_model = None # Subclasses must define this attribute.
relationship = 'job_events'
view_name = 'Job Events List'
view_name = _('Job Events List')
class HostJobEventsList(BaseJobEventsList):
@ -3128,7 +3130,7 @@ class JobJobEventsList(BaseJobEventsList):
class JobJobPlaysList(BaseJobEventsList):
parent_model = Job
view_name = 'Job Plays List'
view_name = _('Job Plays List')
new_in_200 = True
@paginated
@ -3203,7 +3205,7 @@ class JobJobTasksList(BaseJobEventsList):
and their completion status.
"""
parent_model = Job
view_name = 'Job Play Tasks List'
view_name = _('Job Play Tasks List')
new_in_200 = True
@paginated
@ -3218,15 +3220,15 @@ class JobJobTasksList(BaseJobEventsList):
# If there's no event ID specified, this will return a 404.
job = Job.objects.filter(pk=self.kwargs['pk'])
if not job.exists():
return ({'detail': 'Job not found.'}, -1, status.HTTP_404_NOT_FOUND)
return ({'detail': _('Job not found.')}, -1, status.HTTP_404_NOT_FOUND)
job = job[0]
if 'event_id' not in request.query_params:
return ({"detail": "'event_id' not provided."}, -1, status.HTTP_400_BAD_REQUEST)
return ({"detail": _("'event_id' not provided.")}, -1, status.HTTP_400_BAD_REQUEST)
parent_task = job.job_events.filter(pk=int(request.query_params.get('event_id', -1)))
if not parent_task.exists():
return ({'detail': 'Parent event not found.'}, -1, status.HTTP_404_NOT_FOUND)
return ({'detail': _('Parent event not found.')}, -1, status.HTTP_404_NOT_FOUND)
parent_task = parent_task[0]
STARTING_EVENTS = ('playbook_on_task_start', 'playbook_on_setup')
@ -3498,7 +3500,7 @@ class BaseAdHocCommandEventsList(SubListAPIView):
serializer_class = AdHocCommandEventSerializer
parent_model = None # Subclasses must define this attribute.
relationship = 'ad_hoc_command_events'
view_name = 'Ad Hoc Command Events List'
view_name = _('Ad Hoc Command Events List')
new_in_220 = True
@ -3529,8 +3531,8 @@ class AdHocCommandActivityStreamList(SubListAPIView):
# Sanity check: Does this license allow activity streams?
# If not, forbid this request.
if not feature_enabled('activity_streams'):
raise LicenseForbids('Your license does not allow use of '
'the activity stream.')
raise LicenseForbids(_('Your license does not allow use of '
'the activity stream.'))
# Okay, let it through.
return super(AdHocCommandActivityStreamList, self).get(request, *args, **kwargs)
@ -3551,7 +3553,7 @@ class SystemJobList(ListCreateAPIView):
def get(self, request, *args, **kwargs):
if not request.user.is_superuser and not request.user.is_system_auditor:
raise PermissionDenied("Superuser privileges needed.")
raise PermissionDenied(_("Superuser privileges needed."))
return super(SystemJobList, self).get(request, *args, **kwargs)
@ -3607,8 +3609,9 @@ class UnifiedJobStdout(RetrieveAPIView):
unified_job = self.get_object()
obj_size = unified_job.result_stdout_size
if request.accepted_renderer.format != 'txt_download' and obj_size > settings.STDOUT_MAX_BYTES_DISPLAY:
response_message = "Standard Output too large to display (%d bytes), only download supported for sizes over %d bytes" % (obj_size,
settings.STDOUT_MAX_BYTES_DISPLAY)
response_message = _("Standard Output too large to display (%(text_size)d bytes), "
"only download supported for sizes over %(supported_size)d bytes") % {
'text_size': obj_size, 'supported_size': settings.STDOUT_MAX_BYTES_DISPLAY}
if request.accepted_renderer.format == 'json':
return Response({'range': {'start': 0, 'end': 1, 'absolute_end': 1}, 'content': response_message})
else:
@ -3655,7 +3658,7 @@ class UnifiedJobStdout(RetrieveAPIView):
response["Content-Disposition"] = 'attachment; filename="job_%s.txt"' % str(unified_job.id)
return response
except Exception as e:
return Response({"error": "Error generating stdout download file: %s" % str(e)}, status=status.HTTP_400_BAD_REQUEST)
return Response({"error": _("Error generating stdout download file: %s") % str(e)}, status=status.HTTP_400_BAD_REQUEST)
elif request.accepted_renderer.format == 'txt':
return Response(unified_job.result_stdout)
else:
@ -3695,13 +3698,13 @@ class NotificationTemplateDetail(RetrieveUpdateDestroyAPIView):
if not request.user.can_access(self.model, 'delete', obj):
return Response(status=status.HTTP_404_NOT_FOUND)
if obj.notifications.filter(status='pending').exists():
return Response({"error": "Delete not allowed while there are pending notifications"},
return Response({"error": _("Delete not allowed while there are pending notifications")},
status=status.HTTP_405_METHOD_NOT_ALLOWED)
return super(NotificationTemplateDetail, self).delete(request, *args, **kwargs)
class NotificationTemplateTest(GenericAPIView):
view_name = 'NotificationTemplate Test'
view_name = _('NotificationTemplate Test')
model = NotificationTemplate
serializer_class = EmptySerializer
new_in_300 = True
@ -3762,8 +3765,8 @@ class ActivityStreamList(SimpleListAPIView):
# Sanity check: Does this license allow activity streams?
# If not, forbid this request.
if not feature_enabled('activity_streams'):
raise LicenseForbids('Your license does not allow use of '
'the activity stream.')
raise LicenseForbids(_('Your license does not allow use of '
'the activity stream.'))
# Okay, let it through.
return super(ActivityStreamList, self).get(request, *args, **kwargs)
@ -3779,8 +3782,8 @@ class ActivityStreamDetail(RetrieveAPIView):
# Sanity check: Does this license allow activity streams?
# If not, forbid this request.
if not feature_enabled('activity_streams'):
raise LicenseForbids('Your license does not allow use of '
'the activity stream.')
raise LicenseForbids(_('Your license does not allow use of '
'the activity stream.'))
# Okay, let it through.
return super(ActivityStreamDetail, self).get(request, *args, **kwargs)
@ -3830,26 +3833,26 @@ class RoleUsersList(SubListCreateAttachDetachAPIView):
# Forbid implicit user creation here
sub_id = request.data.get('id', None)
if not sub_id:
data = dict(msg="User 'id' field is missing.")
data = dict(msg=_("User 'id' field is missing."))
return Response(data, status=status.HTTP_400_BAD_REQUEST)
user = get_object_or_400(User, pk=sub_id)
role = self.get_parent_object()
if role == self.request.user.admin_role:
raise PermissionDenied('You may not perform any action with your own admin_role.')
raise PermissionDenied(_('You may not perform any action with your own admin_role.'))
user_content_type = ContentType.objects.get_for_model(User)
if role.content_type == user_content_type:
raise PermissionDenied('You may not change the membership of a users admin_role')
raise PermissionDenied(_('You may not change the membership of a users admin_role'))
credential_content_type = ContentType.objects.get_for_model(Credential)
if role.content_type == credential_content_type:
if role.content_object.organization and user not in role.content_object.organization.member_role:
data = dict(msg="You cannot grant credential access to a user not in the credentials' organization")
data = dict(msg=_("You cannot grant credential access to a user not in the credentials' organization"))
return Response(data, status=status.HTTP_400_BAD_REQUEST)
if not role.content_object.organization and not request.user.is_superuser:
data = dict(msg="You cannot grant private credential access to another user")
data = dict(msg=_("You cannot grant private credential access to another user"))
return Response(data, status=status.HTTP_400_BAD_REQUEST)
return super(RoleUsersList, self).post(request, *args, **kwargs)
@ -3873,7 +3876,7 @@ class RoleTeamsList(SubListAPIView):
# Forbid implicit team creation here
sub_id = request.data.get('id', None)
if not sub_id:
data = dict(msg="Team 'id' field is missing.")
data = dict(msg=_("Team 'id' field is missing."))
return Response(data, status=status.HTTP_400_BAD_REQUEST)
team = get_object_or_400(Team, pk=sub_id)
@ -3881,13 +3884,13 @@ class RoleTeamsList(SubListAPIView):
organization_content_type = ContentType.objects.get_for_model(Organization)
if role.content_type == organization_content_type:
data = dict(msg="You cannot assign an Organization role as a child role for a Team.")
data = dict(msg=_("You cannot assign an Organization role as a child role for a Team."))
return Response(data, status=status.HTTP_400_BAD_REQUEST)
credential_content_type = ContentType.objects.get_for_model(Credential)
if role.content_type == credential_content_type:
if not role.content_object.organization or role.content_object.organization.id != team.organization.id:
data = dict(msg="You cannot grant credential access to a team when the Organization field isn't set, or belongs to a different organization")
data = dict(msg=_("You cannot grant credential access to a team when the Organization field isn't set, or belongs to a different organization"))
return Response(data, status=status.HTTP_400_BAD_REQUEST)
action = 'attach'

View File

@ -14,6 +14,7 @@ from django.core.management.base import BaseCommand, CommandError
from django.db import transaction
from django.utils.text import slugify
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
# Tower
from awx import MODE
@ -36,27 +37,27 @@ class Command(BaseCommand):
action='store_true',
dest='dry_run',
default=False,
help='Only show which settings would be commented/migrated.',
help=_('Only show which settings would be commented/migrated.'),
)
parser.add_argument(
'--skip-errors',
action='store_true',
dest='skip_errors',
default=False,
help='Skip over settings that would raise an error when commenting/migrating.',
help=_('Skip over settings that would raise an error when commenting/migrating.'),
)
parser.add_argument(
'--no-comment',
action='store_true',
dest='no_comment',
default=False,
help='Skip commenting out settings in files.',
help=_('Skip commenting out settings in files.'),
)
parser.add_argument(
'--backup-suffix',
dest='backup_suffix',
default=now().strftime('.%Y%m%d%H%M%S'),
help='Backup existing settings files with this suffix.',
help=_('Backup existing settings files with this suffix.'),
)
@transaction.atomic

View File

@ -11,6 +11,7 @@ from django.conf import settings
from django.db.models import Q
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import ugettext_lazy as _
# Django REST Framework
from rest_framework.exceptions import ParseError, PermissionDenied, ValidationError
@ -199,24 +200,24 @@ class BaseAccess(object):
validation_info['grace_period_remaining'] = 99999999
if check_expiration and validation_info.get('time_remaining', None) is None:
raise PermissionDenied("License is missing.")
raise PermissionDenied(_("License is missing."))
if check_expiration and validation_info.get("grace_period_remaining") <= 0:
raise PermissionDenied("License has expired.")
raise PermissionDenied(_("License has expired."))
free_instances = validation_info.get('free_instances', 0)
available_instances = validation_info.get('available_instances', 0)
if add_host and free_instances == 0:
raise PermissionDenied("License count of %s instances has been reached." % available_instances)
raise PermissionDenied(_("License count of %s instances has been reached.") % available_instances)
elif add_host and free_instances < 0:
raise PermissionDenied("License count of %s instances has been exceeded." % available_instances)
raise PermissionDenied(_("License count of %s instances has been exceeded.") % available_instances)
elif not add_host and free_instances < 0:
raise PermissionDenied("Host count exceeds available instances.")
raise PermissionDenied(_("Host count exceeds available instances."))
if feature is not None:
if "features" in validation_info and not validation_info["features"].get(feature, False):
raise LicenseForbids("Feature %s is not enabled in the active license." % feature)
raise LicenseForbids(_("Feature %s is not enabled in the active license.") % feature)
elif "features" not in validation_info:
raise LicenseForbids("Features not found in active license.")
raise LicenseForbids(_("Features not found in active license."))
def get_user_capabilities(self, obj, method_list=[], parent_obj=None):
if obj is None:
@ -416,7 +417,7 @@ class OrganizationAccess(BaseAccess):
active_jobs.extend([dict(type="inventory_update", id=o.id)
for o in InventoryUpdate.objects.filter(inventory_source__inventory__organization=obj, status__in=ACTIVE_STATES)])
if len(active_jobs) > 0:
raise StateConflict({"conflict": "Resource is being used by running jobs",
raise StateConflict({"conflict": _("Resource is being used by running jobs"),
"active_jobs": active_jobs})
return True
@ -490,7 +491,7 @@ class InventoryAccess(BaseAccess):
active_jobs.extend([dict(type="inventory_update", id=o.id)
for o in InventoryUpdate.objects.filter(inventory_source__inventory=obj, status__in=ACTIVE_STATES)])
if len(active_jobs) > 0:
raise StateConflict({"conflict": "Resource is being used by running jobs",
raise StateConflict({"conflict": _("Resource is being used by running jobs"),
"active_jobs": active_jobs})
return True
@ -535,7 +536,7 @@ class HostAccess(BaseAccess):
# Prevent moving a host to a different inventory.
inventory_pk = get_pk_from_dict(data, 'inventory')
if obj and inventory_pk and obj.inventory.pk != inventory_pk:
raise PermissionDenied('Unable to change inventory on a host.')
raise PermissionDenied(_('Unable to change inventory on a host.'))
# Checks for admin or change permission on inventory, controls whether
# the user can edit variable data.
return obj and self.user in obj.inventory.admin_role
@ -547,7 +548,7 @@ class HostAccess(BaseAccess):
return False
# Prevent assignments between different inventories.
if obj.inventory != sub_obj.inventory:
raise ParseError('Cannot associate two items from different inventories.')
raise ParseError(_('Cannot associate two items from different inventories.'))
return True
def can_delete(self, obj):
@ -581,7 +582,7 @@ class GroupAccess(BaseAccess):
# Prevent moving a group to a different inventory.
inventory_pk = get_pk_from_dict(data, 'inventory')
if obj and inventory_pk and obj.inventory.pk != inventory_pk:
raise PermissionDenied('Unable to change inventory on a group.')
raise PermissionDenied(_('Unable to change inventory on a group.'))
# Checks for admin or change permission on inventory, controls whether
# the user can attach subgroups or edit variable data.
return obj and self.user in obj.inventory.admin_role
@ -593,7 +594,7 @@ class GroupAccess(BaseAccess):
return False
# Prevent assignments between different inventories.
if obj.inventory != sub_obj.inventory:
raise ParseError('Cannot associate two items from different inventories.')
raise ParseError(_('Cannot associate two items from different inventories.'))
# Prevent group from being assigned as its own (grand)child.
if type(obj) == type(sub_obj):
parent_pks = set(obj.all_parents.values_list('pk', flat=True))
@ -612,7 +613,7 @@ class GroupAccess(BaseAccess):
active_jobs.extend([dict(type="inventory_update", id=o.id)
for o in InventoryUpdate.objects.filter(inventory_source__in=obj.inventory_sources.all(), status__in=ACTIVE_STATES)])
if len(active_jobs) > 0:
raise StateConflict({"conflict": "Resource is being used by running jobs",
raise StateConflict({"conflict": _("Resource is being used by running jobs"),
"active_jobs": active_jobs})
return True
@ -804,7 +805,7 @@ class TeamAccess(BaseAccess):
# Prevent moving a team to a different organization.
org_pk = get_pk_from_dict(data, 'organization')
if obj and org_pk and obj.organization.pk != org_pk:
raise PermissionDenied('Unable to change organization on a team.')
raise PermissionDenied(_('Unable to change organization on a team.'))
if self.user.is_superuser:
return True
return self.user in obj.admin_role
@ -817,9 +818,9 @@ class TeamAccess(BaseAccess):
of a resource role to the team."""
if isinstance(sub_obj, Role):
if sub_obj.content_object is None:
raise PermissionDenied("The {} role cannot be assigned to a team".format(sub_obj.name))
raise PermissionDenied(_("The {} role cannot be assigned to a team").format(sub_obj.name))
elif isinstance(sub_obj.content_object, User):
raise PermissionDenied("The admin_role for a User cannot be assigned to a team")
raise PermissionDenied(_("The admin_role for a User cannot be assigned to a team"))
if isinstance(sub_obj.content_object, ResourceMixin):
role_access = RoleAccess(self.user)
@ -888,7 +889,7 @@ class ProjectAccess(BaseAccess):
active_jobs.extend([dict(type="project_update", id=o.id)
for o in ProjectUpdate.objects.filter(project=obj, status__in=ACTIVE_STATES)])
if len(active_jobs) > 0:
raise StateConflict({"conflict": "Resource is being used by running jobs",
raise StateConflict({"conflict": _("Resource is being used by running jobs"),
"active_jobs": active_jobs})
return True
@ -1130,7 +1131,7 @@ class JobTemplateAccess(BaseAccess):
active_jobs = [dict(type="job", id=o.id)
for o in obj.jobs.filter(status__in=ACTIVE_STATES)]
if len(active_jobs) > 0:
raise StateConflict({"conflict": "Resource is being used by running jobs",
raise StateConflict({"conflict": _("Resource is being used by running jobs"),
"active_jobs": active_jobs})
return True
@ -1542,7 +1543,7 @@ class WorkflowJobTemplateAccess(BaseAccess):
active_jobs = [dict(type="job", id=o.id)
for o in obj.jobs.filter(status__in=ACTIVE_STATES)]
if len(active_jobs) > 0:
raise StateConflict({"conflict": "Resource is being used by running jobs",
raise StateConflict({"conflict": _("Resource is being used by running jobs"),
"active_jobs": active_jobs})
return True

View File

@ -95,14 +95,14 @@ class AdHocCommand(UnifiedJob, JobNotificationMixin):
def clean_inventory(self):
inv = self.inventory
if not inv:
raise ValidationError('No valid inventory.')
raise ValidationError(_('No valid inventory.'))
return inv
def clean_credential(self):
cred = self.credential
if cred and cred.kind != 'ssh':
raise ValidationError(
'You must provide a machine / SSH credential.',
_('You must provide a machine / SSH credential.'),
)
return cred
@ -113,18 +113,18 @@ class AdHocCommand(UnifiedJob, JobNotificationMixin):
def clean_module_name(self):
if type(self.module_name) not in (str, unicode):
raise ValidationError("Invalid type for ad hoc command")
raise ValidationError(_("Invalid type for ad hoc command"))
module_name = self.module_name.strip() or 'command'
if module_name not in settings.AD_HOC_COMMANDS:
raise ValidationError('Unsupported module for ad hoc commands.')
raise ValidationError(_('Unsupported module for ad hoc commands.'))
return module_name
def clean_module_args(self):
if type(self.module_args) not in (str, unicode):
raise ValidationError("Invalid type for ad hoc command")
raise ValidationError(_("Invalid type for ad hoc command"))
module_args = self.module_args
if self.module_name in ('command', 'shell') and not module_args:
raise ValidationError('No argument passed to %s module.' % self.module_name)
raise ValidationError(_('No argument passed to %s module.') % self.module_name)
return module_args
@property

View File

@ -278,9 +278,9 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin):
"""
host = self.host or ''
if not host and self.kind == 'vmware':
raise ValidationError('Host required for VMware credential.')
raise ValidationError(_('Host required for VMware credential.'))
if not host and self.kind == 'openstack':
raise ValidationError('Host required for OpenStack credential.')
raise ValidationError(_('Host required for OpenStack credential.'))
return host
def clean_domain(self):
@ -289,32 +289,32 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin):
def clean_username(self):
username = self.username or ''
if not username and self.kind == 'aws':
raise ValidationError('Access key required for AWS credential.')
raise ValidationError(_('Access key required for AWS credential.'))
if not username and self.kind == 'rax':
raise ValidationError('Username required for Rackspace '
'credential.')
raise ValidationError(_('Username required for Rackspace '
'credential.'))
if not username and self.kind == 'vmware':
raise ValidationError('Username required for VMware credential.')
raise ValidationError(_('Username required for VMware credential.'))
if not username and self.kind == 'openstack':
raise ValidationError('Username required for OpenStack credential.')
raise ValidationError(_('Username required for OpenStack credential.'))
return username
def clean_password(self):
password = self.password or ''
if not password and self.kind == 'aws':
raise ValidationError('Secret key required for AWS credential.')
raise ValidationError(_('Secret key required for AWS credential.'))
if not password and self.kind == 'rax':
raise ValidationError('API key required for Rackspace credential.')
raise ValidationError(_('API key required for Rackspace credential.'))
if not password and self.kind == 'vmware':
raise ValidationError('Password required for VMware credential.')
raise ValidationError(_('Password required for VMware credential.'))
if not password and self.kind == 'openstack':
raise ValidationError('Password or API key required for OpenStack credential.')
raise ValidationError(_('Password or API key required for OpenStack credential.'))
return password
def clean_project(self):
project = self.project or ''
if self.kind == 'openstack' and not project:
raise ValidationError('Project name required for OpenStack credential.')
raise ValidationError(_('Project name required for OpenStack credential.'))
return project
def clean_ssh_key_data(self):
@ -341,13 +341,13 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin):
def clean_ssh_key_unlock(self):
if self.has_encrypted_ssh_key_data and not self.ssh_key_unlock:
raise ValidationError('SSH key unlock must be set when SSH key '
'is encrypted.')
raise ValidationError(_('SSH key unlock must be set when SSH key '
'is encrypted.'))
return self.ssh_key_unlock
def clean(self):
if self.deprecated_user and self.deprecated_team:
raise ValidationError('Credential cannot be assigned to both a user and team.')
raise ValidationError(_('Credential cannot be assigned to both a user and team.'))
def _password_field_allows_ask(self, field):
return bool(self.kind == 'ssh' and field != 'ssh_key_data')

View File

@ -889,16 +889,16 @@ class InventorySourceOptions(BaseModel):
@classmethod
def get_ec2_group_by_choices(cls):
return [
('availability_zone', 'Availability Zone'),
('ami_id', 'Image ID'),
('instance_id', 'Instance ID'),
('instance_type', 'Instance Type'),
('key_pair', 'Key Name'),
('region', 'Region'),
('security_group', 'Security Group'),
('tag_keys', 'Tags'),
('vpc_id', 'VPC ID'),
('tag_none', 'Tag None'),
('availability_zone', _('Availability Zone')),
('ami_id', _('Image ID')),
('instance_id', _('Instance ID')),
('instance_type', _('Instance Type')),
('key_pair', _('Key Name')),
('region', _('Region')),
('security_group', _('Security Group')),
('tag_keys', _('Tags')),
('vpc_id', _('VPC ID')),
('tag_none', _('Tag None')),
]
@classmethod
@ -969,14 +969,14 @@ class InventorySourceOptions(BaseModel):
# credentials; Rackspace requires Rackspace credentials; etc...)
if self.source.replace('ec2', 'aws') != cred.kind:
raise ValidationError(
'Cloud-based inventory sources (such as %s) require '
'credentials for the matching cloud service.' % self.source
_('Cloud-based inventory sources (such as %s) require '
'credentials for the matching cloud service.') % self.source
)
# Allow an EC2 source to omit the credential. If Tower is running on
# an EC2 instance with an IAM Role assigned, boto will use credentials
# from the instance metadata instead of those explicitly provided.
elif self.source in CLOUD_PROVIDERS and self.source != 'ec2':
raise ValidationError('Credential is required for a cloud source.')
raise ValidationError(_('Credential is required for a cloud source.'))
return cred
def clean_source_regions(self):
@ -1001,9 +1001,9 @@ class InventorySourceOptions(BaseModel):
if r not in valid_regions and r not in invalid_regions:
invalid_regions.append(r)
if invalid_regions:
raise ValidationError('Invalid %s region%s: %s' % (self.source,
'' if len(invalid_regions) == 1 else 's',
', '.join(invalid_regions)))
raise ValidationError(_('Invalid %(source)s region%(plural)s: %(region)s') % {
'source': self.source, 'plural': '' if len(invalid_regions) == 1 else 's',
'region': ', '.join(invalid_regions)})
return ','.join(regions)
source_vars_dict = VarsDictProperty('source_vars')
@ -1027,9 +1027,9 @@ class InventorySourceOptions(BaseModel):
if instance_filter_name not in self.INSTANCE_FILTER_NAMES:
invalid_filters.append(instance_filter)
if invalid_filters:
raise ValidationError('Invalid filter expression%s: %s' %
('' if len(invalid_filters) == 1 else 's',
', '.join(invalid_filters)))
raise ValidationError(_('Invalid filter expression%(plural)s: %(filter)s') %
{'plural': '' if len(invalid_filters) == 1 else 's',
'filter': ', '.join(invalid_filters)})
return instance_filters
def clean_group_by(self):
@ -1046,9 +1046,9 @@ class InventorySourceOptions(BaseModel):
if c not in valid_choices and c not in invalid_choices:
invalid_choices.append(c)
if invalid_choices:
raise ValidationError('Invalid group by choice%s: %s' %
('' if len(invalid_choices) == 1 else 's',
', '.join(invalid_choices)))
raise ValidationError(_('Invalid group by choice%(plural)s: %(choice)s') %
{'plural': '' if len(invalid_choices) == 1 else 's',
'choice': ', '.join(invalid_choices)})
return ','.join(choices)
@ -1194,7 +1194,7 @@ class InventorySource(UnifiedJobTemplate, InventorySourceOptions):
existing_sources = qs.exclude(pk=self.pk)
if existing_sources.count():
s = u', '.join([x.group.name for x in existing_sources])
raise ValidationError('Unable to configure this item for cloud sync. It is already managed by %s.' % s)
raise ValidationError(_('Unable to configure this item for cloud sync. It is already managed by %s.') % s)
return source

View File

@ -159,7 +159,7 @@ class JobOptions(BaseModel):
cred = self.credential
if cred and cred.kind != 'ssh':
raise ValidationError(
'You must provide a machine / SSH credential.',
_('You must provide a machine / SSH credential.'),
)
return cred
@ -167,7 +167,7 @@ class JobOptions(BaseModel):
cred = self.network_credential
if cred and cred.kind != 'net':
raise ValidationError(
'You must provide a network credential.',
_('You must provide a network credential.'),
)
return cred
@ -175,8 +175,8 @@ class JobOptions(BaseModel):
cred = self.cloud_credential
if cred and cred.kind not in CLOUD_PROVIDERS + ('aws',):
raise ValidationError(
'Must provide a credential for a cloud provider, such as '
'Amazon Web Services or Rackspace.',
_('Must provide a credential for a cloud provider, such as '
'Amazon Web Services or Rackspace.'),
)
return cred
@ -275,19 +275,19 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, ResourceMixin):
if self.inventory is None:
resources_needed_to_start.append('inventory')
if not self.ask_inventory_on_launch:
validation_errors['inventory'] = ["Job Template must provide 'inventory' or allow prompting for it.",]
validation_errors['inventory'] = [_("Job Template must provide 'inventory' or allow prompting for it."),]
if self.credential is None:
resources_needed_to_start.append('credential')
if not self.ask_credential_on_launch:
validation_errors['credential'] = ["Job Template must provide 'credential' or allow prompting for it.",]
validation_errors['credential'] = [_("Job Template must provide 'credential' or allow prompting for it."),]
# Job type dependent checks
if self.job_type == PERM_INVENTORY_SCAN:
if self.inventory is None or self.ask_inventory_on_launch:
validation_errors['inventory'] = ["Scan jobs must be assigned a fixed inventory.",]
validation_errors['inventory'] = [_("Scan jobs must be assigned a fixed inventory."),]
elif self.project is None:
resources_needed_to_start.append('project')
validation_errors['project'] = ["Job types 'run' and 'check' must have assigned a project.",]
validation_errors['project'] = [_("Job types 'run' and 'check' must have assigned a project."),]
return (validation_errors, resources_needed_to_start)
@ -496,10 +496,10 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, ResourceMixin):
if 'job_type' in data and self.ask_job_type_on_launch:
if ((self.job_type == PERM_INVENTORY_SCAN and not data['job_type'] == PERM_INVENTORY_SCAN) or
(data['job_type'] == PERM_INVENTORY_SCAN and not self.job_type == PERM_INVENTORY_SCAN)):
errors['job_type'] = 'Can not override job_type to or from a scan job.'
errors['job_type'] = _('Can not override job_type to or from a scan job.')
if (self.job_type == PERM_INVENTORY_SCAN and ('inventory' in data) and self.ask_inventory_on_launch and
self.inventory != data['inventory']):
errors['inventory'] = 'Inventory can not be changed at runtime for scan jobs.'
errors['inventory'] = _('Inventory can not be changed at runtime for scan jobs.')
return errors
@property

View File

@ -267,7 +267,7 @@ class AuthToken(BaseModel):
def invalidate(self, reason='timeout_reached', save=True):
if not AuthToken.reason_long(reason):
raise ValueError('Invalid reason specified')
raise ValueError(_('Invalid reason specified'))
self.reason = reason
if save:
self.save()

View File

@ -124,10 +124,10 @@ class ProjectOptions(models.Model):
scm_url = update_scm_url(self.scm_type, scm_url,
check_special_cases=False)
except ValueError as e:
raise ValidationError((e.args or ('Invalid SCM URL.',))[0])
raise ValidationError((e.args or (_('Invalid SCM URL.'),))[0])
scm_url_parts = urlparse.urlsplit(scm_url)
if self.scm_type and not any(scm_url_parts):
raise ValidationError('SCM URL is required.')
raise ValidationError(_('SCM URL is required.'))
return unicode(self.scm_url or '')
def clean_credential(self):
@ -136,7 +136,7 @@ class ProjectOptions(models.Model):
cred = self.credential
if cred:
if cred.kind != 'scm':
raise ValidationError("Credential kind must be 'scm'.")
raise ValidationError(_("Credential kind must be 'scm'."))
try:
scm_url = update_scm_url(self.scm_type, self.scm_url,
check_special_cases=False)
@ -151,7 +151,7 @@ class ProjectOptions(models.Model):
update_scm_url(self.scm_type, self.scm_url, scm_username,
scm_password)
except ValueError as e:
raise ValidationError((e.args or ('Invalid credential.',))[0])
raise ValidationError((e.args or (_('Invalid credential.'),))[0])
except ValueError:
pass
return cred

View File

@ -62,12 +62,12 @@ class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique, Notificatio
]
COMMON_STATUS_CHOICES = JOB_STATUS_CHOICES + [
('never updated', 'Never Updated'), # A job has never been run using this template.
('never updated', _('Never Updated')), # A job has never been run using this template.
]
PROJECT_STATUS_CHOICES = COMMON_STATUS_CHOICES + [
('ok', 'OK'), # Project is not configured for SCM and path exists.
('missing', 'Missing'), # Project path does not exist.
('ok', _('OK')), # Project is not configured for SCM and path exists.
('missing', _('Missing')), # Project path does not exist.
]
INVENTORY_SOURCE_STATUS_CHOICES = COMMON_STATUS_CHOICES + [

View File

@ -5,6 +5,8 @@ import json
from django.utils.encoding import smart_text
from django.core.mail.backends.base import BaseEmailBackend
from django.utils.translation import ugettext_lazy as _
class TowerBaseEmailBackend(BaseEmailBackend):
@ -12,9 +14,8 @@ class TowerBaseEmailBackend(BaseEmailBackend):
if "body" in body:
body_actual = body['body']
else:
body_actual = smart_text("{} #{} had status {} on Ansible Tower, view details at {}\n\n".format(body['friendly_name'],
body['id'],
body['status'],
body['url']))
body_actual = smart_text(_("{} #{} had status {} on Ansible Tower, view details at {}\n\n").format(
body['friendly_name'], body['id'], body['status'], body['url'])
)
body_actual += json.dumps(body, indent=4)
return body_actual

View File

@ -5,6 +5,8 @@ import json
from django.utils.encoding import smart_text
from django.core.mail.backends.smtp import EmailBackend
from django.utils.translation import ugettext_lazy as _
class CustomEmailBackend(EmailBackend):
@ -23,9 +25,8 @@ class CustomEmailBackend(EmailBackend):
if "body" in body:
body_actual = body['body']
else:
body_actual = smart_text("{} #{} had status {} on Ansible Tower, view details at {}\n\n".format(body['friendly_name'],
body['id'],
body['status'],
body['url']))
body_actual = smart_text(_("{} #{} had status {} on Ansible Tower, view details at {}\n\n").format(
body['friendly_name'], body['id'], body['status'], body['url'])
)
body_actual += json.dumps(body, indent=4)
return body_actual

View File

@ -6,11 +6,12 @@ import logging
import requests
from django.utils.encoding import smart_text
from django.utils.translation import ugettext_lazy as _
from awx.main.notifications.base import TowerBaseEmailBackend
logger = logging.getLogger('awx.main.notifications.hipchat_backend')
class HipChatBackend(TowerBaseEmailBackend):
init_parameters = {"token": {"label": "Token", "type": "password"},
@ -42,8 +43,8 @@ class HipChatBackend(TowerBaseEmailBackend):
"from": m.from_email,
"message_format": "text"})
if r.status_code != 204:
logger.error(smart_text("Error sending messages: {}".format(r.text)))
logger.error(smart_text(_("Error sending messages: {}").format(r.text)))
if not self.fail_silently:
raise Exception(smart_text("Error sending message to hipchat: {}".format(r.text)))
raise Exception(smart_text(_("Error sending message to hipchat: {}").format(r.text)))
sent_messages += 1
return sent_messages

View File

@ -8,11 +8,12 @@ import logging
import irc.client
from django.utils.encoding import smart_text
from django.utils.translation import ugettext_lazy as _
from awx.main.notifications.base import TowerBaseEmailBackend
logger = logging.getLogger('awx.main.notifications.irc_backend')
class IrcBackend(TowerBaseEmailBackend):
init_parameters = {"server": {"label": "IRC Server Address", "type": "string"},
@ -50,7 +51,7 @@ class IrcBackend(TowerBaseEmailBackend):
connect_factory=connection_factory,
)
except irc.client.ServerConnectionError as e:
logger.error(smart_text("Exception connecting to irc server: {}".format(e)))
logger.error(smart_text(_("Exception connecting to irc server: {}").format(e)))
if not self.fail_silently:
raise
return True

View File

@ -5,11 +5,12 @@ import logging
import pygerduty
from django.utils.encoding import smart_text
from django.utils.translation import ugettext_lazy as _
from awx.main.notifications.base import TowerBaseEmailBackend
logger = logging.getLogger('awx.main.notifications.pagerduty_backend')
class PagerDutyBackend(TowerBaseEmailBackend):
init_parameters = {"subdomain": {"label": "Pagerduty subdomain", "type": "string"},
@ -35,7 +36,7 @@ class PagerDutyBackend(TowerBaseEmailBackend):
except Exception as e:
if not self.fail_silently:
raise
logger.error(smart_text("Exception connecting to PagerDuty: {}".format(e)))
logger.error(smart_text(_("Exception connecting to PagerDuty: {}").format(e)))
for m in messages:
try:
pager.trigger_incident(m.recipients()[0],
@ -44,7 +45,7 @@ class PagerDutyBackend(TowerBaseEmailBackend):
client=m.from_email)
sent_messages += 1
except Exception as e:
logger.error(smart_text("Exception sending messages: {}".format(e)))
logger.error(smart_text(_("Exception sending messages: {}").format(e)))
if not self.fail_silently:
raise
return sent_messages

View File

@ -5,11 +5,12 @@ import logging
from slackclient import SlackClient
from django.utils.encoding import smart_text
from django.utils.translation import ugettext_lazy as _
from awx.main.notifications.base import TowerBaseEmailBackend
logger = logging.getLogger('awx.main.notifications.slack_backend')
class SlackBackend(TowerBaseEmailBackend):
init_parameters = {"token": {"label": "Token", "type": "password"},
@ -48,7 +49,7 @@ class SlackBackend(TowerBaseEmailBackend):
self.connection.rtm_send_message(r, m.subject)
sent_messages += 1
except Exception as e:
logger.error(smart_text("Exception sending messages: {}".format(e)))
logger.error(smart_text(_("Exception sending messages: {}").format(e)))
if not self.fail_silently:
raise
return sent_messages

View File

@ -6,11 +6,12 @@ import logging
from twilio.rest import TwilioRestClient
from django.utils.encoding import smart_text
from django.utils.translation import ugettext_lazy as _
from awx.main.notifications.base import TowerBaseEmailBackend
logger = logging.getLogger('awx.main.notifications.twilio_backend')
class TwilioBackend(TowerBaseEmailBackend):
init_parameters = {"account_sid": {"label": "Account SID", "type": "string"},
@ -32,7 +33,7 @@ class TwilioBackend(TowerBaseEmailBackend):
except Exception as e:
if not self.fail_silently:
raise
logger.error(smart_text("Exception connecting to Twilio: {}".format(e)))
logger.error(smart_text(_("Exception connecting to Twilio: {}").format(e)))
for m in messages:
try:
@ -42,7 +43,7 @@ class TwilioBackend(TowerBaseEmailBackend):
body=m.subject)
sent_messages += 1
except Exception as e:
logger.error(smart_text("Exception sending messages: {}".format(e)))
logger.error(smart_text(_("Exception sending messages: {}").format(e)))
if not self.fail_silently:
raise
return sent_messages

View File

@ -5,12 +5,13 @@ import logging
import requests
from django.utils.encoding import smart_text
from django.utils.translation import ugettext_lazy as _
from awx.main.notifications.base import TowerBaseEmailBackend
from awx.main.utils import get_awx_version
logger = logging.getLogger('awx.main.notifications.webhook_backend')
class WebhookBackend(TowerBaseEmailBackend):
init_parameters = {"url": {"label": "Target URL", "type": "string"},
@ -34,8 +35,8 @@ class WebhookBackend(TowerBaseEmailBackend):
json=m.body,
headers=self.headers)
if r.status_code >= 400:
logger.error(smart_text("Error sending notification webhook: {}".format(r.text)))
logger.error(smart_text(_("Error sending notification webhook: {}").format(r.text)))
if not self.fail_silently:
raise Exception(smart_text("Error sending notification webhook: {}".format(r.text)))
raise Exception(smart_text(_("Error sending notification webhook: {}").format(r.text)))
sent_messages += 1
return sent_messages

View File

@ -41,6 +41,7 @@ from django.utils.timezone import now
from django.utils.encoding import smart_str
from django.core.mail import send_mail
from django.contrib.auth.models import User
from django.utils.translation import ugettext_lazy as _
# AWX
from awx.main.constants import CLOUD_PROVIDERS
@ -112,12 +113,12 @@ def run_administrative_checks(self):
tower_admin_emails = User.objects.filter(is_superuser=True).values_list('email', flat=True)
if (used_percentage * 100) > 90:
send_mail("Ansible Tower host usage over 90%",
"Ansible Tower host usage over 90%",
_("Ansible Tower host usage over 90%"),
tower_admin_emails,
fail_silently=True)
if validation_info.get('date_warning', False):
send_mail("Ansible Tower license will expire soon",
"Ansible Tower license will expire soon",
_("Ansible Tower license will expire soon"),
tower_admin_emails,
fail_silently=True)
@ -165,7 +166,7 @@ def tower_periodic_scheduler(self):
def _send_notification_templates(instance, status_str):
if status_str not in ['succeeded', 'failed']:
raise ValueError("status_str must be either succeeded or failed")
raise ValueError(_("status_str must be either succeeded or failed"))
notification_templates = instance.get_notification_templates()
if notification_templates:
all_notification_templates = set(notification_templates.get('success', []) + notification_templates.get('any', []))

View File

@ -20,6 +20,9 @@ import tempfile
# Decorator
from decorator import decorator
# Django
from django.utils.translation import ugettext_lazy as _
# Django REST Framework
from rest_framework.exceptions import ParseError, PermissionDenied
from django.utils.encoding import smart_str
@ -78,7 +81,7 @@ def to_python_boolean(value, allow_none=False):
elif allow_none and value.lower() in ('none', 'null'):
return None
else:
raise ValueError(u'Unable to convert "%s" to boolean' % unicode(value))
raise ValueError(_(u'Unable to convert "%s" to boolean') % unicode(value))
def camelcase_to_underscore(s):
'''
@ -193,7 +196,7 @@ def decrypt_field(instance, field_name, subfield=None):
return value
algo, b64data = value[len('$encrypted$'):].split('$', 1)
if algo != 'AES':
raise ValueError('unsupported algorithm: %s' % algo)
raise ValueError(_('unsupported algorithm: %s') % algo)
encrypted = base64.b64decode(b64data)
key = get_encryption_key(instance, field_name)
cipher = AES.new(key, AES.MODE_ECB)
@ -214,16 +217,16 @@ def update_scm_url(scm_type, url, username=True, password=True,
# hg: http://www.selenic.com/mercurial/hg.1.html#url-paths
# svn: http://svnbook.red-bean.com/en/1.7/svn-book.html#svn.advanced.reposurls
if scm_type not in ('git', 'hg', 'svn'):
raise ValueError('Unsupported SCM type "%s"' % str(scm_type))
raise ValueError(_('Unsupported SCM type "%s"') % str(scm_type))
if not url.strip():
return ''
parts = urlparse.urlsplit(url)
try:
parts.port
except ValueError:
raise ValueError('Invalid %s URL' % scm_type)
raise ValueError(_('Invalid %s URL') % scm_type)
if parts.scheme == 'git+ssh' and not scp_format:
raise ValueError('Unsupported %s URL' % scm_type)
raise ValueError(_('Unsupported %s URL') % scm_type)
if '://' not in url:
# Handle SCP-style URLs for git (e.g. [user@]host.xz:path/to/repo.git/).
@ -233,7 +236,7 @@ def update_scm_url(scm_type, url, username=True, password=True,
else:
userpass, hostpath = '', url
if hostpath.count(':') > 1:
raise ValueError('Invalid %s URL' % scm_type)
raise ValueError(_('Invalid %s URL') % scm_type)
host, path = hostpath.split(':', 1)
#if not path.startswith('/') and not path.startswith('~/'):
# path = '~/%s' % path
@ -252,7 +255,7 @@ def update_scm_url(scm_type, url, username=True, password=True,
else:
parts = urlparse.urlsplit('file://%s' % url)
else:
raise ValueError('Invalid %s URL' % scm_type)
raise ValueError(_('Invalid %s URL') % scm_type)
# Validate that scheme is valid for given scm_type.
scm_type_schemes = {
@ -261,11 +264,11 @@ def update_scm_url(scm_type, url, username=True, password=True,
'svn': ('http', 'https', 'svn', 'svn+ssh', 'file'),
}
if parts.scheme not in scm_type_schemes.get(scm_type, ()):
raise ValueError('Unsupported %s URL' % scm_type)
raise ValueError(_('Unsupported %s URL') % scm_type)
if parts.scheme == 'file' and parts.netloc not in ('', 'localhost'):
raise ValueError('Unsupported host "%s" for file:// URL' % (parts.netloc))
raise ValueError(_('Unsupported host "%s" for file:// URL') % (parts.netloc))
elif parts.scheme != 'file' and not parts.netloc:
raise ValueError('Host is required for %s URL' % parts.scheme)
raise ValueError(_('Host is required for %s URL') % parts.scheme)
if username is True:
netloc_username = parts.username or ''
elif username:
@ -283,13 +286,13 @@ def update_scm_url(scm_type, url, username=True, password=True,
if check_special_cases:
special_git_hosts = ('github.com', 'bitbucket.org', 'altssh.bitbucket.org')
if scm_type == 'git' and parts.scheme.endswith('ssh') and parts.hostname in special_git_hosts and netloc_username != 'git':
raise ValueError('Username must be "git" for SSH access to %s.' % parts.hostname)
raise ValueError(_('Username must be "git" for SSH access to %s.') % parts.hostname)
if scm_type == 'git' and parts.scheme.endswith('ssh') and parts.hostname in special_git_hosts and netloc_password:
#raise ValueError('Password not allowed for SSH access to %s.' % parts.hostname)
netloc_password = ''
special_hg_hosts = ('bitbucket.org', 'altssh.bitbucket.org')
if scm_type == 'hg' and parts.scheme == 'ssh' and parts.hostname in special_hg_hosts and netloc_username != 'hg':
raise ValueError('Username must be "hg" for SSH access to %s.' % parts.hostname)
raise ValueError(_('Username must be "hg" for SSH access to %s.') % parts.hostname)
if scm_type == 'hg' and parts.scheme == 'ssh' and netloc_password:
#raise ValueError('Password not supported for SSH with Mercurial.')
netloc_password = ''

View File

@ -188,4 +188,4 @@ def vars_validate_or_raise(vars_str):
return vars_str
except yaml.YAMLError:
pass
raise RestValidationError('Must be valid JSON or YAML.')
raise RestValidationError(_('Must be valid JSON or YAML.'))

View File

@ -4,6 +4,7 @@
# Django
from django.shortcuts import render
from django.utils.html import format_html
from django.utils.translation import ugettext_lazy as _
# Django REST Framework
from rest_framework import exceptions, permissions, views
@ -16,7 +17,7 @@ class ApiErrorView(views.APIView):
metadata_class = None
allowed_methods = ('GET', 'HEAD')
exception_class = exceptions.APIException
view_name = 'API Error'
view_name = _('API Error')
def get_view_name(self):
return self.view_name
@ -45,31 +46,31 @@ def handle_error(request, status=404, **kwargs):
def handle_400(request):
kwargs = {
'name': 'Bad Request',
'content': 'The request could not be understood by the server.',
'name': _('Bad Request'),
'content': _('The request could not be understood by the server.'),
}
return handle_error(request, 400, **kwargs)
def handle_403(request):
kwargs = {
'name': 'Forbidden',
'content': 'You don\'t have permission to access the requested resource.',
'name': _('Forbidden'),
'content': _('You don\'t have permission to access the requested resource.'),
}
return handle_error(request, 403, **kwargs)
def handle_404(request):
kwargs = {
'name': 'Not Found',
'content': 'The requested resource could not be found.',
'name': _('Not Found'),
'content': _('The requested resource could not be found.'),
}
return handle_error(request, 404, **kwargs)
def handle_500(request):
kwargs = {
'name': 'Server Error',
'content': 'A server error has occurred.',
'name': _('Server Error'),
'content': _('A server error has occurred.'),
}
return handle_error(request, 500, **kwargs)

View File

@ -10,8 +10,12 @@ from datetime import timedelta
from kombu import Queue, Exchange
# Update this module's local settings from the global settings module.
# global settings
from django.conf import global_settings
# ugettext lazy
from django.utils.translation import ugettext_lazy as _
# Update this module's local settings from the global settings module.
this_module = sys.modules[__name__]
for setting in dir(global_settings):
if setting == setting.upper():
@ -117,6 +121,11 @@ LOG_ROOT = os.path.join(BASE_DIR)
# The heartbeat file for the tower scheduler
SCHEDULE_METADATA_LOCATION = os.path.join(BASE_DIR, '.tower_cycle')
# Django gettext files path: locale/<lang-code>/LC_MESSAGES/django.po, django.mo
LOCALE_PATHS = (
os.path.join(BASE_DIR, 'locale'),
)
# Maximum number of the same job that can be waiting to run when launching from scheduler
# Note: This setting may be overridden by database settings.
SCHEDULE_MAX_JOBS = 10
@ -154,8 +163,9 @@ TEMPLATE_CONTEXT_PROCESSORS = ( # NOQA
)
MIDDLEWARE_CLASSES = ( # NOQA
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.locale.LocaleMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
@ -571,12 +581,12 @@ AD_HOC_COMMANDS = [
# instead (based on docs from:
# http://docs.rackspace.com/loadbalancers/api/v1.0/clb-devguide/content/Service_Access_Endpoints-d1e517.html)
RAX_REGION_CHOICES = [
('ORD', 'Chicago'),
('DFW', 'Dallas/Ft. Worth'),
('IAD', 'Northern Virginia'),
('LON', 'London'),
('SYD', 'Sydney'),
('HKG', 'Hong Kong'),
('ORD', _('Chicago')),
('DFW', _('Dallas/Ft. Worth')),
('IAD', _('Northern Virginia')),
('LON', _('London')),
('SYD', _('Sydney')),
('HKG', _('Hong Kong')),
]
# Inventory variable name/values for determining if host is active/enabled.
@ -603,20 +613,20 @@ INV_ENV_VARIABLE_BLACKLIST = ("HOME", "USER", "_", "TERM")
# list of names here. The available region IDs will be pulled from boto.
# http://docs.aws.amazon.com/general/latest/gr/rande.html#ec2_region
EC2_REGION_NAMES = {
'us-east-1': 'US East (Northern Virginia)',
'us-east-2': 'US East (Ohio)',
'us-west-2': 'US West (Oregon)',
'us-west-1': 'US West (Northern California)',
'eu-central-1': 'EU (Frankfurt)',
'eu-west-1': 'EU (Ireland)',
'ap-southeast-1': 'Asia Pacific (Singapore)',
'ap-southeast-2': 'Asia Pacific (Sydney)',
'ap-northeast-1': 'Asia Pacific (Tokyo)',
'ap-northeast-2': 'Asia Pacific (Seoul)',
'ap-south-1': 'Asia Pacific (Mumbai)',
'sa-east-1': 'South America (Sao Paulo)',
'us-gov-west-1': 'US West (GovCloud)',
'cn-north-1': 'China (Beijing)',
'us-east-1': _('US East (Northern Virginia)'),
'us-east-2': _('US East (Ohio)'),
'us-west-2': _('US West (Oregon)'),
'us-west-1': _('US West (Northern California)'),
'eu-central-1': _('EU (Frankfurt)'),
'eu-west-1': _('EU (Ireland)'),
'ap-southeast-1': _('Asia Pacific (Singapore)'),
'ap-southeast-2': _('Asia Pacific (Sydney)'),
'ap-northeast-1': _('Asia Pacific (Tokyo)'),
'ap-northeast-2': _('Asia Pacific (Seoul)'),
'ap-south-1': _('Asia Pacific (Mumbai)'),
'sa-east-1': _('South America (Sao Paulo)'),
'us-gov-west-1': _('US West (GovCloud)'),
'cn-north-1': _('China (Beijing)'),
}
EC2_REGIONS_BLACKLIST = [
@ -665,19 +675,19 @@ VMWARE_EXCLUDE_EMPTY_GROUPS = True
# provide a list here.
# Source: https://developers.google.com/compute/docs/zones
GCE_REGION_CHOICES = [
('us-east1-b', 'US East (B)'),
('us-east1-c', 'US East (C)'),
('us-east1-d', 'US East (D)'),
('us-central1-a', 'US Central (A)'),
('us-central1-b', 'US Central (B)'),
('us-central1-c', 'US Central (C)'),
('us-central1-f', 'US Central (F)'),
('europe-west1-b', 'Europe West (B)'),
('europe-west1-c', 'Europe West (C)'),
('europe-west1-d', 'Europe West (D)'),
('asia-east1-a', 'Asia East (A)'),
('asia-east1-b', 'Asia East (B)'),
('asia-east1-c', 'Asia East (C)'),
('us-east1-b', _('US East (B)')),
('us-east1-c', _('US East (C)')),
('us-east1-d', _('US East (D)')),
('us-central1-a', _('US Central (A)')),
('us-central1-b', _('US Central (B)')),
('us-central1-c', _('US Central (C)')),
('us-central1-f', _('US Central (F)')),
('europe-west1-b', _('Europe West (B)')),
('europe-west1-c', _('Europe West (C)')),
('europe-west1-d', _('Europe West (D)')),
('asia-east1-a', _('Asia East (A)')),
('asia-east1-b', _('Asia East (B)')),
('asia-east1-c', _('Asia East (C)')),
]
GCE_REGIONS_BLACKLIST = []
@ -701,19 +711,19 @@ GCE_INSTANCE_ID_VAR = None
# It's not possible to get zones in Azure without authenticating, so we
# provide a list here.
AZURE_REGION_CHOICES = [
('Central_US', 'US Central'),
('East_US_1', 'US East'),
('East_US_2', 'US East 2'),
('North_Central_US', 'US North Central'),
('South_Central_US', 'US South Central'),
('West_US', 'US West'),
('North_Europe', 'Europe North'),
('West_Europe', 'Europe West'),
('East_Asia_Pacific', 'Asia Pacific East'),
('Southest_Asia_Pacific', 'Asia Pacific Southeast'),
('East_Japan', 'Japan East'),
('West_Japan', 'Japan West'),
('South_Brazil', 'Brazil South'),
('Central_US', _('US Central')),
('East_US_1', _('US East')),
('East_US_2', _('US East 2')),
('North_Central_US', _('US North Central')),
('South_Central_US', _('US South Central')),
('West_US', _('US West')),
('North_Europe', _('Europe North')),
('West_Europe', _('Europe West')),
('East_Asia_Pacific', _('Asia Pacific East')),
('Southest_Asia_Pacific', _('Asia Pacific Southeast')),
('East_Japan', _('Japan East')),
('West_Japan', _('Japan West')),
('South_Brazil', _('Brazil South')),
]
AZURE_REGIONS_BLACKLIST = []

View File

@ -7,6 +7,9 @@ import re
# Python Social Auth
from social.exceptions import AuthException
# Django
from django.utils.translation import ugettext_lazy as _
# Tower
from awx.conf.license import feature_enabled
@ -18,13 +21,13 @@ class AuthNotFound(AuthException):
super(AuthNotFound, self).__init__(backend, *args, **kwargs)
def __str__(self):
return 'An account cannot be found for {0}'.format(self.email_or_uid)
return _('An account cannot be found for {0}').format(self.email_or_uid)
class AuthInactive(AuthException):
def __str__(self):
return 'Your account is inactive'
return _('Your account is inactive')
def check_user_found_or_created(backend, details, user=None, *args, **kwargs):

View File

@ -36,9 +36,9 @@
{% if user.is_authenticated %}
<li><a href="{% url 'api:user_me_list' %}" data-toggle="tooltip" data-placement="bottom" data-delay="1000" title="Logged in as {{ user }}{% if user.get_full_name %} ({{ user.get_full_name }}){% endif %}"><span class="glyphicon glyphicon-user"></span> <span class="visible-xs-inline">Logged in as </span>{{ user }}{% if user.get_full_name %}<span class="visible-xs-inline"> ({{ user.get_full_name }})</span>{% endif %}</a></li>
{% endif %}
<li><a href="//docs.ansible.com/ansible-tower/{{short_tower_version}}/html/towerapi/index.html" target="_blank" data-toggle="tooltip" data-placement="bottom" data-delay="1000" title="Ansible Tower API Guide"><span class="glyphicon glyphicon-question-sign"></span><span class="visible-xs-inline"> Ansible Tower API Guide</span></a></li>
<li><a href="/" data-toggle="tooltip" data-placement="bottom" data-delay="1000" title="Back to Ansible Tower"><span class="glyphicon glyphicon-circle-arrow-left"></span><span class="visible-xs-inline"> Back to Ansible Tower</span></a></li>
<li class="hidden-xs"><a href="#" class="resize" data-toggle="tooltip" data-placement="bottom" data-delay="1000" title="Resize"><span class="glyphicon glyphicon-resize-full"></span></a></li>
<li><a href="//docs.ansible.com/ansible-tower/{{short_tower_version}}/html/towerapi/index.html" target="_blank" data-toggle="tooltip" data-placement="bottom" data-delay="1000" title="{% trans 'Ansible Tower API Guide' %}"><span class="glyphicon glyphicon-question-sign"></span><span class="visible-xs-inline">{% trans 'Ansible Tower API Guide' %}</span></a></li>
<li><a href="/" data-toggle="tooltip" data-placement="bottom" data-delay="1000" title="{% trans 'Back to Ansible Tower' %}"><span class="glyphicon glyphicon-circle-arrow-left"></span><span class="visible-xs-inline">{% trans 'Back to Ansible Tower' %}</span></a></li>
<li class="hidden-xs"><a href="#" class="resize" data-toggle="tooltip" data-placement="bottom" data-delay="1000" title="{% trans 'Resize' %}"><span class="glyphicon glyphicon-resize-full"></span></a></li>
</ul>
</div>
</div>

View File

@ -1,8 +1,8 @@
<!DOCTYPE html>
{# Copy of base.html from rest_framework with minor Ansible Tower change. #}
{% load staticfiles %}
{% load rest_framework %}
{% load i18n %}
<!DOCTYPE html>
<html>
<head>
{% block head %}
@ -75,21 +75,21 @@
<fieldset>
{% if api_settings.URL_FORMAT_OVERRIDE %}
<div class="btn-group format-selection">
<a class="btn btn-primary js-tooltip" href="{{ request.get_full_path }}" rel="nofollow" title="Make a GET request on the {{ name }} resource">GET</a>
<a class="btn btn-primary js-tooltip" href="{{ request.get_full_path }}" rel="nofollow" title="{% blocktrans %}Make a GET request on the {{ name }} resource{% endblocktrans %}">GET</a>
<button class="btn btn-primary dropdown-toggle js-tooltip" data-toggle="dropdown" title="Specify a format for the GET request">
<button class="btn btn-primary dropdown-toggle js-tooltip" data-toggle="dropdown" title="{% trans 'Specify a format for the GET request' %}">
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
{% for format in available_formats %}
<li>
<a class="js-tooltip format-option" href="{% add_query_param request api_settings.URL_FORMAT_OVERRIDE format %}" rel="nofollow" title="Make a GET request on the {{ name }} resource with the format set to `{{ format }}`">{{ format }}</a>
<a class="js-tooltip format-option" href="{% add_query_param request api_settings.URL_FORMAT_OVERRIDE format %}" rel="nofollow" title="{% blocktrans %}Make a GET request on the {{ name }} resource with the format set to `{{ format }}`{% endblocktrans %}">{{ format }}</a>
</li>
{% endfor %}
</ul>
</div>
{% else %}
<a class="btn btn-primary js-tooltip" href="{{ request.get_full_path }}" rel="nofollow" title="Make a GET request on the {{ name }} resource">GET</a>
<a class="btn btn-primary js-tooltip" href="{{ request.get_full_path }}" rel="nofollow" title="{% blocktrans %}Make a GET request on the {{ name }} resource{% endblocktrans %}">GET</a>
{% endif %}
</fieldset>
</form>
@ -97,13 +97,13 @@
{% if options_form %}
<form class="button-form" action="{{ request.get_full_path }}" data-method="OPTIONS">
<button class="btn btn-primary js-tooltip" title="Make an OPTIONS request on the {{ name }} resource">OPTIONS</button>
<button class="btn btn-primary js-tooltip" title="{% blocktrans %}Make an OPTIONS request on the {{ name }} resource{% endblocktrans %}">OPTIONS</button>
</form>
{% endif %}
{% if delete_form %}
<form class="button-form" action="{{ request.get_full_path }}" data-method="DELETE">
<button class="btn btn-danger js-tooltip" title="Make a DELETE request on the {{ name }} resource">DELETE</button>
<button class="btn btn-danger js-tooltip" title="{% blocktrans %}Make a DELETE request on the {{ name }} resource{% endblocktrans %}">DELETE</button>
</form>
{% endif %}
@ -169,7 +169,7 @@
{% csrf_token %}
{{ post_form }}
<div class="form-actions">
<button class="btn btn-primary" title="Make a POST request on the {{ name }} resource">POST</button>
<button class="btn btn-primary" title="{% blocktrans %}Make a POST request on the {{ name }} resource{% endblocktrans %}">POST</button>
</div>
</fieldset>
</form>
@ -183,7 +183,7 @@
<fieldset>
{% include "rest_framework/raw_data_form.html" %}
<div class="form-actions">
<button class="btn btn-primary" title="Make a POST request on the {{ name }} resource">POST</button>
<button class="btn btn-primary" title="{% blocktrans %}Make a POST request on the {{ name }} resource{% endblocktrans %}">POST</button>
</div>
</fieldset>
</form>
@ -213,7 +213,7 @@
<fieldset>
{{ put_form }}
<div class="form-actions">
<button class="btn btn-primary js-tooltip" title="Make a PUT request on the {{ name }} resource">PUT</button>
<button class="btn btn-primary js-tooltip" title="{% blocktrans %}Make a PUT request on the {{ name }} resource{% endblocktrans %}">PUT</button>
</div>
</fieldset>
</form>
@ -227,10 +227,10 @@
{% include "rest_framework/raw_data_form.html" %}
<div class="form-actions">
{% if raw_data_put_form %}
<button class="btn btn-primary js-tooltip" title="Make a PUT request on the {{ name }} resource">PUT</button>
<button class="btn btn-primary js-tooltip" title="{% blocktrans %}Make a PUT request on the {{ name }} resource{% endblocktrans %}">PUT</button>
{% endif %}
{% if raw_data_patch_form %}
<button data-method="PATCH" class="btn btn-primary js-tooltip" title="Make a PATCH request on the {{ name }} resource">PATCH</button>
<button data-method="PATCH" class="btn btn-primary js-tooltip" title="{% blocktrans %}Make a PATCH request on the {{ name }} resource{% endblocktrans %}">PATCH</button>
{% endif %}
</div>
</fieldset>

View File

@ -1,10 +1,11 @@
<!DOCTYPE html>
{% load i18n %}
<html lang="en" ng-app="Tower">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>Ansible Tower</title>
<title>{% trans 'Ansible Tower' %}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="{{ STATIC_URL }}assets/custom-theme/jquery-ui-1.10.3.custom.min.css" />
<link rel="stylesheet" href="{{ STATIC_URL }}assets/ansible-bootstrap.min.css" />
@ -44,7 +45,7 @@
</div>
<!-- Password Dialog -->
<div id="password-modal" style="display: none;"></div>
<div id="idle-modal" style="display:none">Your session will expire in <span id="remaining_seconds" class="IdleModal-remainingSeconds">60</span> seconds, would you like to continue?</div>
<div id="idle-modal" style="display:none">{% blocktrans %}Your session will expire in <span id="remaining_seconds" class="IdleModal-remainingSeconds">60</span> seconds, would you like to continue?{% endblocktrans %}</div>
<stream-detail-modal></stream-detail-modal>
<!-- Confirmation Dialog -->
<div id="prompt-modal" class="modal fade">
@ -59,7 +60,7 @@
<div class="Modal-body" ng-bind-html="promptBody" id="prompt-body">
</div>
<div class="Modal-footer">
<a href="#" data-target="#prompt-modal" data-dismiss="modal" id="prompt_cancel_btn" class="btn Modal-defaultButton Modal-footerButton">CANCEL</a>
<a href="#" data-target="#prompt-modal" data-dismiss="modal" id="prompt_cancel_btn" class="btn Modal-defaultButton Modal-footerButton">{% trans 'CANCEL' %}</a>
<a href="" ng-class="promptActionBtnClass" ng-click="promptAction()" id="prompt_action_btn" class="btn Modal-footerButton" ng-bind="promptActionText"></a>
</div>
</div>
@ -80,7 +81,7 @@
<div id="alert-modal-msg" class="alert" ng-bind-html="alertBody"></div>
</div>
<div class="modal-footer">
<a href="#" ng-hide="disableButtons" data-target="#form-modal" data-dismiss="modal" id="alert_ok_btn" class="btn btn-primary">OK</a>
<a href="#" ng-hide="disableButtons" data-target="#form-modal" data-dismiss="modal" id="alert_ok_btn" class="btn btn-primary">{% trans 'OK' %}</a>
</div>
</div>
<!-- modal-content -->
@ -99,7 +100,7 @@
<div id="alert2-modal-msg" class="alert" ng-bind-html="alertBody2"></div>
</div>
<div class="modal-footer">
<a href="#" ng-hide="disableButtons2" data-target="#form-modal2" data-dismiss="modal" id="alert2_ok_btn" class="btn btn-primary">OK</a>
<a href="#" ng-hide="disableButtons2" data-target="#form-modal2" data-dismiss="modal" id="alert2_ok_btn" class="btn btn-primary">{% trans 'OK' %}</a>
</div>
</div>
<!-- modal-content -->
@ -111,27 +112,27 @@
<div id="help-modal-dialog" style="display: none;"></div>
<div id="prompt-for-days" style="display:none">
<form name="prompt_for_days_form" id="prompt_for_days_form" class="MgmtCards-promptText">
Set how many days of data should be retained.
{% trans 'Set how many days of data should be retained.' %}
<br>
<input type="integer" id="days_to_keep" name="days_to_keep" ng-model="days_to_keep" ng-required="true" class="form-control Form-textInput" min=0 max=9999 style="margin-top:10px;" integer>
<div class="error" ng-show="prompt_for_days_form.days_to_keep.$dirty && (prompt_for_days_form.days_to_keep.$error.number || prompt_for_days_form.days_to_keep.$error.integer ||
prompt_for_days_form.days_to_keep.$error.required ||
prompt_for_days_form.days_to_keep.$error.min ||
prompt_for_days_form.days_to_keep.$error.max)">Please enter an integer<span ng-show="prompt_for_days_form.days_to_keep.$dirty && prompt_for_days_form.days_to_keep.$error.min"> that is not negative</span><span ng-show="prompt_for_days_form.days_to_keep.$dirty && prompt_for_days_form.days_to_keep.$error.max"> that is lower than 9999</span>.</div>
prompt_for_days_form.days_to_keep.$error.max)">{% blocktrans %}Please enter an integer<span ng-show="prompt_for_days_form.days_to_keep.$dirty && prompt_for_days_form.days_to_keep.$error.min"> that is not negative</span><span ng-show="prompt_for_days_form.days_to_keep.$dirty && prompt_for_days_form.days_to_keep.$error.max"> that is lower than 9999</span>.{% endblocktrans %}</div>
</form>
</div>
<div id="prompt-for-days-facts" style="display:none">
<form name="prompt_for_days_facts_form" id="prompt_for_days_facts_form" class="MgmtCards-promptText">
<div style="padding-bottom:15px;">For facts collected older than the time period specified, save one fact scan (snapshot) per time window (frequency). For example, facts older than 30 days are purged, while one weekly fact scan is kept.
<div style="padding-bottom:15px;">{% blocktrans %}For facts collected older than the time period specified, save one fact scan (snapshot) per time window (frequency). For example, facts older than 30 days are purged, while one weekly fact scan is kept.
<br>
<br> CAUTION: Setting both numerical variables to "0" will delete all facts.
<br>
<br>
<br>{% endblocktrans %}
</div>
<div class="form-group">
<label for="description">
<span class="label-text">
Select a time period after which to remove old facts
{% trans 'Select a time period after which to remove old facts' %}
</span>
</label>
<div class="row">
@ -145,12 +146,12 @@
<div class="error" ng-show="prompt_for_days_facts_form.keep_amount.$dirty && (prompt_for_days_facts_form.keep_amount.$error.number || prompt_for_days_facts_form.keep_amount.$error.integer ||
prompt_for_days_facts_form.keep_amount.$error.required ||
prompt_for_days_facts_form.keep_amount.$error.min ||
prompt_for_days_facts_form.keep_amount.$error.max)">Please enter an integer<span ng-show="prompt_for_days_facts_form.keep_amount.$dirty && prompt_for_days_facts_form.keep_amount.$error.min"> that is not negative</span><span ng-show="prompt_for_days_facts_form.keep_amount.$dirty && prompt_for_days_facts_form.keep_amount.$error.max"> that is lower than 9999</span>.</div>
prompt_for_days_facts_form.keep_amount.$error.max)">{% blocktrans %}Please enter an integer<span ng-show="prompt_for_days_facts_form.keep_amount.$dirty && prompt_for_days_facts_form.keep_amount.$error.min"> that is not negative</span><span ng-show="prompt_for_days_facts_form.keep_amount.$dirty && prompt_for_days_facts_form.keep_amount.$error.max"> that is lower than 9999</span>.{% endblocktrans %}</div>
</div>
<div class="form-group ">
<label for="description">
<span class="label-text">
Select a frequency for snapshot retention
{% trans 'Select a frequency for snapshot retention' %}
</span>
</label>
<div class="row">
@ -164,13 +165,13 @@
<div class="error" ng-show="prompt_for_days_facts_form.granularity_keep_amount.$dirty && (prompt_for_days_facts_form.granularity_keep_amount.$error.number || prompt_for_days_facts_form.granularity_keep_amount.$error.integer ||
prompt_for_days_facts_form.granularity_keep_amount.$error.required ||
prompt_for_days_facts_form.granularity_keep_amount.$error.min ||
prompt_for_days_facts_form.granularity_keep_amount.$error.max)">Please enter an integer<span ng-show="prompt_for_days_facts_form.granularity_keep_amount.$dirty && prompt_for_days_facts_form.granularity_keep_amount.$error.min"> that is not negative</span><span ng-show="prompt_for_days_facts_form.granularity_keep_amount.$dirty && prompt_for_days_facts_form.granularity_keep_amount.$error.max"> that is lower than 9999</span>.</div>
prompt_for_days_facts_form.granularity_keep_amount.$error.max)">{% blocktrans %}Please enter an integer<span ng-show="prompt_for_days_facts_form.granularity_keep_amount.$dirty && prompt_for_days_facts_form.granularity_keep_amount.$error.min"> that is not negative</span><span ng-show="prompt_for_days_facts_form.granularity_keep_amount.$dirty && prompt_for_days_facts_form.granularity_keep_amount.$error.max"> that is lower than 9999</span>.{% endblocktrans %}</div>
</div>
</form>
</div>
<div class="overlay"></div>
<div class="spinny"><i class="fa fa-cog fa-spin fa-2x"></i>
<p>working...</p>
<p>{% trans 'working...' %}</p>
</div>
</div>
<tower-footer></tower-footer>

View File

@ -0,0 +1,177 @@
#!/usr/bin/env python
#
# NOTE: This script is based on django's manage_translations.py script
# (https://github.com/django/django/blob/master/scripts/manage_translations.py)
#
# This python file contains utility scripts to manage Ansible-Tower translations.
# It has to be run inside the ansible-tower git root directory.
#
# The following commands are available:
#
# * update: check for new strings in ansible-tower catalogs, and
# output how much strings are new/changed.
#
# * stats: output statistics for each language
#
# * pull: pull/fetch translations from Zanata
#
# * push: update resources in Zanata with the local files
#
# Each command support the --lang option to limit their operation to
# the specified language(s). For example,
# to pull translations for Japanese and French, run:
#
# $ python tools/scripts/manage_translations.py pull --lang ja,fr
import os
from argparse import ArgumentParser
from subprocess import PIPE, Popen
from xml.etree import ElementTree as ET
from xml.etree.ElementTree import ParseError
import django
from django.conf import settings
from django.core.management import call_command
PROJECT_CONFIG = "tools/scripts/zanata_config/backend-translations.xml"
MIN_TRANS_PERCENT_SETTING = False
MIN_TRANS_PERCENT = '10'
def _get_zanata_project_url():
project_url = ''
try:
zanata_config = ET.parse(PROJECT_CONFIG).getroot()
server_url = zanata_config.getchildren()[0].text
project_id = zanata_config.getchildren()[1].text
version_id = zanata_config.getchildren()[2].text
middle_url = "iteration/view/" if server_url[-1:] == '/' else "/iteration/view/"
project_url = server_url + middle_url + project_id + "/" + version_id + "/documents"
except (ParseError, IndexError):
print("Please re-check zanata project configuration.")
return project_url
def _handle_response(output, errors):
if not errors and '\n' in output:
for response in output.split('\n'):
print(response)
return True
else:
print(errors.strip())
return False
def _check_diff(base_path):
"""
Output the approximate number of changed/added strings in the POT
"""
po_path = '%s/django.pot' % base_path
p = Popen("git diff -U0 %s | egrep '^[-+]msgid' | wc -l" % po_path,
stdout=PIPE, stderr=PIPE, shell=True)
output, errors = p.communicate()
num_changes = int(output.strip())
print("[ %d ] changed/added messages in catalog." % num_changes)
def pull(lang=None, both=None):
"""
Pull translations .po from Zanata
"""
command = "zanata pull --project-config %(config)s --disable-ssl-cert"
if MIN_TRANS_PERCENT_SETTING:
command += " --min-doc-percent " + MIN_TRANS_PERCENT
if lang:
command += " --lang %s" % lang[0]
p = Popen(command % {'config': PROJECT_CONFIG},
stdout=PIPE, stderr=PIPE, shell=True)
output, errors = p.communicate()
_handle_response(output, errors)
def push(lang=None, both=None):
"""
Push django.pot to Zanata
At Zanata:
(1) project_type should be podir - {locale}/{filename}.po format
(2) only required languages should be kept enabled
"""
p = Popen("zanata push --project-config %(config)s --push-type source --disable-ssl-cert" %
{'config': PROJECT_CONFIG}, stdout=PIPE, stderr=PIPE, shell=True)
output, errors = p.communicate()
if _handle_response(output, errors):
print("Zanata URL: %s\n" % _get_zanata_project_url())
def stats(lang=None, both=None):
"""
Get translation stats from Zanata
"""
command = "zanata stats --project-config %(config)s --disable-ssl-cert"
if lang:
command += " --lang %s" % lang[0]
p = Popen(command % {'config': PROJECT_CONFIG},
stdout=PIPE, stderr=PIPE, shell=True)
output, errors = p.communicate()
_handle_response(output, errors)
def update(lang=None, both=None):
"""
Update (1) awx/locale/django.pot and/or
(2) awx/ui/po/ansible-tower.pot files with
new/updated translatable strings.
"""
settings.configure()
django.setup()
print("Updating catalog for Ansible Tower:")
if both:
print("Angular...")
p = Popen("make pot", stdout=PIPE, stderr=PIPE, shell=True)
output, errors = p.communicate()
_handle_response(output, errors)
print("Django...")
lang = (lang[0].split(',') if ',' in lang[0] else lang) if lang else []
os.chdir(os.path.join(os.getcwd(), 'awx'))
call_command('makemessages', '--keep-pot', locale=lang)
# Output changed stats
_check_diff(os.path.join(os.getcwd(), 'locale'))
if __name__ == "__main__":
try:
devnull = open(os.devnull)
Popen(["zanata"], stdout=devnull, stderr=devnull).communicate()
except OSError as e:
if e.errno == os.errno.ENOENT:
print('''
You need zanata-python-client, install it.
1. Install zanata-python-client, use
$ dnf install zanata-python-client
2. Create ~/.config/zanata.ini file:
$ vim ~/.config/zanata.ini
[servers]
translate_zanata_org.url=https://translate.engineering.redhat.com/
translate_zanata_org.username=ansibletoweruser
translate_zanata_org.key=
''')
exit(1)
RUNABLE_SCRIPTS = ('update', 'stats', 'pull', 'push')
parser = ArgumentParser()
parser.add_argument('cmd', nargs=1, choices=RUNABLE_SCRIPTS)
parser.add_argument("-l", "--lang", action='append', help="specify comma seperated locales")
parser.add_argument("-u", "--both", action='store_true', help="specify to include ui tasks")
options = parser.parse_args()
eval(options.cmd[0])(options.lang, options.both)