From 96904968d850c4af22e58b69c0aa3a3221d70e9c Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Thu, 9 Nov 2017 17:24:54 -0500 Subject: [PATCH] Fix migration issues, tests, and templates --- awx/api/filters.py | 9 +- awx/api/generics.py | 4 +- awx/api/serializers.py | 2 +- awx/api/views.py | 5 +- awx/main/access.py | 16 +- awx/main/fields.py | 26 +- awx/main/tests/base.py | 2 +- .../functional/api/test_rbac_displays.py | 18 +- .../functional/commands/test_commands.py | 4 +- .../commands/test_inventory_import.py | 12 +- awx/main/tests/functional/conftest.py | 2 +- awx/main/tests/functional/test_rbac_api.py | 4 +- .../serializers/test_workflow_serializers.py | 8 +- awx/main/tests/unit/api/test_views.py | 17 - .../unit/commands/test_inventory_import.py | 12 +- awx/main/tests/unit/models/test_rbac_unit.py | 103 ---- awx/main/tests/unit/settings/test_defaults.py | 10 +- awx/main/tests/unit/test_access.py | 19 +- awx/main/utils/common.py | 4 +- awx/main/utils/db.py | 13 + awx/templates/rest_framework/base.html | 475 +++++++++--------- 21 files changed, 346 insertions(+), 419 deletions(-) delete mode 100644 awx/main/tests/unit/models/test_rbac_unit.py diff --git a/awx/api/filters.py b/awx/api/filters.py index a231c8af8d..03f848e6a8 100644 --- a/awx/api/filters.py +++ b/awx/api/filters.py @@ -22,6 +22,7 @@ from rest_framework.filters import BaseFilterBackend # AWX from awx.main.utils import get_type_for_model, to_python_boolean +from awx.main.utils.db import get_all_field_names from awx.main.models.credential import CredentialType from awx.main.models.rbac import RoleAncestorEntry @@ -70,7 +71,7 @@ class TypeFilterBackend(BaseFilterBackend): types_map[ct_type] = ct.pk model = queryset.model model_type = get_type_for_model(model) - if 'polymorphic_ctype' in model._meta.get_all_field_names(): + if 'polymorphic_ctype' in get_all_field_names(model): types_pks = set([v for k,v in types_map.items() if k in types]) queryset = queryset.filter(polymorphic_ctype_id__in=types_pks) elif model_type in types: @@ -119,7 +120,7 @@ class FieldLookupBackend(BaseFilterBackend): 'last_updated': 'last_job_run', }.get(name, name) - if name == 'type' and 'polymorphic_ctype' in model._meta.get_all_field_names(): + if name == 'type' and 'polymorphic_ctype' in get_all_field_names(model): name = 'polymorphic_ctype' new_parts.append('polymorphic_ctype__model') else: @@ -136,7 +137,7 @@ class FieldLookupBackend(BaseFilterBackend): new_parts.pop() new_parts.append(name_alt) else: - field = model._meta.get_field_by_name(name)[0] + field = model._meta.get_field(name) if isinstance(field, ForeignObjectRel) and getattr(field.field, '__prevent_search__', False): raise PermissionDenied(_('Filtering on %s is not allowed.' % name)) elif getattr(field, '__prevent_search__', False): @@ -375,7 +376,7 @@ class OrderByBackend(BaseFilterBackend): # given the limited number of views with multiple types, # sorting on polymorphic_ctype.model is effectively the same. new_order_by = [] - if 'polymorphic_ctype' in queryset.model._meta.get_all_field_names(): + if 'polymorphic_ctype' in get_all_field_names(queryset.model): for field in order_by: if field == 'type': new_order_by.append('polymorphic_ctype__model') diff --git a/awx/api/generics.py b/awx/api/generics.py index d2d7f0ff40..f69a4efd09 100644 --- a/awx/api/generics.py +++ b/awx/api/generics.py @@ -31,6 +31,7 @@ from rest_framework import views from awx.api.filters import FieldLookupBackend from awx.main.models import * # noqa from awx.main.utils import * # noqa +from awx.main.utils.db import get_all_field_names from awx.api.serializers import ResourceAccessListElementSerializer from awx.api.versioning import URLPathVersioning, get_request_version from awx.api.metadata import SublistAttachDetatchMetadata @@ -321,8 +322,7 @@ class ListAPIView(generics.ListAPIView, GenericAPIView): return page def get_description_context(self): - opts = self.model._meta - if 'username' in opts.get_all_field_names(): + if 'username' in get_all_field_names(self.model): order_field = 'username' else: order_field = 'name' diff --git a/awx/api/serializers.py b/awx/api/serializers.py index c8d6028844..19fae02fd5 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -477,7 +477,7 @@ class BaseSerializer(serializers.ModelSerializer): return super(BaseSerializer, self).run_validation(data) except ValidationError as exc: # Avoid bug? in DRF if exc.detail happens to be a list instead of a dict. - raise ValidationError(detail=serializers.get_validation_error_detail(exc)) + raise ValidationError(detail=serializers.as_serializer_error(exc)) def get_validation_exclusions(self, obj=None): # Borrowed from DRF 2.x - return model fields that should be excluded diff --git a/awx/api/views.py b/awx/api/views.py index 2cc692e7e7..1328a6571c 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -27,7 +27,6 @@ from django.utils.timezone import now from django.views.decorators.csrf import csrf_exempt from django.views.decorators.cache import never_cache 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 _ @@ -53,7 +52,9 @@ import qsstats import ansiconv # Python Social Auth -from social.backends.utils import load_backends +from social_core.backends.utils import load_backends + +from wsgiref.util import FileWrapper # AWX from awx.main.tasks import send_notifications diff --git a/awx/main/access.py b/awx/main/access.py index aa34551576..5d790cf560 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -54,12 +54,12 @@ def get_object_from_data(field, Model, data, obj=None): # Calling method needs to deal with non-existence of key raise ParseError(_("Required related field %s for permission check." % field)) - if isinstance(raw_value, Model): - return raw_value - elif raw_value is None: - return None - else: - try: + try: + if isinstance(raw_value, Model): + return raw_value + elif raw_value is None: + return None + else: new_pk = int(raw_value) # Avoid database query by comparing pk to model for similarity if obj and new_pk == getattr(obj, '%s_id' % field, None): @@ -67,8 +67,8 @@ def get_object_from_data(field, Model, data, obj=None): else: # Get the new resource from the database return get_object_or_400(Model, pk=new_pk) - except (TypeError, ValueError): - raise ParseError(_("Bad data found in related field %s." % field)) + except (TypeError, ValueError): + raise ParseError(_("Bad data found in related field %s." % field)) class StateConflict(ValidationError): diff --git a/awx/main/fields.py b/awx/main/fields.py index 6a746518d9..19a1f1b78e 100644 --- a/awx/main/fields.py +++ b/awx/main/fields.py @@ -18,12 +18,12 @@ from django.db.models.signals import ( ) from django.db.models.signals import m2m_changed from django.db import models -from django.db.models.fields.related import ( - add_lazy_relation, - SingleRelatedObjectDescriptor, - ReverseSingleRelatedObjectDescriptor, - ManyRelatedObjectsDescriptor, - ReverseManyRelatedObjectsDescriptor, +from django.db.models.fields.related import add_lazy_relation +from django.db.models.fields.related_descriptors import ( + ReverseOneToOneDescriptor, + ForwardManyToOneDescriptor, + ManyToManyDescriptor, + ReverseManyToOneDescriptor, ) from django.utils.encoding import smart_text from django.utils.translation import ugettext_lazy as _ @@ -96,7 +96,7 @@ class JSONBField(upstream_JSONBField): # https://bitbucket.org/offline/django-annoying/src/a0de8b294db3/annoying/fields.py -class AutoSingleRelatedObjectDescriptor(SingleRelatedObjectDescriptor): +class AutoSingleRelatedObjectDescriptor(ReverseOneToOneDescriptor): """Descriptor for access to the object from its related class.""" def __get__(self, instance, instance_type=None): @@ -139,7 +139,7 @@ def resolve_role_field(obj, field): raise Exception(smart_text('{} refers to a {}, not a Role'.format(field, type(obj)))) ret.append(obj.id) else: - if type(obj) is ManyRelatedObjectsDescriptor: + if type(obj) is ManyToManyDescriptor: for o in obj.all(): ret += resolve_role_field(o, field_components[1]) else: @@ -179,7 +179,7 @@ def is_implicit_parent(parent_role, child_role): return False -class ImplicitRoleDescriptor(ReverseSingleRelatedObjectDescriptor): +class ImplicitRoleDescriptor(ForwardManyToOneDescriptor): pass @@ -230,18 +230,18 @@ class ImplicitRoleField(models.ForeignKey): field_name, sep, field_attr = field_name.partition('.') field = getattr(cls, field_name) - if type(field) is ReverseManyRelatedObjectsDescriptor or \ - type(field) is ManyRelatedObjectsDescriptor: + if type(field) is ReverseManyToOneDescriptor or \ + type(field) is ManyToManyDescriptor: if '.' in field_attr: raise Exception('Referencing deep roles through ManyToMany fields is unsupported.') - if type(field) is ReverseManyRelatedObjectsDescriptor: + if type(field) is ReverseManyToOneDescriptor: sender = field.through else: sender = field.related.through - reverse = type(field) is ManyRelatedObjectsDescriptor + reverse = type(field) is ManyToManyDescriptor m2m_changed.connect(self.m2m_update(field_attr, reverse), sender, weak=False) def m2m_update(self, field_attr, _reverse): diff --git a/awx/main/tests/base.py b/awx/main/tests/base.py index 8f8d56936e..14863bda6a 100644 --- a/awx/main/tests/base.py +++ b/awx/main/tests/base.py @@ -132,7 +132,7 @@ class BaseTestMixin(MockCommonlySlowTestMixin): # Set flag so that task chain works with unit tests. settings.CELERY_UNIT_TEST = True settings.SYSTEM_UUID='00000000-0000-0000-0000-000000000000' - settings.BROKER_URL='redis://localhost:55672/' + settings.CELERY_BROKER_URL='redis://localhost:55672/' settings.CALLBACK_QUEUE = 'callback_tasks_unit' # Disable socket notifications for unit tests. diff --git a/awx/main/tests/functional/api/test_rbac_displays.py b/awx/main/tests/functional/api/test_rbac_displays.py index 688d4cf174..de16354d47 100644 --- a/awx/main/tests/functional/api/test_rbac_displays.py +++ b/awx/main/tests/functional/api/test_rbac_displays.py @@ -92,24 +92,21 @@ class TestJobTemplateCopyEdit: credential=None, ask_credential_on_launch=True, name='deploy-job-template' ) - serializer = JobTemplateSerializer(jt_res) - serializer.context = self.fake_context(admin_user) + serializer = JobTemplateSerializer(jt_res, context=self.fake_context(admin_user)) response = serializer.to_representation(jt_res) assert not response['summary_fields']['user_capabilities']['copy'] assert response['summary_fields']['user_capabilities']['edit'] def test_sys_admin_copy_edit(self, jt_copy_edit, admin_user): "Absent a validation error, system admins can do everything" - serializer = JobTemplateSerializer(jt_copy_edit) - serializer.context = self.fake_context(admin_user) + serializer = JobTemplateSerializer(jt_copy_edit, context=self.fake_context(admin_user)) response = serializer.to_representation(jt_copy_edit) assert response['summary_fields']['user_capabilities']['copy'] assert response['summary_fields']['user_capabilities']['edit'] def test_org_admin_copy_edit(self, jt_copy_edit, org_admin): "Organization admins SHOULD be able to copy a JT firmly in their org" - serializer = JobTemplateSerializer(jt_copy_edit) - serializer.context = self.fake_context(org_admin) + serializer = JobTemplateSerializer(jt_copy_edit, context=self.fake_context(org_admin)) response = serializer.to_representation(jt_copy_edit) assert response['summary_fields']['user_capabilities']['copy'] assert response['summary_fields']['user_capabilities']['edit'] @@ -125,8 +122,7 @@ class TestJobTemplateCopyEdit: jt_copy_edit.credential = machine_credential jt_copy_edit.save() - serializer = JobTemplateSerializer(jt_copy_edit) - serializer.context = self.fake_context(org_admin) + serializer = JobTemplateSerializer(jt_copy_edit, context=self.fake_context(org_admin)) response = serializer.to_representation(jt_copy_edit) assert not response['summary_fields']['user_capabilities']['copy'] assert response['summary_fields']['user_capabilities']['edit'] @@ -140,8 +136,7 @@ class TestJobTemplateCopyEdit: jt_copy_edit.admin_role.members.add(rando) jt_copy_edit.save() - serializer = JobTemplateSerializer(jt_copy_edit) - serializer.context = self.fake_context(rando) + serializer = JobTemplateSerializer(jt_copy_edit, context=self.fake_context(rando)) response = serializer.to_representation(jt_copy_edit) assert not response['summary_fields']['user_capabilities']['copy'] assert response['summary_fields']['user_capabilities']['edit'] @@ -155,8 +150,7 @@ class TestJobTemplateCopyEdit: jt_copy_edit.project.admin_role.members.add(rando) jt_copy_edit.project.save() - serializer = JobTemplateSerializer(jt_copy_edit) - serializer.context = self.fake_context(rando) + serializer = JobTemplateSerializer(jt_copy_edit, context=self.fake_context(rando)) response = serializer.to_representation(jt_copy_edit) assert response['summary_fields']['user_capabilities']['copy'] assert response['summary_fields']['user_capabilities']['edit'] diff --git a/awx/main/tests/functional/commands/test_commands.py b/awx/main/tests/functional/commands/test_commands.py index 95cd291cee..0b3b582279 100644 --- a/awx/main/tests/functional/commands/test_commands.py +++ b/awx/main/tests/functional/commands/test_commands.py @@ -39,8 +39,8 @@ def run_command(name, *args, **options): @pytest.mark.parametrize( "username,password,expected,changed", [ - ('admin', 'dingleberry', 'Password updated\n', True), - ('admin', 'admin', 'Password not updated\n', False), + ('admin', 'dingleberry', 'Password updated', True), + ('admin', 'admin', 'Password not updated', False), (None, 'foo', 'username required', False), ('admin', None, 'password required', False), ] diff --git a/awx/main/tests/functional/commands/test_inventory_import.py b/awx/main/tests/functional/commands/test_inventory_import.py index 7c385ad1dd..f8f601a3c8 100644 --- a/awx/main/tests/functional/commands/test_inventory_import.py +++ b/awx/main/tests/functional/commands/test_inventory_import.py @@ -92,7 +92,7 @@ class TestInvalidOptionsFunctional: cmd = inventory_import.Command() with mock.patch('django.db.transaction.rollback'): with pytest.raises(IOError) as err: - cmd.handle_noargs( + cmd.handle( inventory_id=inventory.id, source='/tmp/pytest-of-root/pytest-7/inv_files0-invalid') assert 'Source does not exist' in err.value.message @@ -100,14 +100,14 @@ class TestInvalidOptionsFunctional: def test_invalid_inventory_id(self): cmd = inventory_import.Command() with pytest.raises(CommandError) as err: - cmd.handle_noargs(inventory_id=42, source='/notapath/shouldnotmatter') + cmd.handle(inventory_id=42, source='/notapath/shouldnotmatter') assert 'id = 42' in err.value.message assert 'cannot be found' in err.value.message def test_invalid_inventory_name(self): cmd = inventory_import.Command() with pytest.raises(CommandError) as err: - cmd.handle_noargs(inventory_name='fooservers', source='/notapath/shouldnotmatter') + cmd.handle(inventory_name='fooservers', source='/notapath/shouldnotmatter') assert 'name = fooservers' in err.value.message assert 'cannot be found' in err.value.message @@ -122,7 +122,7 @@ class TestINIImports: @mock.patch.object(inventory_import.AnsibleInventoryLoader, 'load', mock.MagicMock(return_value=TEST_MEM_OBJECTS)) def test_inventory_single_ini_import(self, inventory, capsys): cmd = inventory_import.Command() - r = cmd.handle_noargs( + r = cmd.handle( inventory_id=inventory.pk, source=__file__, method='backport') out, err = capsys.readouterr() @@ -192,7 +192,7 @@ class TestINIImports: ) def test_hostvars_are_saved(self, inventory): cmd = inventory_import.Command() - cmd.handle_noargs(inventory_id=inventory.pk, source='doesnt matter') + cmd.handle(inventory_id=inventory.pk, source='doesnt matter') assert inventory.hosts.count() == 1 h = inventory.hosts.all()[0] assert h.name == 'foo' @@ -219,4 +219,4 @@ class TestINIImports: ) def test_recursive_group_error(self, inventory): cmd = inventory_import.Command() - cmd.handle_noargs(inventory_id=inventory.pk, source='doesnt matter') + cmd.handle(inventory_id=inventory.pk, source='doesnt matter') diff --git a/awx/main/tests/functional/conftest.py b/awx/main/tests/functional/conftest.py index d712d66d26..81c9ab7767 100644 --- a/awx/main/tests/functional/conftest.py +++ b/awx/main/tests/functional/conftest.py @@ -64,7 +64,7 @@ def celery_memory_broker(): Allows django signal code to execute without the need for redis ''' - settings.BROKER_URL='memory://localhost/' + settings.CELERY_BROKER_URL='memory://localhost/' @pytest.fixture diff --git a/awx/main/tests/functional/test_rbac_api.py b/awx/main/tests/functional/test_rbac_api.py index a789abd99f..a390b4c54f 100644 --- a/awx/main/tests/functional/test_rbac_api.py +++ b/awx/main/tests/functional/test_rbac_api.py @@ -38,7 +38,7 @@ def test_get_roles_list_user(organization, inventory, team, get, user): 'Users can see all roles they have access to, but not all roles' this_user = user('user-test_get_roles_list_user') organization.member_role.members.add(this_user) - custom_role = Role.objects.create(name='custom_role-test_get_roles_list_user') + custom_role = Role.objects.create(role_field='custom_role-test_get_roles_list_user') organization.member_role.children.add(custom_role) url = reverse('api:role_list') @@ -128,7 +128,7 @@ def test_user_view_other_user_roles(organization, inventory, team, get, alice, b organization.member_role.members.add(alice) organization.admin_role.members.add(bob) organization.member_role.members.add(bob) - custom_role = Role.objects.create(name='custom_role-test_user_view_admin_roles_list') + custom_role = Role.objects.create(role_field='custom_role-test_user_view_admin_roles_list') organization.member_role.children.add(custom_role) team.member_role.members.add(bob) diff --git a/awx/main/tests/unit/api/serializers/test_workflow_serializers.py b/awx/main/tests/unit/api/serializers/test_workflow_serializers.py index 55f2015b86..7ac067c93a 100644 --- a/awx/main/tests/unit/api/serializers/test_workflow_serializers.py +++ b/awx/main/tests/unit/api/serializers/test_workflow_serializers.py @@ -125,11 +125,15 @@ class TestWorkflowJobTemplateNodeSerializerCharPrompts(): serializer = WorkflowJobTemplateNodeSerializer() node = WorkflowJobTemplateNode(pk=1) node.char_prompts = {'limit': 'webservers'} - serializer.instance = node + view = FakeView(node) view.request = FakeRequest() view.request.method = "PATCH" - serializer.context = {'view': view} + + serializer = WorkflowJobTemplateNodeSerializer() + serializer = WorkflowJobTemplateNodeSerializer(context={'view':view}) + serializer.instance = node + return serializer def test_change_single_field(self, WFJT_serializer): diff --git a/awx/main/tests/unit/api/test_views.py b/awx/main/tests/unit/api/test_views.py index edebdf5525..8014e9c4a6 100644 --- a/awx/main/tests/unit/api/test_views.py +++ b/awx/main/tests/unit/api/test_views.py @@ -9,7 +9,6 @@ from awx.api.views import ( JobTemplateLabelList, JobTemplateSurveySpec, InventoryInventorySourcesUpdate, - InventoryHostsList, HostInsights, ) @@ -17,8 +16,6 @@ from awx.main.models import ( Host, ) -from awx.main.managers import HostManager - @pytest.fixture def mock_response_new(mocker): @@ -223,17 +220,3 @@ class TestHostInsights(): assert resp.data['error'] == 'The Insights Credential for "inventory_name_here" was not found.' assert resp.status_code == 404 - - -class TestInventoryHostsList(object): - - def test_host_list_smart_inventory(self, mocker): - Inventory = namedtuple('Inventory', ['kind', 'host_filter', 'hosts', 'organization_id']) - obj = Inventory(kind='smart', host_filter='localhost', hosts=HostManager(), organization_id=None) - obj.hosts.instance = obj - - with mock.patch.object(InventoryHostsList, 'get_parent_object', return_value=obj): - with mock.patch('awx.main.utils.filters.SmartFilter.query_from_string') as mock_query: - view = InventoryHostsList() - view.get_queryset() - mock_query.assert_called_once_with('localhost') diff --git a/awx/main/tests/unit/commands/test_inventory_import.py b/awx/main/tests/unit/commands/test_inventory_import.py index 21b7fe391d..8bbe219011 100644 --- a/awx/main/tests/unit/commands/test_inventory_import.py +++ b/awx/main/tests/unit/commands/test_inventory_import.py @@ -19,7 +19,7 @@ class TestInvalidOptions: def test_invalid_options_no_options_specified(self): cmd = Command() with pytest.raises(CommandError) as err: - cmd.handle_noargs() + cmd.handle() assert 'inventory-id' in err.value.message assert 'required' in err.value.message @@ -27,7 +27,7 @@ class TestInvalidOptions: # You can not specify both name and if of the inventory cmd = Command() with pytest.raises(CommandError) as err: - cmd.handle_noargs( + cmd.handle( inventory_id=42, inventory_name='my-inventory' ) assert 'inventory-id' in err.value.message @@ -37,7 +37,7 @@ class TestInvalidOptions: # You can't overwrite and keep_vars at the same time, that wouldn't make sense cmd = Command() with pytest.raises(CommandError) as err: - cmd.handle_noargs( + cmd.handle( inventory_id=42, overwrite=True, keep_vars=True ) assert 'overwrite-vars' in err.value.message @@ -47,13 +47,13 @@ class TestInvalidOptions: # Need a source to import cmd = Command() with pytest.raises(CommandError) as err: - cmd.handle_noargs( + cmd.handle( inventory_id=42, overwrite=True, keep_vars=True ) assert 'overwrite-vars' in err.value.message assert 'exclusive' in err.value.message with pytest.raises(CommandError) as err: - cmd.handle_noargs( + cmd.handle( inventory_id=42, overwrite_vars=True, keep_vars=True ) assert 'overwrite-vars' in err.value.message @@ -62,7 +62,7 @@ class TestInvalidOptions: def test_invalid_options_missing_source(self): cmd = Command() with pytest.raises(CommandError) as err: - cmd.handle_noargs(inventory_id=42) + cmd.handle(inventory_id=42) assert '--source' in err.value.message assert 'required' in err.value.message diff --git a/awx/main/tests/unit/models/test_rbac_unit.py b/awx/main/tests/unit/models/test_rbac_unit.py deleted file mode 100644 index 24d9a657ba..0000000000 --- a/awx/main/tests/unit/models/test_rbac_unit.py +++ /dev/null @@ -1,103 +0,0 @@ -import pytest -import mock - -from django.contrib.contenttypes.models import ContentType - -from awx.main.models.rbac import ( - Role, - ROLE_SINGLETON_SYSTEM_ADMINISTRATOR, - ROLE_SINGLETON_SYSTEM_AUDITOR -) -from awx.main.models import Organization, JobTemplate, Project - -from awx.main.fields import ( - ImplicitRoleField, - is_implicit_parent -) - - -def apply_fake_roles(obj): - ''' - Creates an un-saved role for all the implicit role fields on an object - ''' - for fd in obj._meta.fields: - if not isinstance(fd, ImplicitRoleField): - continue - r = Role(role_field=fd.name) - setattr(obj, fd.name, r) - with mock.patch('django.contrib.contenttypes.fields.GenericForeignKey.get_content_type') as mck_ct: - mck_ct.return_value = ContentType(model=obj._meta.model_name) - r.content_object = obj - - -@pytest.fixture -def system_administrator(): - return Role( - role_field=ROLE_SINGLETON_SYSTEM_ADMINISTRATOR, - singleton_name=ROLE_SINGLETON_SYSTEM_ADMINISTRATOR - ) - - -@pytest.fixture -def system_auditor(): - return Role( - role_field=ROLE_SINGLETON_SYSTEM_AUDITOR, - singleton_name=ROLE_SINGLETON_SYSTEM_AUDITOR - ) - - -@pytest.fixture -def organization(): - o = Organization(name='unit-test-org') - apply_fake_roles(o) - return o - - -@pytest.fixture -def project(organization): - p = Project(name='unit-test-proj', organization=organization) - apply_fake_roles(p) - return p - - -@pytest.fixture -def job_template(project): - jt = JobTemplate(name='unit-test-jt', project=project) - apply_fake_roles(jt) - return jt - - -class TestIsImplicitParent: - ''' - Tests to confirm that `is_implicit_parent` gives the right answers - ''' - def test_sys_admin_implicit_parent(self, organization, system_administrator): - assert is_implicit_parent( - parent_role=system_administrator, - child_role=organization.admin_role - ) - - - def test_admin_is_parent_of_member_role(self, organization): - assert is_implicit_parent( - parent_role=organization.admin_role, - child_role=organization.member_role - ) - - def test_member_is_not_parent_of_admin_role(self, organization): - assert not is_implicit_parent( - parent_role=organization.member_role, - child_role=organization.admin_role - ) - - def test_second_level_implicit_parent_role(self, job_template, organization): - assert is_implicit_parent( - parent_role=organization.admin_role, - child_role=job_template.admin_role - ) - - def test_second_level_is_not_an_implicit_parent_role(self, job_template, organization): - assert not is_implicit_parent( - parent_role=organization.member_role, - child_role=job_template.admin_role - ) diff --git a/awx/main/tests/unit/settings/test_defaults.py b/awx/main/tests/unit/settings/test_defaults.py index 52eeb56425..3cc0ed46f6 100644 --- a/awx/main/tests/unit/settings/test_defaults.py +++ b/awx/main/tests/unit/settings/test_defaults.py @@ -8,11 +8,11 @@ from datetime import timedelta ('admin_checks', 'awx.main.tasks.run_administrative_checks'), ('tower_scheduler', 'awx.main.tasks.awx_periodic_scheduler'), ]) -def test_CELERYBEAT_SCHEDULE(mocker, job_name, function_path): - assert job_name in settings.CELERYBEAT_SCHEDULE - assert 'schedule' in settings.CELERYBEAT_SCHEDULE[job_name] - assert type(settings.CELERYBEAT_SCHEDULE[job_name]['schedule']) is timedelta - assert settings.CELERYBEAT_SCHEDULE[job_name]['task'] == function_path +def test_CELERY_BEAT_SCHEDULE(mocker, job_name, function_path): + assert job_name in settings.CELERY_BEAT_SCHEDULE + assert 'schedule' in settings.CELERY_BEAT_SCHEDULE[job_name] + assert type(settings.CELERY_BEAT_SCHEDULE[job_name]['schedule']) is timedelta + assert settings.CELERY_BEAT_SCHEDULE[job_name]['task'] == function_path # Ensures that the function exists mocker.patch(function_path) diff --git a/awx/main/tests/unit/test_access.py b/awx/main/tests/unit/test_access.py index 61660afeb0..961a1d4039 100644 --- a/awx/main/tests/unit/test_access.py +++ b/awx/main/tests/unit/test_access.py @@ -16,6 +16,7 @@ from awx.main.access import ( from awx.conf.license import LicenseForbids from awx.main.models import ( Credential, + CredentialType, Inventory, Project, Role, @@ -57,7 +58,7 @@ class TestRelatedFieldAccess: def test_new_with_bad_data(self, access, mocker): data = {'related': 3.1415} with pytest.raises(ParseError): - access.check_related('related', mocker.MagicMock, data) + access.check_related('related', mocker.MagicMock(), data) def test_new_mandatory_fail(self, access, mocker): access.user.is_superuser = False @@ -118,10 +119,18 @@ class TestRelatedFieldAccess: @pytest.fixture def job_template_with_ids(job_template_factory): # Create non-persisted objects with IDs to send to job_template_factory - credential = Credential(id=1, pk=1, name='testcred', kind='ssh') - net_cred = Credential(id=2, pk=2, name='testnetcred', kind='net') - cloud_cred = Credential(id=3, pk=3, name='testcloudcred', kind='aws') - vault_cred = Credential(id=4, pk=4, name='testnetcred', kind='vault') + ssh_type = CredentialType(kind='ssh') + credential = Credential(id=1, pk=1, name='testcred', credential_type=ssh_type) + + net_type = CredentialType(kind='net') + net_cred = Credential(id=2, pk=2, name='testnetcred', credential_type=net_type) + + cloud_type = CredentialType(kind='aws') + cloud_cred = Credential(id=3, pk=3, name='testcloudcred', credential_type=cloud_type) + + vault_type = CredentialType(kind='vault') + vault_cred = Credential(id=4, pk=4, name='testnetcred', credential_type=vault_type) + inv = Inventory(id=11, pk=11, name='testinv') proj = Project(id=14, pk=14, name='testproj') diff --git a/awx/main/utils/common.py b/awx/main/utils/common.py index 449f32bcf8..8512541cd3 100644 --- a/awx/main/utils/common.py +++ b/awx/main/utils/common.py @@ -450,7 +450,7 @@ def copy_model_by_class(obj1, Class2, fields, kwargs): elif not isinstance(Class2._meta.get_field(field_name), (ForeignObjectRel, ManyToManyField)): create_kwargs[field_name] = kwargs[field_name] elif hasattr(obj1, field_name): - field_obj = obj1._meta.get_field_by_name(field_name)[0] + field_obj = obj1._meta.get_field(field_name) if not isinstance(field_obj, ManyToManyField): create_kwargs[field_name] = getattr(obj1, field_name) @@ -471,7 +471,7 @@ def copy_m2m_relationships(obj1, obj2, fields, kwargs=None): ''' for field_name in fields: if hasattr(obj1, field_name): - field_obj = obj1._meta.get_field_by_name(field_name)[0] + field_obj = obj1._meta.get_field(field_name) if isinstance(field_obj, ManyToManyField): # Many to Many can be specified as field_name src_field_value = getattr(obj1, field_name) diff --git a/awx/main/utils/db.py b/awx/main/utils/db.py index f9c625a7a1..bd2f5db69b 100644 --- a/awx/main/utils/db.py +++ b/awx/main/utils/db.py @@ -6,6 +6,7 @@ from django.db.migrations.loader import MigrationLoader from django.db import connection # Python +from itertools import chain import re @@ -20,3 +21,15 @@ def get_tower_migration_version(): if migration_version > v: v = migration_version return v + + +def get_all_field_names(model): + # Implements compatibility with _meta.get_all_field_names + # See: https://docs.djangoproject.com/en/1.11/ref/models/meta/#migrating-from-the-old-api + return list(set(chain.from_iterable( + (field.name, field.attname) if hasattr(field, 'attname') else (field.name,) + for field in model._meta.get_fields() + # For complete backwards compatibility, you may want to exclude + # GenericForeignKey from the results. + if not (field.many_to_one and field.related_model is None) + ))) diff --git a/awx/templates/rest_framework/base.html b/awx/templates/rest_framework/base.html index 7ae5cb8c1a..cbd761cf33 100644 --- a/awx/templates/rest_framework/base.html +++ b/awx/templates/rest_framework/base.html @@ -1,270 +1,295 @@ - {# Copy of base.html from rest_framework with minor AWX change. #} {% load staticfiles %} -{% load rest_framework %} {% load i18n %} +{% load rest_framework %} + + - - {% block head %} + + {% block head %} - {% block meta %} - - - {% endblock %} - - {% block title %}Django REST framework{% endblock %} - - {% block style %} - {% block bootstrap_theme %} - - + {% block meta %} + + {% endblock %} - - - {% endblock %} + {% block title %}{% if name %}{{ name }} – {% endif %}Django REST framework{% endblock %} - {% endblock %} - + {% block style %} + {% block bootstrap_theme %} + + + {% endblock %} -{% block body %} - - -
- {% block navbar %} - - {% endblock %} - -
- {% block breadcrumbs %} - + + {% endblock %} - -
+ {% endblock %} + - {% if 'GET' in allowed_methods %} -
-
- {% if api_settings.URL_FORMAT_OVERRIDE %} -
- GET + {% block body %} + - - -
- {% else %} - GET - {% endif %} -
-
- {% endif %} - - {% if options_form %} -
- -
- {% endif %} - - {% if delete_form %} -
- -
- {% endif %} - - {% if filter_form %} - - {% endif %} - -
- -
- {% block description %} - {{ description }} +
+ {% block navbar %} + +
+ {% endblock %} - {% if paginator %} - - {% endif %} +
+ {% block breadcrumbs %} + + {% endblock %} -
-
{{ request.method }} {{ request.get_full_path }}
-
+ +
+ {% block content %} -
-
HTTP {{ response.status_code }} {{ response.status_text }}{% autoescape off %}
-{% for key, val in response_headers.items %}{{ key }}: {{ val|break_long_headers|urlize_quoted_links }}
-{% endfor %}
-{# Original line below had the side effect of also escaping content: #}
-{#   {{ content|urlize_quoted_links }}
{% endautoescape %} #} -{# For Ansible Tower, disable automatic URL creation and move content outside of autoescape off block. #} -{% endautoescape %}{{ content }} +
+ {% if 'GET' in allowed_methods %} +
+
+ {% if api_settings.URL_FORMAT_OVERRIDE %} +
+ GET + + + +
+ {% else %} + GET + {% endif %} +
+
+ {% endif %} + + {% if options_form %} +
+ +
+ {% endif %} + + {% if delete_form %} + + + + + {% endif %} + + {% if filter_form %} + + {% endif %}
- {% if display_edit_forms %} +
+ +
+ {% block description %} + {{ description }} + {% endblock %} +
- {% if post_form or raw_data_post_form %} -
- {% if post_form %} - - {% endif %} + {% if paginator %} + + {% endif %} -
+
+
{{ request.method }} {{ request.get_full_path }}
+
+ +
+
HTTP {{ response.status_code }} {{ response.status_text }}{% autoescape off %}{% for key, val in response_headers|items %}
+{{ key }}: {{ val|break_long_headers|urlize_quoted_links }}{% endfor %}
+{# Original line below had the side effect of also escaping content: #}
+{#   {{ content|urlize_quoted_links }}
{% endautoescape %} #} +{# For AWX, disable automatic URL creation and move content outside of autoescape off block. #} +{% endautoescape %}{{ content }} +
+
+ + {% if display_edit_forms %} + {% if post_form or raw_data_post_form %} +
{% if post_form %} -
- {% with form=post_form %} -
+ + {% endif %} + +
+ {% if post_form %} +
+ {% with form=post_form %} + +
+ {% csrf_token %} + {{ post_form }} +
+ +
+
+ + {% endwith %} +
+ {% endif %} + +
+ {% with form=raw_data_post_form %} +
- {% csrf_token %} - {{ post_form }} + {% include "rest_framework/raw_data_form.html" %}
- +
{% endwith %}
- {% endif %} - -
- {% with form=raw_data_post_form %} -
-
- {% include "rest_framework/raw_data_form.html" %} -
- -
-
-
- {% endwith %}
-
- {% endif %} + {% endif %} - {% if put_form or raw_data_put_form or raw_data_patch_form %} -
- {% if put_form %} - - {% endif %} - -
+ {% if put_form or raw_data_put_form or raw_data_patch_form %} +
{% if put_form %} -
-
-
- {{ put_form }} -
- -
-
-
-
+ {% endif %} -
- {% with form=raw_data_put_or_patch_form %} -
-
- {% include "rest_framework/raw_data_form.html" %} -
- {% if raw_data_put_form %} - - {% endif %} - {% if raw_data_patch_form %} - +
+ {% if put_form %} +
+ +
+ {{ put_form }} +
+ +
+
+ +
+ {% endif %} + +
+ {% with form=raw_data_put_or_patch_form %} +
+
+ {% include "rest_framework/raw_data_form.html" %} +
+ {% if raw_data_put_form %} + {% endif %} -
-
-
- {% endwith %} + {% if raw_data_patch_form %} + + {% endif %} +
+
+ + {% endwith %} +
-
+ {% endif %} {% endif %} - {% endif %} -
-
- {# div#push added for Ansible Tower. #} -
-
+ {% endblock content %} +
+
+
- {% block script %} - - - - - - - + + + + + + + + + {% endblock %} + + {% endblock %} - - {% if filter_form %} - {{ filter_form }} - {% endif %} - - -{% endblock %}