diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 4eae56f54e..7216b9cb97 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -624,8 +624,8 @@ class UnifiedJobSerializer(BaseSerializer): obj_size = obj.result_stdout_size if 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} + "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 @@ -682,8 +682,8 @@ class UnifiedJobStdoutSerializer(UnifiedJobSerializer): obj_size = obj.result_stdout_size if 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} + "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): @@ -1849,7 +1849,7 @@ class JobOptionsSerializer(LabelsListMixin, BaseSerializer): 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.')}) - if project and playbook and force_text(playbook) not in project.playbooks: + if project and playbook and force_text(playbook) not in project.playbook_files: raise serializers.ValidationError({'playbook': _('Playbook not found for project.')}) if project and not playbook: raise serializers.ValidationError({'playbook': _('Must select playbook for project.')}) @@ -2324,6 +2324,11 @@ class WorkflowJobTemplateNodeSerializer(WorkflowNodeBaseSerializer): raise serializers.ValidationError({ "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") + }) ujt_obj = attrs.get('unified_job_template', None) if isinstance(ujt_obj, (WorkflowJobTemplate, SystemJobTemplate)): raise serializers.ValidationError({ @@ -2832,7 +2837,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() diff --git a/awx/api/views.py b/awx/api/views.py index 06a598c32f..d8a61547f9 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -2343,8 +2343,8 @@ class JobTemplateSurveySpec(GenericAPIView): if "variable" not in survey_item: 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' '%(item)s' duplicated in survey question %(survey)s.") % - {'item': survey_item['variable'], 'survey': 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: @@ -3568,8 +3568,8 @@ class UnifiedJobStdout(RetrieveAPIView): 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 (%(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} + "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: diff --git a/awx/lib/__init__.py b/awx/lib/__init__.py deleted file mode 100644 index e484e62be1..0000000000 --- a/awx/lib/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# Copyright (c) 2015 Ansible, Inc. -# All Rights Reserved. diff --git a/awx/lib/compat.py b/awx/lib/compat.py deleted file mode 100644 index fb686dd11c..0000000000 --- a/awx/lib/compat.py +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright (c) 2015 Ansible, Inc. -# All Rights Reserved. - -''' -Compability library for support of both Django 1.4.x and Django 1.5.x. -''' - -try: - from django.utils.html import format_html -except ImportError: - from django.utils.html import conditional_escape - from django.utils.safestring import mark_safe - - def format_html(format_string, *args, **kwargs): - args_safe = map(conditional_escape, args) - kwargs_safe = dict([(k, conditional_escape(v)) for (k, v) in - kwargs.items()]) - return mark_safe(format_string.format(*args_safe, **kwargs_safe)) - -try: - from django.utils.log import RequireDebugTrue -except ImportError: - import logging - from django.conf import settings - - class RequireDebugTrue(logging.Filter): - def filter(self, record): - return settings.DEBUG - -try: - from django.utils.text import slugify # noqa -except ImportError: - from django.template.defaultfilters import slugify # noqa diff --git a/awx/main/access.py b/awx/main/access.py index 37092d9398..ab57dc1bf5 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -1195,7 +1195,10 @@ class JobAccess(BaseAccess): return True return self.org_access(obj, role_types=['auditor_role', 'admin_role']) - def can_add(self, data): + def can_add(self, data, validate_license=True): + if validate_license: + self.check_license() + if not data: # So the browseable API will work return True if not self.user.is_superuser: @@ -1219,7 +1222,9 @@ class JobAccess(BaseAccess): return True def can_change(self, obj, data): - return obj.status == 'new' and self.can_read(obj) and self.can_add(data) + return (obj.status == 'new' and + self.can_read(obj) and + self.can_add(data, validate_license=False)) @check_superuser def can_delete(self, obj): diff --git a/awx/main/conf.py b/awx/main/conf.py index e0d16e8542..862238f3fa 100644 --- a/awx/main/conf.py +++ b/awx/main/conf.py @@ -173,3 +173,21 @@ register( category=_('Jobs'), category_slug='jobs', ) + +register( + 'DEFAULT_JOB_TIMEOUTS', + field_class=fields.DictField, + default={ + 'Job': 0, + 'InventoryUpdate': 0, + 'ProjectUpdate': 0, + }, + label=_('Default Job Timeouts'), + help_text=_('Maximum time to allow jobs to run. Use sub-keys of Job, ' + 'InventoryUpdate, and ProjectUpdate to configure this value ' + 'for each job type. Use value of 0 to indicate that no ' + 'timeout should be imposed. A timeout set on an individual ' + 'job template will override this.'), + category=_('Jobs'), + category_slug='jobs', +) diff --git a/awx/main/models/projects.py b/awx/main/models/projects.py index 155687fb76..9e00038f33 100644 --- a/awx/main/models/projects.py +++ b/awx/main/models/projects.py @@ -15,12 +15,12 @@ from django.conf import settings from django.db import models from django.utils.translation import ugettext_lazy as _ from django.utils.encoding import smart_str, smart_text +from django.utils.text import slugify from django.core.exceptions import ValidationError from django.core.urlresolvers import reverse from django.utils.timezone import now, make_aware, get_default_timezone # AWX -from awx.lib.compat import slugify from awx.main.models.base import * # noqa from awx.main.models.jobs import Job from awx.main.models.notifications import ( diff --git a/awx/main/models/workflow.py b/awx/main/models/workflow.py index 2848b38a4a..d109b43519 100644 --- a/awx/main/models/workflow.py +++ b/awx/main/models/workflow.py @@ -140,7 +140,6 @@ class WorkflowNodeBase(CreatedModifiedModel): 'inventory', 'credential', 'char_prompts'] class WorkflowJobTemplateNode(WorkflowNodeBase): - # TODO: Ensure the API forces workflow_job_template being set workflow_job_template = models.ForeignKey( 'WorkflowJobTemplate', related_name='workflow_job_template_nodes', @@ -149,7 +148,7 @@ class WorkflowJobTemplateNode(WorkflowNodeBase): default=None, on_delete=models.CASCADE, ) - + def get_absolute_url(self): return reverse('api:workflow_job_template_node_detail', args=(self.pk,)) diff --git a/awx/main/tests/factories/fixtures.py b/awx/main/tests/factories/fixtures.py index ac6c93348d..8f6e5df414 100644 --- a/awx/main/tests/factories/fixtures.py +++ b/awx/main/tests/factories/fixtures.py @@ -74,7 +74,8 @@ def mk_user(name, is_superuser=False, organization=None, team=None, persisted=Tr def mk_project(name, organization=None, description=None, persisted=True): description = description or '{}-description'.format(name) - project = Project(name=name, description=description) + project = Project(name=name, description=description, + playbook_files=['helloworld.yml', 'alt-helloworld.yml']) if organization is not None: project.organization = organization if persisted: @@ -134,7 +135,7 @@ def mk_job_template(name, job_type='run', extra_vars = json.dumps(extra_vars) jt = JobTemplate(name=name, job_type=job_type, extra_vars=extra_vars, - playbook='mocked') + playbook='helloworld.yml') jt.inventory = inventory if jt.inventory is None: diff --git a/awx/main/tests/functional/api/test_job_template.py b/awx/main/tests/functional/api/test_job_template.py index 68a7e7aecd..ca6bdf3d31 100644 --- a/awx/main/tests/functional/api/test_job_template.py +++ b/awx/main/tests/functional/api/test_job_template.py @@ -1,22 +1,15 @@ import pytest -import mock # AWX from awx.api.serializers import JobTemplateSerializer, JobLaunchSerializer from awx.main.models.jobs import Job -from awx.main.models.projects import ProjectOptions from awx.main.migrations import _save_password_keys as save_password_keys # Django from django.core.urlresolvers import reverse from django.apps import apps -@property -def project_playbooks(self): - return ['mocked', 'mocked.yml', 'alt-mocked.yml'] - @pytest.mark.django_db -@mock.patch.object(ProjectOptions, "playbooks", project_playbooks) @pytest.mark.parametrize( "grant_project, grant_credential, grant_inventory, expect", [ (True, True, True, 201), @@ -38,11 +31,10 @@ def test_create(post, project, machine_credential, inventory, alice, grant_proje 'project': project.id, 'credential': machine_credential.id, 'inventory': inventory.id, - 'playbook': 'mocked.yml', + 'playbook': 'helloworld.yml', }, alice, expect=expect) @pytest.mark.django_db -@mock.patch.object(ProjectOptions, "playbooks", project_playbooks) @pytest.mark.parametrize( "grant_project, grant_credential, grant_inventory, expect", [ (True, True, True, 200), @@ -67,11 +59,10 @@ def test_edit_sensitive_fields(patch, job_template_factory, alice, grant_project 'project': objs.project.id, 'credential': objs.credential.id, 'inventory': objs.inventory.id, - 'playbook': 'alt-mocked.yml', + 'playbook': 'alt-helloworld.yml', }, alice, expect=expect) @pytest.mark.django_db -@mock.patch.object(ProjectOptions, "playbooks", project_playbooks) def test_edit_playbook(patch, job_template_factory, alice): objs = job_template_factory('jt', organization='org1', project='prj', inventory='inv', credential='cred') objs.job_template.admin_role.members.add(alice) @@ -80,16 +71,15 @@ def test_edit_playbook(patch, job_template_factory, alice): objs.inventory.use_role.members.add(alice) patch(reverse('api:job_template_detail', args=(objs.job_template.id,)), { - 'playbook': 'alt-mocked.yml', + 'playbook': 'alt-helloworld.yml', }, alice, expect=200) objs.inventory.use_role.members.remove(alice) patch(reverse('api:job_template_detail', args=(objs.job_template.id,)), { - 'playbook': 'mocked.yml', + 'playbook': 'helloworld.yml', }, alice, expect=403) @pytest.mark.django_db -@mock.patch.object(ProjectOptions, "playbooks", project_playbooks) def test_edit_nonsenstive(patch, job_template_factory, alice): objs = job_template_factory('jt', organization='org1', project='prj', inventory='inv', credential='cred') jt = objs.job_template @@ -121,10 +111,6 @@ def jt_copy_edit(job_template_factory, project): project=project) return objects.job_template -@property -def project_playbooks(self): - return ['mocked', 'mocked.yml', 'alt-mocked.yml'] - @pytest.mark.django_db def test_job_template_role_user(post, organization_factory, job_template_factory): objects = organization_factory("org", @@ -143,7 +129,6 @@ def test_job_template_role_user(post, organization_factory, job_template_factory @pytest.mark.django_db -@mock.patch.object(ProjectOptions, "playbooks", project_playbooks) def test_jt_admin_copy_edit_functional(jt_copy_edit, rando, get, post): # Grant random user JT admin access only diff --git a/awx/main/tests/functional/conftest.py b/awx/main/tests/functional/conftest.py index 0c620feb7e..0fb6084edb 100644 --- a/awx/main/tests/functional/conftest.py +++ b/awx/main/tests/functional/conftest.py @@ -110,7 +110,8 @@ def team_member(user, team): def project(instance, organization): prj = Project.objects.create(name="test-proj", description="test-proj-desc", - organization=organization + organization=organization, + playbook_files=['helloworld.yml', 'alt-helloworld.yml'] ) return prj diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 699b9aa288..e21ed1e499 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -10,16 +10,17 @@ 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(): setattr(this_module, setting, getattr(global_settings, setting)) -# gettext -from django.utils.translation import ugettext_lazy as _ - # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(__file__)) @@ -805,7 +806,7 @@ LOGGING = { '()': 'django.utils.log.RequireDebugFalse', }, 'require_debug_true': { - '()': 'awx.lib.compat.RequireDebugTrue', + '()': 'django.utils.log.RequireDebugTrue', }, 'require_debug_true_or_test': { '()': 'awx.main.utils.RequireDebugTrueOrTest', diff --git a/tools/scripts/manage_translations.py b/tools/scripts/manage_translations.py index d35f89e95d..384b2c22b4 100644 --- a/tools/scripts/manage_translations.py +++ b/tools/scripts/manage_translations.py @@ -25,24 +25,42 @@ import os from argparse import ArgumentParser -from subprocess import PIPE, Popen, call +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-trans-config.xml" +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): @@ -82,10 +100,11 @@ def push(lang=None, both=None): (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 --disable-ssl-cert" % + 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() - _handle_response(output, errors) + if _handle_response(output, errors): + print("Zanata URL: %s\n" % _get_zanata_project_url()) def stats(lang=None, both=None): @@ -104,7 +123,8 @@ def stats(lang=None, both=None): def update(lang=None, both=None): """ - Update the awx/locale/django.pot files with + Update (1) awx/locale/django.pot and/or + (2) awx/ui/po/ansible-tower.pot files with new/updated translatable strings. """ settings.configure()