Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Return 400 error when request body has malformed syntax. (based on PR #649 by piotrbulinski) #1375

Open
wants to merge 2 commits into
base: v0.13.0-unstable
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
14 changes: 0 additions & 14 deletions .travis.yml
Expand Up @@ -6,14 +6,11 @@ python:
- "2.7"
- "3.3"
- "3.4"
- "2.6"

env:
- MODE=flake8
- MODE=flake8-strict
- MODE=docs
- DJANGO_VERSION=1.5
- DJANGO_VERSION=1.6
- DJANGO_VERSION=1.7
- DJANGO_VERSION=1.8
- DJANGO_VERSION=dev
Expand All @@ -23,28 +20,17 @@ matrix:
- env: DJANGO_VERSION=dev
- env: MODE=flake8-strict
exclude:
- python: "2.6"
env: MODE=flake8
- python: "3.3"
env: MODE=flake8
- python: "3.4"
env: MODE=flake8
- python: "2.6"
env: MODE=flake8-strict
- python: "3.3"
env: MODE=flake8-strict
- python: "3.4"
env: MODE=flake8-strict
- python: "2.6"
env: MODE=docs
- python: "3.3"
env: MODE=docs
- env: DJANGO_VERSION=dev
python: "2.6"
- env: DJANGO_VERSION=1.8
python: "2.6"
- env: DJANGO_VERSION=1.7
python: "2.6"

addons:
apt:
Expand Down
2 changes: 1 addition & 1 deletion docs/compatibility_notes.rst
Expand Up @@ -19,7 +19,7 @@ updated & a migration was added (``0002_add_apikey_index.py``). However, due
to the way MySQL works & the way Django generates index names, this migration
would fail miserably on many MySQL installs.

If you are using MySQL, South & the ``ApiKey`` authentication class, you should
If you are using MySQL & the ``ApiKey`` authentication class, you may need to
manually add an index for the the ``ApiKey.key`` field. Something to the effect
of::

Expand Down
15 changes: 0 additions & 15 deletions docs/debugging.rst
Expand Up @@ -70,18 +70,3 @@ You can do this over an entire collection as well::
curl -H 'Content-Type: application/json' -X PUT --data @- "http://localhost:8000/api/v1/entry/"

.. _Requests: http://python-requests.org


"Why is my syncdb with superuser failing with a DatabaseError?"
===============================================================

More specifically, this specific ``DatabaseError``::

django.db.utils.DatabaseError: no such table: tastypie_apikey

This is a side effect of the (disabled by default) ``create_api_key`` signal
as described in the :ref:`authentication` section of the
documentation when used in conjunction with South.

To work around this issue, you can disable the ``create_api_key`` signal
until you have completed running ``syncdb --migrate`` for the first time.
4 changes: 2 additions & 2 deletions docs/geodjango.rst
Expand Up @@ -6,7 +6,7 @@ GeoDjango

Tastypie features support for GeoDjango! Resources return and accept
`GeoJSON <http://geojson.org/geojson-spec.html>`_ (or similarly-formatted
analogs for other formats) and all `spatial lookup <https://docs.djangoproject.com/en/1.3/ref/contrib/gis/geoquerysets/#spatial-lookups>`_ filters are supported. Distance lookups are not yet supported.
analogs for other formats) and all `spatial lookup <https://docs.djangoproject.com/en/dev/ref/contrib/gis/geoquerysets/#spatial-lookups>`_ filters are supported. Distance lookups are not yet supported.

Usage
=====
Expand Down Expand Up @@ -61,7 +61,7 @@ GeoJSON analog for your perferred format.
Filtering
---------

We can filter using any standard GeoDjango `spatial lookup <https://docs.djangoproject.com/en/1.3/ref/contrib/gis/geoquerysets/#spatial-lookups>`_ filter. Simply provide a GeoJSON (or the analog) as a ``GET`` parameter value.
We can filter using any standard GeoDjango `spatial lookup <https://docs.djangoproject.com/en/dev/ref/contrib/gis/geoquerysets/#spatial-lookups>`_ filter. Simply provide a GeoJSON (or the analog) as a ``GET`` parameter value.

Let's find all of our ``GeoNote`` resources that contain a point inside
of `Golden Gate Park <https://sf.localwiki.org/Golden_Gate_Park>`_::
Expand Down
2 changes: 1 addition & 1 deletion docs/index.rst
Expand Up @@ -97,7 +97,7 @@ Required
--------

* Python 2.6+ or Python 3.3+
* Django 1.5+
* Django 1.7+
* dateutil (http://labix.org/python-dateutil) >= 2.1

Optional
Expand Down
2 changes: 1 addition & 1 deletion docs/python3.rst
Expand Up @@ -14,7 +14,7 @@ to run your existing software without modification.
All tests pass under both Python 2 & 3.

.. _`six`: http://pythonhosted.org/six/
.. _`Django`: https://docs.djangoproject.com/en/1.5/topics/python3/#str-and-unicode-methods
.. _`Django`: https://docs.djangoproject.com/en/dev/topics/python3/#str-and-unicode-methods


Incompatibilities
Expand Down
2 changes: 1 addition & 1 deletion docs/tutorial.rst
Expand Up @@ -60,7 +60,7 @@ your project or ``PYTHONPATH``.
1. Download the dependencies:

* Python 2.6+ or Python 3.3+
* Django 1.5+
* Django 1.7+
* ``python-mimeparse`` 0.1.4+ (http://pypi.python.org/pypi/python-mimeparse)
* ``dateutil`` (http://labix.org/python-dateutil)
* **OPTIONAL** - ``lxml`` (http://lxml.de/) and ``defusedxml`` (https://pypi.python.org/pypi/defusedxml) if using the XML serializer
Expand Down
1 change: 0 additions & 1 deletion setup.py
Expand Up @@ -21,7 +21,6 @@
'tastypie.utils',
'tastypie.management',
'tastypie.management.commands',
'tastypie.south_migrations',
'tastypie.migrations',
'tastypie.contrib',
'tastypie.contrib.gis',
Expand Down
16 changes: 6 additions & 10 deletions tastypie/authentication.py
@@ -1,5 +1,6 @@
from __future__ import unicode_literals
import base64
from hashlib import sha1
import hmac
import time
import uuid
Expand All @@ -10,14 +11,9 @@
from django.middleware.csrf import _sanitize_token, constant_time_compare
from django.utils.http import same_origin
from django.utils.translation import ugettext as _
from tastypie.http import HttpUnauthorized
from tastypie.compat import get_user_model, get_username_field

try:
from hashlib import sha1
except ImportError:
import sha
sha1 = sha.sha
from tastypie.compat import get_user_model, get_username_field
from tastypie.http import HttpUnauthorized

try:
import python_digest
Expand Down Expand Up @@ -189,8 +185,8 @@ def is_authenticated(self, request, **kwargs):
username_field = get_username_field()
User = get_user_model()

lookup_kwargs = {username_field: username}
try:
lookup_kwargs = {username_field: username}
user = User.objects.select_related('api_key').get(**lookup_kwargs)
except (User.DoesNotExist, User.MultipleObjectsReturned):
return self._unauthorized()
Expand Down Expand Up @@ -315,7 +311,7 @@ def _unauthorized(self):
opaque = hmac.new(str(new_uuid).encode('utf-8'), digestmod=sha1).hexdigest()
response['WWW-Authenticate'] = python_digest.build_digest_challenge(
timestamp=time.time(),
secret=getattr(settings, 'SECRET_KEY', ''),
secret=settings.SECRET_KEY,
realm=self.realm,
opaque=opaque,
stale=False
Expand Down Expand Up @@ -343,7 +339,7 @@ def is_authenticated(self, request, **kwargs):
digest_response = python_digest.parse_digest_credentials(request.META['HTTP_AUTHORIZATION'])

# FIXME: Should the nonce be per-user?
if not python_digest.validate_nonce(digest_response.nonce, getattr(settings, 'SECRET_KEY', '')):
if not python_digest.validate_nonce(digest_response.nonce, settings.SECRET_KEY):
return self._unauthorized()

user = self.get_user(digest_response.username)
Expand Down
26 changes: 7 additions & 19 deletions tastypie/compat.py
Expand Up @@ -2,33 +2,21 @@

import django
from django.conf import settings
from django.contrib.auth import get_user_model # flake8: noqa


__all__ = ['get_user_model', 'get_username_field', 'AUTH_USER_MODEL']

AUTH_USER_MODEL = getattr(settings, 'AUTH_USER_MODEL', 'auth.User')

# Django 1.5+ compatibility
if django.VERSION >= (1, 5):
def get_user_model():
from django.contrib.auth import get_user_model as django_get_user_model
AUTH_USER_MODEL = settings.AUTH_USER_MODEL

return django_get_user_model()

def get_username_field():
return get_user_model().USERNAME_FIELD
else:
def get_user_model():
from django.contrib.auth.models import User

return User

def get_username_field():
return 'username'
def get_username_field():
return get_user_model().USERNAME_FIELD


def get_module_name(meta):
return getattr(meta, 'model_name', None) or getattr(meta, 'module_name')
return meta.model_name


# commit_on_success replaced by atomic in Django >=1.8
atomic_decorator = getattr(django.db.transaction, 'atomic', None) or getattr(django.db.transaction, 'commit_on_success')
atomic_decorator = django.db.transaction.atomic
4 changes: 1 addition & 3 deletions tastypie/contrib/contenttypes/resources.py
Expand Up @@ -10,9 +10,7 @@ class GenericResource(ModelResource):
Provides a stand-in resource for GFK relations.
"""
def __init__(self, resources, *args, **kwargs):
self.resource_mapping = dict(
(r._meta.resource_name, r) for r in resources
)
self.resource_mapping = {r._meta.resource_name: r for r in resources}
super(GenericResource, self).__init__(*args, **kwargs)

def get_via_uri(self, uri, request=None):
Expand Down
14 changes: 7 additions & 7 deletions tastypie/fields.py
Expand Up @@ -4,11 +4,7 @@
from dateutil.parser import parse
import decimal
from decimal import Decimal

try:
import importlib
except ImportError:
from django.utils import importlib
import importlib

from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned
from django.db.models.fields.related import SingleRelatedObjectDescriptor
Expand Down Expand Up @@ -280,7 +276,7 @@ def convert(self, value):
if value is None:
return None

return Decimal(str(value))
return Decimal(value)

def hydrate(self, bundle):
value = super(DecimalField, self).hydrate(bundle)
Expand Down Expand Up @@ -624,7 +620,11 @@ def resource_from_data(self, fk_resource, data, request=None, related_obj=None,
fk_bundle.related_obj = related_obj
fk_bundle.related_name = related_name

unique_keys = dict((k, v) for k, v in data.items() if k == 'pk' or (hasattr(fk_resource, k) and getattr(fk_resource, k).unique))
unique_keys = {
k: v
for k, v in data.items()
if k == 'pk' or (hasattr(fk_resource, k) and getattr(fk_resource, k).unique)
}

# If we have no unique keys, we shouldn't go look for some resource that
# happens to match other kwargs. In the case of a create, it might be the
Expand Down
7 changes: 1 addition & 6 deletions tastypie/models.py
@@ -1,5 +1,6 @@
from __future__ import unicode_literals

from hashlib import sha1
import hmac
import time

Expand All @@ -9,12 +10,6 @@

from tastypie.utils import now

try:
from hashlib import sha1
except ImportError:
import sha
sha1 = sha.sha


@python_2_unicode_compatible
class ApiAccess(models.Model):
Expand Down
22 changes: 8 additions & 14 deletions tastypie/resources.py
@@ -1,5 +1,4 @@
from __future__ import unicode_literals
from __future__ import with_statement
from copy import deepcopy
from datetime import datetime
import logging
Expand All @@ -23,9 +22,10 @@
from django.db.models.constants import LOOKUP_SEP
from django.db.models.sql.constants import QUERY_TERMS
from django.http import HttpResponse, HttpResponseNotFound, Http404
from django.utils import six
from django.utils.cache import patch_cache_control, patch_vary_headers
from django.utils.html import escape
from django.utils import six
from django.views.decorators.csrf import csrf_exempt

from tastypie.authentication import Authentication
from tastypie.authorization import ReadOnlyAuthorization
Expand All @@ -45,13 +45,6 @@
from tastypie.validation import Validation
from tastypie.compat import get_module_name, atomic_decorator

# If ``csrf_exempt`` isn't present, stub it.
try:
from django.views.decorators.csrf import csrf_exempt
except ImportError:
def csrf_exempt(func):
return func


def sanitize(text):
# We put the single quotes back, due to their frequent usage in exception
Expand Down Expand Up @@ -288,9 +281,8 @@ def _handle_500(self, request, exception):

# When DEBUG is False, send an error message to the admins (unless it's
# a 404, in which case we check the setting).
send_broken_links = getattr(settings, 'SEND_BROKEN_LINK_EMAILS', False)

if not response_code == 404 or send_broken_links:
if not response_code == 404:
log = logging.getLogger('django.request.tastypie')
log.error('Internal Server Error: %s' % request.path, exc_info=True,
extra={'status_code': response_code, 'request': request})
Expand Down Expand Up @@ -392,8 +384,10 @@ def deserialize(self, request, data, format='application/json'):

Mostly a hook, this uses the ``Serializer`` from ``Resource._meta``.
"""
deserialized = self._meta.serializer.deserialize(data, format=request.META.get('CONTENT_TYPE', format))
return deserialized
try:
return self._meta.serializer.deserialize(data, format=request.META.get('CONTENT_TYPE', format))
except ValueError as e:
raise BadRequest("Invalid data sent: %s" % e)

def alter_list_data_to_serialize(self, request, data):
"""
Expand Down Expand Up @@ -2124,7 +2118,7 @@ def obj_get(self, bundle, **kwargs):
field_names = self._meta.object_class._meta.get_all_field_names()
field_names.append('pk')

kwargs = dict([(k, v,) for k, v in kwargs.items() if k in field_names])
kwargs = {k: v for k, v in kwargs.items() if k in field_names}

try:
object_list = self.get_object_list(bundle.request).filter(**kwargs)
Expand Down