Skip to content

Commit

Permalink
Update Django to 1.8 and DRF to 3.3, add new Django migrations, updat…
Browse files Browse the repository at this point in the history
…e serializers/pagination/metadata, update browsable API styling.
  • Loading branch information
cchurch committed Feb 2, 2016
1 parent 6242df1 commit 60224cd
Show file tree
Hide file tree
Showing 140 changed files with 2,690 additions and 1,371 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,15 @@ npm-debug.log
/DEBUG

# Testing
.cache
.coverage
.tox
coverage.xml
htmlcov
pep8.txt
scratch
testem.log
awx/awx_test.sqlite3-journal

# Mac OS X
*.DS_Store
Expand Down
10 changes: 3 additions & 7 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ endif

.PHONY: clean rebase push requirements requirements_dev requirements_jenkins \
real-requirements real-requirements_dev real-requirements_jenkins \
develop refresh adduser syncdb migrate dbchange dbshell runserver celeryd \
develop refresh adduser migrate dbchange dbshell runserver celeryd \
receiver test test_unit test_coverage coverage_html test_jenkins dev_build \
release_build release_clean sdist rpmtar mock-rpm mock-srpm rpm-sign \
build-ui sync-ui test-ui build-ui-for-coverage test-ui-for-coverage \
Expand Down Expand Up @@ -280,13 +280,9 @@ refresh: clean requirements_dev version_file develop migrate
adduser:
$(PYTHON) manage.py createsuperuser

# Create initial database tables (excluding migrations).
syncdb:
$(PYTHON) manage.py syncdb --noinput

# Create database tables and apply any new migrations.
migrate: syncdb
$(PYTHON) manage.py migrate --noinput
migrate:
$(PYTHON) manage.py migrate --noinput --fake-initial

# Run after making changes to the models to create a new migration.
dbchange:
Expand Down
20 changes: 18 additions & 2 deletions awx/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,26 @@ def prepare_env():
settings.DATABASES['default'][opt] = os.environ['AWX_TEST_DATABASE_%s' % opt]
# Disable capturing all SQL queries in memory when in DEBUG mode.
if settings.DEBUG and not getattr(settings, 'SQL_DEBUG', True):
from django.db.backends import BaseDatabaseWrapper
from django.db.backends.util import CursorWrapper
from django.db.backends.base.base import BaseDatabaseWrapper
from django.db.backends.utils import CursorWrapper
BaseDatabaseWrapper.make_debug_cursor = lambda self, cursor: CursorWrapper(cursor, self)

# Use the default devserver addr/port defined in settings for runserver.
default_addr = getattr(settings, 'DEVSERVER_DEFAULT_ADDR', '127.0.0.1')
default_port = getattr(settings, 'DEVSERVER_DEFAULT_PORT', 8000)
from django.core.management.commands import runserver as core_runserver
original_handle = core_runserver.Command.handle

def handle(self, *args, **options):
if not options.get('addrport'):
options['addrport'] = '%s:%d' % (default_addr, int(default_port))
elif options.get('addrport').isdigit():
options['addrport'] = '%s:%d' % (default_addr, int(options['addrport']))
return original_handle(self, *args, **options)

core_runserver.Command.handle = handle


def manage():
# Prepare the AWX environment.
prepare_env()
Expand Down
25 changes: 12 additions & 13 deletions awx/api/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@
from django.core.exceptions import FieldError, ValidationError
from django.db import models
from django.db.models import Q
from django.db.models.related import RelatedObject
from django.db.models.fields import FieldDoesNotExist
from django.db.models.fields.related import ForeignObjectRel
from django.contrib.contenttypes.models import ContentType
from django.utils.encoding import force_text

# Django REST Framework
from rest_framework.exceptions import ParseError
Expand Down Expand Up @@ -46,7 +47,7 @@ class TypeFilterBackend(BaseFilterBackend):
def filter_queryset(self, request, queryset, view):
try:
types = None
for key, value in request.QUERY_PARAMS.items():
for key, value in request.query_params.items():
if key == 'type':
if ',' in value:
types = value.split(',')
Expand Down Expand Up @@ -107,23 +108,21 @@ def get_field_from_lookup(self, model, lookup):
'last_updated': 'last_job_run',
}.get(name, name)

new_parts.append(name)

if name == 'pk':
field = model._meta.pk
else:
field = model._meta.get_field_by_name(name)[0]
if n < (len(parts) - 2):
if getattr(field, 'rel', None):
model = field.rel.to
else:
model = field.model
new_parts.append(name)
model = getattr(field, 'related_model', None) or field.model

if parts:
new_parts.append(parts[-1])
new_lookup = '__'.join(new_parts)
return field, new_lookup

def to_python_related(self, value):
value = unicode(value)
value = force_text(value)
if value.lower() in ('none', 'null'):
return None
else:
Expand All @@ -134,7 +133,7 @@ def value_to_python_for_field(self, field, value):
return to_python_boolean(value, allow_none=True)
elif isinstance(field, models.BooleanField):
return to_python_boolean(value)
elif isinstance(field, RelatedObject):
elif isinstance(field, ForeignObjectRel):
return self.to_python_related(value)
else:
return field.to_python(value)
Expand All @@ -159,12 +158,12 @@ def value_to_python(self, model, lookup, value):

def filter_queryset(self, request, queryset, view):
try:
# Apply filters specified via QUERY_PARAMS. Each entry in the lists
# Apply filters specified via query_params. Each entry in the lists
# below is (negate, field, value).
and_filters = []
or_filters = []
chain_filters = []
for key, values in request.QUERY_PARAMS.lists():
for key, values in request.query_params.lists():
if key in self.RESERVED_NAMES:
continue

Expand Down Expand Up @@ -246,7 +245,7 @@ class OrderByBackend(BaseFilterBackend):
def filter_queryset(self, request, queryset, view):
try:
order_by = None
for key, value in request.QUERY_PARAMS.items():
for key, value in request.query_params.items():
if key in ('order', 'order_by'):
order_by = value
if ',' in value:
Expand Down
109 changes: 21 additions & 88 deletions awx/api/generics.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@
from django.db import connection
from django.shortcuts import get_object_or_404
from django.template.loader import render_to_string
from django.utils.encoding import smart_text
from django.utils.safestring import mark_safe

# Django REST Framework
from rest_framework.authentication import get_authorization_header
from rest_framework.exceptions import PermissionDenied
from rest_framework import generics
from rest_framework.response import Response
from rest_framework.request import clone_request
from rest_framework import status
from rest_framework import views

Expand Down Expand Up @@ -155,18 +155,6 @@ def get_description(self, html=False):
context = self.get_description_context()
return render_to_string(template_list, context)

def metadata(self, request):
'''
Add version number where view was added to Tower.
'''
ret = super(APIView, self).metadata(request)
added_in_version = '1.2'
for version in ('3.0.0', '2.4.0', '2.3.0', '2.2.0', '2.1.0', '2.0.0', '1.4.8', '1.4.5', '1.4', '1.3'):
if getattr(self, 'new_in_%s' % version.replace('.', ''), False):
added_in_version = version
break
ret['added_in_version'] = added_in_version
return ret

class GenericAPIView(generics.GenericAPIView, APIView):
# Base class for all model-based views.
Expand All @@ -188,8 +176,12 @@ def get_serializer(self, *args, **kwargs):
def get_queryset(self):
#if hasattr(self.request.user, 'get_queryset'):
# return self.request.user.get_queryset(self.model)
#else:
return super(GenericAPIView, self).get_queryset()
if self.queryset is not None:
return self.queryset._clone()
elif self.model is not None:
return self.model._default_manager.all()
else:
return super(GenericAPIView, self).get_queryset()

def get_description_context(self):
# Set instance attributes needed to get serializer metadata.
Expand All @@ -201,69 +193,13 @@ def get_description_context(self):
if hasattr(self.model, "_meta"):
if hasattr(self.model._meta, "verbose_name"):
d.update({
'model_verbose_name': unicode(self.model._meta.verbose_name),
'model_verbose_name_plural': unicode(self.model._meta.verbose_name_plural),
'model_verbose_name': smart_text(self.model._meta.verbose_name),
'model_verbose_name_plural': smart_text(self.model._meta.verbose_name_plural),
})
d.update({'serializer_fields': self.get_serializer().metadata()})
d['serializer_fields'] = self.metadata_class().get_serializer_info(self.get_serializer())
d['settings'] = settings
return d

def metadata(self, request):
'''
Add field information for GET requests (so field names/labels are
available even when we can't POST/PUT).
'''
ret = super(GenericAPIView, self).metadata(request)
actions = ret.get('actions', {})
# Remove read only fields from PUT/POST data.
for method in ('POST', 'PUT'):
fields = actions.get(method, {})
for field, meta in fields.items():
if not isinstance(meta, dict):
continue
if meta.pop('read_only', False):
fields.pop(field)
if 'GET' in self.allowed_methods:
cloned_request = clone_request(request, 'GET')
try:
# Test global permissions
self.check_permissions(cloned_request)
# Test object permissions
if hasattr(self, 'retrieve'):
try:
self.get_object()
except Http404:
# Http404 should be acceptable and the serializer
# metadata should be populated. Except this so the
# outer "else" clause of the try-except-else block
# will be executed.
pass
except (exceptions.APIException, PermissionDenied):
pass
else:
# If user has appropriate permissions for the view, include
# appropriate metadata about the fields that should be supplied.
serializer = self.get_serializer()
actions['GET'] = serializer.metadata()
if hasattr(serializer, 'get_types'):
ret['types'] = serializer.get_types()
# Remove fields labeled as write_only, remove field attributes
# that aren't relevant for retrieving data.
for field, meta in actions['GET'].items():
if not isinstance(meta, dict):
continue
meta.pop('required', None)
meta.pop('read_only', None)
meta.pop('default', None)
meta.pop('min_length', None)
meta.pop('max_length', None)
if meta.pop('write_only', False):
actions['GET'].pop(field)
if actions:
ret['actions'] = actions
if getattr(self, 'search_fields', None):
ret['search_fields'] = self.search_fields
return ret

class MongoAPIView(GenericAPIView):

Expand Down Expand Up @@ -337,8 +273,8 @@ class SubListAPIView(ListAPIView):
def get_description_context(self):
d = super(SubListAPIView, self).get_description_context()
d.update({
'parent_model_verbose_name': unicode(self.parent_model._meta.verbose_name),
'parent_model_verbose_name_plural': unicode(self.parent_model._meta.verbose_name_plural),
'parent_model_verbose_name': smart_text(self.parent_model._meta.verbose_name),
'parent_model_verbose_name_plural': smart_text(self.parent_model._meta.verbose_name_plural),
})
return d

Expand Down Expand Up @@ -388,10 +324,10 @@ def create(self, request, *args, **kwargs):

# Make a copy of the data provided (since it's readonly) in order to
# inject additional data.
if hasattr(request.DATA, 'dict'):
data = request.DATA.dict()
if hasattr(request.data, 'dict'):
data = request.data.dict()
else:
data = request.DATA
data = request.data

# add the parent key to the post data using the pk from the URL
parent_key = getattr(self, 'parent_key', None)
Expand All @@ -405,7 +341,7 @@ def create(self, request, *args, **kwargs):
status=status.HTTP_400_BAD_REQUEST)

# Verify we have permission to add the object as given.
if not request.user.can_access(self.model, 'add', serializer.init_data):
if not request.user.can_access(self.model, 'add', serializer.initial_data):
raise PermissionDenied()

# save the object through the serializer, reload and returned the saved
Expand All @@ -424,8 +360,8 @@ def attach(self, request, *args, **kwargs):
created = False
parent = self.get_parent_object()
relationship = getattr(parent, self.relationship)
sub_id = request.DATA.get('id', None)
data = request.DATA
sub_id = request.data.get('id', None)
data = request.data

# Create the sub object if an ID is not provided.
if not sub_id:
Expand Down Expand Up @@ -462,7 +398,7 @@ def attach(self, request, *args, **kwargs):
return Response(status=status.HTTP_204_NO_CONTENT)

def unattach(self, request, *args, **kwargs):
sub_id = request.DATA.get('id', None)
sub_id = request.data.get('id', None)
if not sub_id:
data = dict(msg='"id" is required to disassociate')
return Response(data, status=status.HTTP_400_BAD_REQUEST)
Expand All @@ -486,10 +422,10 @@ def unattach(self, request, *args, **kwargs):
return Response(status=status.HTTP_204_NO_CONTENT)

def post(self, request, *args, **kwargs):
if not isinstance(request.DATA, dict):
if not isinstance(request.data, dict):
return Response('invalid type for post data',
status=status.HTTP_400_BAD_REQUEST)
if 'disassociate' in request.DATA:
if 'disassociate' in request.data:
return self.unattach(request, *args, **kwargs)
else:
return self.attach(request, *args, **kwargs)
Expand All @@ -499,9 +435,6 @@ class RetrieveAPIView(generics.RetrieveAPIView, GenericAPIView):

class RetrieveUpdateAPIView(RetrieveAPIView, generics.RetrieveUpdateAPIView):

def pre_save(self, obj):
super(RetrieveUpdateAPIView, self).pre_save(obj)

def update(self, request, *args, **kwargs):
self.update_filter(request, *args, **kwargs)
return super(RetrieveUpdateAPIView, self).update(request, *args, **kwargs)
Expand Down
Loading

0 comments on commit 60224cd

Please sign in to comment.