diff --git a/.travis.yml b/.travis.yml index a5b6d7d918..7dec96cf16 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,7 +22,8 @@ env: - TOX_ENV=py2.6-django1.4 install: - - "pip install tox --download-cache $HOME/.pip-cache" +# Virtualenv < 14 is required to keep the Python 3.2 builds running. + - "pip install tox 'virtualenv<14' --download-cache $HOME/.pip-cache" script: - tox -e $TOX_ENV diff --git a/README.md b/README.md index 428fb8e9d1..fd91064168 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ **Awesome web-browseable Web APIs.** -**Note**: Full documentation for the project is available at [http://www.django-rest-framework.org][docs]. +**Note**: Full documentation for the project is available at [http://tomchristie.github.io/rest-framework-2-docs/][docs]. # Overview @@ -135,7 +135,7 @@ Or to create a new user: # Documentation & Support -Full documentation for the project is available at [http://www.django-rest-framework.org][docs]. +Full documentation for the project is available at [http://tomchristie.github.io/rest-framework-2-docs/][docs]. For questions and support, use the [REST framework discussion group][group], or `#restframework` on freenode IRC. @@ -202,7 +202,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [wlonk]: https://twitter.com/wlonk/status/261689665952833536 [laserllama]: https://twitter.com/laserllama/status/328688333750407168 -[docs]: http://www.django-rest-framework.org/ +[docs]: http://tomchristie.github.io/rest-framework-2-docs/ [urlobject]: https://github.com/zacharyvoase/urlobject [markdown]: http://pypi.python.org/pypi/Markdown/ [pyyaml]: http://pypi.python.org/pypi/PyYAML diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 11d12ae326..682d57b130 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -40,6 +40,28 @@ You can determine your currently installed version using `pip freeze`: ## 2.4.x series +### 2.4.8 + +**Date**: 18 August 2015 + +* Repackage 2.4.7 without pyc files. + + +### 2.4.7 + +**Date**: [18 August 2015](https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%222.4.7+Release%22+) + +* Upgrade guardian support to 1.3 + + +### 2.4.6 + +### 2.4.5 + +**Date**: 24 March 2015 + +* **Security fix**: Escape tab switching cookie name in browsable API. [Backported from 3.1.1](http://www.django-rest-framework.org/topics/release-notes/#311). + ### 2.4.4 **Date**: [3rd November 2014](https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%222.4.4+Release%22+). diff --git a/requirements-test.txt b/requirements-test.txt index 411daeba2d..2880f5a987 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -2,7 +2,8 @@ pytest-django==2.6 pytest==2.5.2 pytest-cov==1.6 -flake8==2.2.2 +pep8==1.5.7 +flake8==2.4.0 # Optional packages markdown>=2.1.0 diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index 15b12d9bea..a15ed7cd49 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -8,7 +8,7 @@ """ __title__ = 'Django REST framework' -__version__ = '2.4.4' +__version__ = '2.4.9' __author__ = 'Tom Christie' __license__ = 'BSD 2-Clause' __copyright__ = 'Copyright 2011-2014 Tom Christie' diff --git a/rest_framework/compat.py b/rest_framework/compat.py index fa0f0bfb17..b0150f3d58 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -73,6 +73,12 @@ from collections import UserDict from collections import MutableMapping as DictMixin +# http responses move in Python 3 +try: + from httplib import responses +except ImportError: + from http.client import responses + # Try to import PIL in either of the two ways it can end up installed. try: from PIL import Image @@ -265,3 +271,30 @@ def python_2_unicode_compatible(klass): klass.__unicode__ = klass.__str__ klass.__str__ = lambda self: self.__unicode__().encode('utf-8') return klass + +""" +SortedDict deprecated since django 1.8. There is collections.OrderedDict +which available since python 2.7 and python 3.1. There are no need of other +checks because of django 1.7+ requires python 2.7+ +""" +try: + from collections import OrderedDict as SortedDict +except ImportError: + from django.utils.datastructures import SortedDict + +""" +GenericForeignKey moves from generic to fields in django 1.9 and in 1.8 shows +deprecation warnings +""" +if django.VERSION >= (1, 8): + from django.contrib.contenttypes.fields import GenericForeignKey +else: + from django.contrib.contenttypes.generic import GenericForeignKey + +""" +django.utils.importlib is deprecated since django 1.8 +""" +try: + import importlib +except ImportError: + from django.utils import importlib diff --git a/rest_framework/fields.py b/rest_framework/fields.py index c0253f86b8..d310c5df36 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -10,6 +10,7 @@ import inspect import re import warnings +import collections from decimal import Decimal, DecimalException from django import forms from django.core import validators @@ -21,11 +22,10 @@ from django.utils import six, timezone from django.utils.encoding import is_protected_type from django.utils.translation import ugettext_lazy as _ -from django.utils.datastructures import SortedDict from django.utils.dateparse import parse_date, parse_datetime, parse_time from rest_framework import ISO_8601 from rest_framework.compat import ( - BytesIO, smart_text, + BytesIO, smart_text, SortedDict, force_text, is_non_str_iterable ) from rest_framework.settings import api_settings @@ -52,7 +52,7 @@ def get_component(obj, attr_name): Given an object, and an attribute name, return that attribute on the object. """ - if isinstance(obj, dict): + if isinstance(obj, collections.Mapping): val = obj.get(attr_name) else: val = getattr(obj, attr_name) diff --git a/rest_framework/filters.py b/rest_framework/filters.py index c580f9351b..18f4862e0f 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -188,4 +188,9 @@ def filter_queryset(self, request, queryset, view): 'model_name': get_model_name(model_cls) } permission = self.perm_format % kwargs - return guardian.shortcuts.get_objects_for_user(user, permission, queryset) + if guardian.VERSION >= (1, 3): + # Maintain behavior compatibility with versions prior to 1.3 + extra = {'accept_global_perms': False} + else: + extra = {} + return guardian.shortcuts.get_objects_for_user(user, permission, queryset, **extra) diff --git a/rest_framework/response.py b/rest_framework/response.py old mode 100644 new mode 100755 index 0a7d313f40..3e7e22bac1 --- a/rest_framework/response.py +++ b/rest_framework/response.py @@ -6,9 +6,9 @@ """ from __future__ import unicode_literals import django -from django.core.handlers.wsgi import STATUS_CODE_TEXT from django.template.response import SimpleTemplateResponse from django.utils import six +from rest_framework.compat import responses class Response(SimpleTemplateResponse): @@ -81,7 +81,7 @@ def status_text(self): """ # TODO: Deprecate and use a template tag instead # TODO: Status code text for RFC 6585 status codes - return STATUS_CODE_TEXT.get(self.status_code, '') + return responses.get(self.status_code, '') def __getstate__(self): """ @@ -91,4 +91,5 @@ def __getstate__(self): for key in ('accepted_renderer', 'renderer_context', 'data'): if key in state: del state[key] + state['_closable_objects'] = [] return state diff --git a/rest_framework/routers.py b/rest_framework/routers.py index 169e6e8bc4..9e5813b398 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -20,8 +20,8 @@ from django.conf.urls import patterns, url from django.core.exceptions import ImproperlyConfigured from django.core.urlresolvers import NoReverseMatch -from django.utils.datastructures import SortedDict from rest_framework import views +from rest_framework.compat import SortedDict from rest_framework.response import Response from rest_framework.reverse import reverse from rest_framework.urlpatterns import format_suffix_patterns diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 7d85894f63..67cf432e09 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -15,15 +15,17 @@ import datetime import inspect import types + +import django + from decimal import Decimal -from django.contrib.contenttypes.generic import GenericForeignKey from django.core.paginator import Page from django.db import models from django.forms import widgets from django.utils import six -from django.utils.datastructures import SortedDict from django.utils.functional import cached_property from django.core.exceptions import ObjectDoesNotExist +from rest_framework.compat import SortedDict, GenericForeignKey from rest_framework.settings import api_settings @@ -110,9 +112,17 @@ class SortedDictWithMetadata(SortedDict): """ A sorted dict-like object, that can have additional properties attached. """ + def __reduce__(self): + """ + Used by pickle (e.g., caching) if OrderedDict is used instead of SortedDict + Overriden to remove the metadata from the dict, since it shouldn't be + pickle and may in some instances be unpickleable. + """ + return self.__class__, (SortedDict(self), ) + def __getstate__(self): """ - Used by pickle (e.g., caching). + Used by pickle (e.g., caching) in SortedDict Overriden to remove the metadata from the dict, since it shouldn't be pickle and may in some instances be unpickleable. """ @@ -986,7 +996,11 @@ def restore_object(self, attrs, instance=None): m2m_data[field_name] = attrs.pop(field_name) # Forward m2m relations - for field in meta.many_to_many + meta.virtual_fields: + if issubclass(meta.many_to_many.__class__, tuple): + temp_m2m = list(meta.many_to_many) + else: + temp_m2m = meta.many_to_many + for field in temp_m2m + meta.virtual_fields: if isinstance(field, GenericForeignKey): continue if field.name in attrs: @@ -1068,6 +1082,12 @@ def save_object(self, obj, **kwargs): fk_field = obj._meta.get_field_by_name(accessor_name)[0].field.name setattr(related, fk_field, obj) self.save_object(related) + elif isinstance(related, list): + # Many to One/Many + if django.VERSION >= (1, 9): + getattr(obj, accessor_name).add(*related, bulk=False) + else: + getattr(obj, accessor_name).add(*related) else: # Reverse FK or reverse one-one setattr(obj, accessor_name, related) diff --git a/rest_framework/settings.py b/rest_framework/settings.py index 644751f877..e77ec754fc 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -19,8 +19,9 @@ """ from __future__ import unicode_literals from django.conf import settings -from django.utils import importlib, six +from django.utils import six from rest_framework import ISO_8601 +from rest_framework.compat import importlib USER_SETTINGS = getattr(settings, 'REST_FRAMEWORK', None) diff --git a/rest_framework/static/rest_framework/js/default.js b/rest_framework/static/rest_framework/js/default.js index bcb1964dbe..f04e55696d 100644 --- a/rest_framework/static/rest_framework/js/default.js +++ b/rest_framework/static/rest_framework/js/default.js @@ -43,6 +43,10 @@ $('a[data-toggle="tab"]').click(function(){ var selectedTab = null; var selectedTabName = getCookie('tabstyle'); +if (selectedTabName) { + selectedTabName = selectedTabName.replace(/[^a-z-]/g, ''); +} + if (selectedTabName) { selectedTab = $('.form-switcher a[name=' + selectedTabName + ']'); } diff --git a/rest_framework/utils/encoders.py b/rest_framework/utils/encoders.py index 00ffdfbae5..813108b777 100644 --- a/rest_framework/utils/encoders.py +++ b/rest_framework/utils/encoders.py @@ -4,9 +4,8 @@ from __future__ import unicode_literals from django.utils import timezone from django.db.models.query import QuerySet -from django.utils.datastructures import SortedDict from django.utils.functional import Promise -from rest_framework.compat import force_text +from rest_framework.compat import force_text, SortedDict from rest_framework.serializers import DictWithMetadata, SortedDictWithMetadata import datetime import decimal diff --git a/rest_framework/views.py b/rest_framework/views.py index 38346ab799..fb30775b19 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -5,10 +5,9 @@ from django.core.exceptions import PermissionDenied from django.http import Http404 -from django.utils.datastructures import SortedDict from django.views.decorators.csrf import csrf_exempt from rest_framework import status, exceptions -from rest_framework.compat import smart_text, HttpResponseBase, View +from rest_framework.compat import smart_text, HttpResponseBase, SortedDict, View from rest_framework.request import Request from rest_framework.response import Response from rest_framework.settings import api_settings diff --git a/setup.py b/setup.py index 2c56cd758f..f9c10456bc 100755 --- a/setup.py +++ b/setup.py @@ -59,8 +59,14 @@ def get_package_data(package): if sys.argv[-1] == 'publish': - os.system("python setup.py sdist upload") - os.system("python setup.py bdist_wheel upload") + if os.system("pip freeze | grep wheel"): + print("wheel not installed.\nUse `pip install wheel`.\nExiting.") + sys.exit() + if os.system("pip freeze | grep twine"): + print("twine not installed.\nUse `pip install twine`.\nExiting.") + sys.exit() + os.system("python setup.py sdist bdist_wheel") + os.system("twine upload dist/*") print("You probably want to also tag the version now:") print(" git tag -a %s -m 'version %s'" % (version, version)) print(" git push --tags") diff --git a/tests/test_fields.py b/tests/test_fields.py index 0ddbe48b5b..058ad70355 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -10,8 +10,8 @@ from django.core import validators from django.db import models from django.test import TestCase -from django.utils.datastructures import SortedDict from rest_framework import serializers +from rest_framework.compat import SortedDict from tests.models import RESTFrameworkModel diff --git a/tox.ini b/tox.ini index b3f53cce28..0e17ca5116 100644 --- a/tox.ini +++ b/tox.ini @@ -15,7 +15,8 @@ setenv = [testenv:flake8] basepython = python2.7 deps = pytest==2.5.2 - flake8==2.2.2 + pep8==1.5.7 + flake8==2.4.0 commands = ./runtests.py --lintonly [testenv:py3.4-django1.7]