diff options
29 files changed, 125 insertions, 66 deletions
| @@ -3,19 +3,14 @@  *~  .* -site/ -htmlcov/ -coverage/ -build/ -dist/ -*.egg-info/ +/site/ +/htmlcov/ +/coverage/ +/build/ +/dist/ +/*.egg-info/ +/env/  MANIFEST -bin/ -include/ -lib/ -local/ -env/ -  !.gitignore  !.travis.yml diff --git a/.travis.yml b/.travis.yml index 28ebfc00..4f929785 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,18 +25,6 @@ env:      - TOX_ENV=py33-django18alpha      - TOX_ENV=py32-django18alpha      - TOX_ENV=py27-django18alpha -    - TOX_ENV=py34-djangomaster -    - TOX_ENV=py33-djangomaster -    - TOX_ENV=py32-djangomaster -    - TOX_ENV=py27-djangomaster - -matrix: -  fast_finish: true -  allow_failures: -    - env: TOX_ENV=py34-djangomaster -    - env: TOX_ENV=py33-djangomaster -    - env: TOX_ENV=py32-djangomaster -    - env: TOX_ENV=py27-djangomaster  install:    - pip install tox @@ -34,7 +34,7 @@ There is a live example API for testing purposes, [available here][sandbox].  # Requirements  * Python (2.6.5+, 2.7, 3.2, 3.3, 3.4) -* Django (1.4.11+, 1.5.6+, 1.6.3+, 1.7) +* Django (1.4.11+, 1.5.6+, 1.6.3+, 1.7, 1.8-alpha)  # Installation diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 9c33974f..f113bb23 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -591,6 +591,10 @@ The [drf-compound-fields][drf-compound-fields] package provides "compound" seria  The [drf-extra-fields][drf-extra-fields] package provides extra serializer fields for REST framework, including `Base64ImageField` and `PointField` classes. +## djangrestframework-recursive + +the [djangorestframework-recursive][djangorestframework-recursive] package provides a `RecursiveField` for serializing and deserializing recursive structures +  ## django-rest-framework-gis  The [django-rest-framework-gis][django-rest-framework-gis] package provides geographic addons for django rest framework like a  `GeometryField` field and a GeoJSON serializer. @@ -607,6 +611,7 @@ The [django-rest-framework-hstore][django-rest-framework-hstore] package provide  [iso8601]: http://www.w3.org/TR/NOTE-datetime  [drf-compound-fields]: http://drf-compound-fields.readthedocs.org  [drf-extra-fields]: https://github.com/Hipo/drf-extra-fields +[djangorestframework-recursive]: https://github.com/heywbj/django-rest-framework-recursive  [django-rest-framework-gis]: https://github.com/djangonauts/django-rest-framework-gis  [django-rest-framework-hstore]: https://github.com/djangonauts/django-rest-framework-hstore  [django-hstore]: https://github.com/djangonauts/django-hstore diff --git a/docs/api-guide/routers.md b/docs/api-guide/routers.md index 592f7d66..222b6cd2 100644 --- a/docs/api-guide/routers.md +++ b/docs/api-guide/routers.md @@ -304,7 +304,7 @@ The [wq.db package][wq.db] provides an advanced [Router][wq.db-router] class (an  The [`DRF-extensions` package][drf-extensions] provides [routers][drf-extensions-routers] for creating [nested viewsets][drf-extensions-nested-viewsets], [collection level controllers][drf-extensions-collection-level-controllers] with [customizable endpoint names][drf-extensions-customizable-endpoint-names].  [cite]: http://guides.rubyonrails.org/routing.html -[route-decorators]: viewsets.html#marking-extra-actions-for-routing +[route-decorators]: viewsets.md#marking-extra-actions-for-routing  [drf-nested-routers]: https://github.com/alanjds/drf-nested-routers  [wq.db]: http://wq.io/wq.db  [wq.db-router]: http://wq.io/docs/app.py diff --git a/docs/api-guide/viewsets.md b/docs/api-guide/viewsets.md index b09dfc9e..bbf92c6c 100644 --- a/docs/api-guide/viewsets.md +++ b/docs/api-guide/viewsets.md @@ -201,7 +201,7 @@ Note that you can use any of the standard attributes or method overrides provide          def get_queryset(self):              return self.request.user.accounts.all() -Note however that upon removal of the `queryset` property from your `ViewSet`, any associated [router][routers] will be unable to derive the base_name of your Model automatically, and so you you will have to specify the `base_name` kwarg as part of your [router registration][routers]. +Note however that upon removal of the `queryset` property from your `ViewSet`, any associated [router][routers] will be unable to derive the base_name of your Model automatically, and so you will have to specify the `base_name` kwarg as part of your [router registration][routers].  Also note that although this class provides the complete set of create/list/retrieve/update/destroy actions by default, you can restrict the available operations by using the standard permission classes. diff --git a/docs/topics/3.0-announcement.md b/docs/topics/3.0-announcement.md index 24e69c2d..59fe779c 100644 --- a/docs/topics/3.0-announcement.md +++ b/docs/topics/3.0-announcement.md @@ -826,7 +826,7 @@ The `style` keyword argument can be used to pass through additional information  For example, to use a `textarea` control instead of the default `input` control, you would use the following…      additional_notes = serializers.CharField( -        style={'base_template': 'text_area.html'} +        style={'base_template': 'textarea.html'}      )  Similarly, to use a radio button control instead of the default `select` control, you would use the following… diff --git a/docs/topics/third-party-resources.md b/docs/topics/third-party-resources.md index 6f4df288..e26e3a2f 100644 --- a/docs/topics/third-party-resources.md +++ b/docs/topics/third-party-resources.md @@ -237,7 +237,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque  * [django-rest-framework-proxy][django-rest-framework-proxy] - Proxy to redirect incoming request to another API server.  * [gaiarestframework][gaiarestframework] - Utils for django-rest-framewok  * [drf-extensions][drf-extensions] - A collection of custom extensions -* [ember-data-django-rest-adapter][ember-data-django-rest-adapter] - An ember-data adapter +* [ember-django-adapter][ember-django-adapter] - An adapter for working with Ember.js  ## Other Resources @@ -309,7 +309,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque  [django-rest-framework-proxy]: https://github.com/eofs/django-rest-framework-proxy  [gaiarestframework]: https://github.com/AppsFuel/gaiarestframework  [drf-extensions]: https://github.com/chibisov/drf-extensions -[ember-data-django-rest-adapter]: https://github.com/toranb/ember-data-django-rest-adapter +[ember-django-adapter]: https://github.com/dustinfarris/ember-django-adapter  [beginners-guide-to-the-django-rest-framework]: http://code.tutsplus.com/tutorials/beginners-guide-to-the-django-rest-framework--cms-19786  [getting-started-with-django-rest-framework-and-angularjs]: http://blog.kevinastone.com/getting-started-with-django-rest-framework-and-angularjs.html  [end-to-end-web-app-with-django-rest-framework-angularjs]: http://blog.mourafiq.com/post/55034504632/end-to-end-web-app-with-django-rest-framework diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md index 80e869ea..ceb23a02 100644 --- a/docs/tutorial/1-serialization.md +++ b/docs/tutorial/1-serialization.md @@ -97,7 +97,7 @@ The first thing we need to get started on our Web API is to provide a way of ser      class SnippetSerializer(serializers.Serializer):          pk = serializers.IntegerField(read_only=True)          title = serializers.CharField(required=False, allow_blank=True, max_length=100) -        code = serializers.CharField(style={'type': 'textarea'}) +        code = serializers.CharField(style={'base_template': 'textarea.html'})          linenos = serializers.BooleanField(required=False)          language = serializers.ChoiceField(choices=LANGUAGE_CHOICES, default='python')          style = serializers.ChoiceField(choices=STYLE_CHOICES, default='friendly') diff --git a/docs/tutorial/2-requests-and-responses.md b/docs/tutorial/2-requests-and-responses.md index 4ca4e264..e2c173d6 100644 --- a/docs/tutorial/2-requests-and-responses.md +++ b/docs/tutorial/2-requests-and-responses.md @@ -96,7 +96,7 @@ Notice that we're no longer explicitly tying our requests or responses to a give  ## Adding optional format suffixes to our URLs -To take advantage of the fact that our responses are no longer hardwired to a single content type let's add support for format suffixes to our API endpoints.  Using format suffixes gives us URLs that explicitly refer to a given format, and means our API will be able to handle URLs such as [http://example.com/api/items/4.json][json-url]. +To take advantage of the fact that our responses are no longer hardwired to a single content type let's add support for format suffixes to our API endpoints.  Using format suffixes gives us URLs that explicitly refer to a given format, and means our API will be able to handle URLs such as [http://example.com/api/items/4/.json][json-url].  Start by adding a `format` keyword argument to both of the views, like so. diff --git a/docs/tutorial/5-relationships-and-hyperlinked-apis.md b/docs/tutorial/5-relationships-and-hyperlinked-apis.md index 2841f03e..740a4ce2 100644 --- a/docs/tutorial/5-relationships-and-hyperlinked-apis.md +++ b/docs/tutorial/5-relationships-and-hyperlinked-apis.md @@ -138,7 +138,7 @@ After adding all those names into our URLconf, our final `snippets/urls.py` file  The list views for users and code snippets could end up returning quite a lot of instances, so really we'd like to make sure we paginate the results, and allow the API client to step through each of the individual pages. -We can change the default list style to use pagination, by modifying our `settings.py` file slightly.  Add the following setting: +We can change the default list style to use pagination, by modifying our `tutorial/settings.py` file slightly.  Add the following setting:      REST_FRAMEWORK = {          'PAGINATE_BY': 10 diff --git a/requirements.txt b/requirements.txt index cb044958..bf461179 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,6 +14,8 @@ django-filter>=0.9.2  # wheel for PyPI installs  wheel==0.24.0 +# twine for secured PyPI uploads +twine==1.4.0  # MkDocs for documentation previews/deploys  mkdocs==0.11.1 diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index a75cd30c..f0702286 100644 --- a/rest_framework/authentication.py +++ b/rest_framework/authentication.py @@ -168,7 +168,7 @@ class TokenAuthentication(BaseAuthentication):      def authenticate_credentials(self, key):          try: -            token = self.model.objects.get(key=key) +            token = self.model.objects.select_related('user').get(key=key)          except self.model.DoesNotExist:              raise exceptions.AuthenticationFailed(_('Invalid token.')) diff --git a/rest_framework/decorators.py b/rest_framework/decorators.py index 325435b3..21de1acf 100644 --- a/rest_framework/decorators.py +++ b/rest_framework/decorators.py @@ -18,8 +18,7 @@ def api_view(http_method_names=None):      Decorator that converts a function-based view into an APIView subclass.      Takes a list of allowed methods for the view as an argument.      """ -    if http_method_names is None: -        http_method_names = ['GET'] +    http_method_names = ['GET'] if (http_method_names is None) else http_method_names      def decorator(func): @@ -109,10 +108,12 @@ def permission_classes(permission_classes):      return decorator -def detail_route(methods=['get'], **kwargs): +def detail_route(methods=None, **kwargs):      """      Used to mark a method on a ViewSet that should be routed for detail requests.      """ +    methods = ['get'] if (methods is None) else methods +      def decorator(func):          func.bind_to_methods = methods          func.detail = True @@ -121,10 +122,12 @@ def detail_route(methods=['get'], **kwargs):      return decorator -def list_route(methods=['get'], **kwargs): +def list_route(methods=None, **kwargs):      """      Used to mark a method on a ViewSet that should be routed for list requests.      """ +    methods = ['get'] if (methods is None) else methods +      def decorator(func):          func.bind_to_methods = methods          func.detail = False diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index b3658aca..496500ba 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -168,7 +168,9 @@ def _reverse_ordering(ordering_tuple):      Given an order_by tuple such as `('-created', 'uuid')` reverse the      ordering and return a new tuple, eg. `('created', '-uuid')`.      """ -    invert = lambda x: x[1:] if (x.startswith('-')) else '-' + x +    def invert(x): +        return x[1:] if (x.startswith('-')) else '-' + x +      return tuple([invert(item) for item in ordering_tuple]) diff --git a/rest_framework/request.py b/rest_framework/request.py index 86fb1ef1..081ace23 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -12,12 +12,13 @@ from __future__ import unicode_literals  from django.conf import settings  from django.http import QueryDict  from django.http.multipartparser import parse_header +from django.utils import six  from django.utils.datastructures import MultiValueDict  from django.utils.datastructures import MergeDict as DjangoMergeDict -from django.utils.six import BytesIO  from rest_framework import HTTP_HEADER_ENCODING  from rest_framework import exceptions  from rest_framework.settings import api_settings +import sys  import warnings @@ -366,7 +367,7 @@ class Request(object):          elif hasattr(self._request, 'read'):              self._stream = self._request          else: -            self._stream = BytesIO(self.raw_post_data) +            self._stream = six.BytesIO(self.raw_post_data)      def _perform_form_overloading(self):          """ @@ -408,7 +409,7 @@ class Request(object):              self._CONTENTTYPE_PARAM in self._data          ):              self._content_type = self._data[self._CONTENTTYPE_PARAM] -            self._stream = BytesIO(self._data[self._CONTENT_PARAM].encode(self.parser_context['encoding'])) +            self._stream = six.BytesIO(self._data[self._CONTENT_PARAM].encode(self.parser_context['encoding']))              self._data, self._files, self._full_data = (Empty, Empty, Empty)      def _parse(self): @@ -489,8 +490,16 @@ class Request(object):          else:              self.auth = None -    def __getattr__(self, attr): +    def __getattribute__(self, attr):          """ -        Proxy other attributes to the underlying HttpRequest object. +        If an attribute does not exist on this instance, then we also attempt +        to proxy it to the underlying HttpRequest object.          """ -        return getattr(self._request, attr) +        try: +            return super(Request, self).__getattribute__(attr) +        except AttributeError: +            info = sys.exc_info() +            try: +                return getattr(self._request, attr) +            except AttributeError: +                six.reraise(info[0], info[1], info[2].tb_next) diff --git a/rest_framework/response.py b/rest_framework/response.py index 7f90bae1..c21c60a2 100644 --- a/rest_framework/response.py +++ b/rest_framework/response.py @@ -86,8 +86,9 @@ class Response(SimpleTemplateResponse):          state = super(Response, self).__getstate__()          for key in (              'accepted_renderer', 'renderer_context', 'resolver_match', -            'client', 'request', 'wsgi_request', '_closable_objects' +            'client', 'request', 'wsgi_request'          ):              if key in state:                  del state[key] +        state['_closable_objects'] = []          return state diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 7235d8c5..c60574d4 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -177,7 +177,7 @@ class BaseSerializer(Field):          )          assert hasattr(self, 'initial_data'), ( -            'Cannot call `.is_valid()` as no `data=` keyword argument was' +            'Cannot call `.is_valid()` as no `data=` keyword argument was '              'passed when instantiating the serializer instance.'          ) @@ -635,11 +635,11 @@ def raise_errors_on_nested_writes(method_name, serializer, validated_data):      If we don't do this explicitly they'd get a less helpful error when      calling `.save()` on the serializer. -    We don't *automatically* support these sorts of nested writes brecause +    We don't *automatically* support these sorts of nested writes because      there are too many ambiguities to define a default behavior.      Eg. Suppose we have a `UserSerializer` with a nested profile. How should -    we handle the case of an update, where the `profile` realtionship does +    we handle the case of an update, where the `profile` relationship does      not exist? Any of the following might be valid:      * Raise an application error. diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py index a969836f..699ea897 100644 --- a/rest_framework/templatetags/rest_framework.py +++ b/rest_framework/templatetags/rest_framework.py @@ -143,7 +143,9 @@ def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=Tru      If autoescape is True, the link text and URLs will get autoescaped.      """ -    trim_url = lambda x, limit=trim_url_limit: limit is not None and (len(x) > limit and ('%s...' % x[:max(0, limit - 3)])) or x +    def trim_url(x, limit=trim_url_limit): +        return limit is not None and (len(x) > limit and ('%s...' % x[:max(0, limit - 3)])) or x +      safe_input = isinstance(text, SafeData)      words = word_split_re.split(force_text(text))      for i, word in enumerate(words): diff --git a/rest_framework/throttling.py b/rest_framework/throttling.py index 0f10136d..261fc246 100644 --- a/rest_framework/throttling.py +++ b/rest_framework/throttling.py @@ -191,7 +191,7 @@ class UserRateThrottle(SimpleRateThrottle):      def get_cache_key(self, request, view):          if request.user.is_authenticated(): -            ident = request.user.id +            ident = request.user.pk          else:              ident = self.get_ident(request) @@ -239,7 +239,7 @@ class ScopedRateThrottle(SimpleRateThrottle):          with the '.throttle_scope` property of the view.          """          if request.user.is_authenticated(): -            ident = request.user.id +            ident = request.user.pk          else:              ident = self.get_ident(request) @@ -48,8 +48,11 @@ if sys.argv[-1] == 'publish':      if os.system("pip freeze | grep wheel"):          print("wheel not installed.\nUse `pip install wheel`.\nExiting.")          sys.exit() -    os.system("python setup.py sdist upload") -    os.system("python setup.py bdist_wheel upload") +    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_authentication.py b/tests/test_authentication.py index 04c5782e..91e49f9d 100644 --- a/tests/test_authentication.py +++ b/tests/test_authentication.py @@ -168,6 +168,15 @@ class TokenAuthTests(TestCase):          response = self.csrf_client.post('/token/', {'example': 'example'}, format='json', HTTP_AUTHORIZATION=auth)          self.assertEqual(response.status_code, status.HTTP_200_OK) +    def test_post_json_makes_one_db_query(self): +        """Ensure that authenticating a user using a token performs only one DB query""" +        auth = "Token " + self.key + +        def func_to_test(): +            return self.csrf_client.post('/token/', {'example': 'example'}, format='json', HTTP_AUTHORIZATION=auth) + +        self.assertNumQueries(1, func_to_test) +      def test_post_form_failing_token_auth(self):          """Ensure POSTing form over token auth without correct credentials fails"""          response = self.csrf_client.post('/token/', {'example': 'example'}) diff --git a/tests/test_relations_hyperlink.py b/tests/test_relations_hyperlink.py index aede61d2..33b09713 100644 --- a/tests/test_relations_hyperlink.py +++ b/tests/test_relations_hyperlink.py @@ -12,7 +12,9 @@ factory = APIRequestFactory()  request = factory.get('/')  # Just to ensure we have a request in the serializer context -dummy_view = lambda request, pk: None +def dummy_view(request, pk): +    pass +  urlpatterns = [      url(r'^dummyurl/(?P<pk>[0-9]+)/$', dummy_view, name='dummy-url'), diff --git a/tests/test_renderers.py b/tests/test_renderers.py index f68405f0..60a08225 100644 --- a/tests/test_renderers.py +++ b/tests/test_renderers.py @@ -21,8 +21,13 @@ import re  DUMMYSTATUS = status.HTTP_200_OK  DUMMYCONTENT = 'dummycontent' -RENDERER_A_SERIALIZER = lambda x: ('Renderer A: %s' % x).encode('ascii') -RENDERER_B_SERIALIZER = lambda x: ('Renderer B: %s' % x).encode('ascii') + +def RENDERER_A_SERIALIZER(x): +    return ('Renderer A: %s' % x).encode('ascii') + + +def RENDERER_B_SERIALIZER(x): +    return ('Renderer B: %s' % x).encode('ascii')  expected_results = [ diff --git a/tests/test_request.py b/tests/test_request.py index 02a9b1e2..c274ab69 100644 --- a/tests/test_request.py +++ b/tests/test_request.py @@ -249,9 +249,29 @@ class TestUserSetter(TestCase):          login(self.request, self.user)          self.assertEqual(self.wrapped_request.user, self.user) +    def test_calling_user_fails_when_attribute_error_is_raised(self): +        """ +        This proves that when an AttributeError is raised inside of the request.user +        property, that we can handle this and report the true, underlying error. +        """ +        class AuthRaisesAttributeError(object): +            def authenticate(self, request): +                import rest_framework +                rest_framework.MISSPELLED_NAME_THAT_DOESNT_EXIST -class TestAuthSetter(TestCase): +        self.request = Request(factory.get('/'), authenticators=(AuthRaisesAttributeError(),)) +        SessionMiddleware().process_request(self.request) +        login(self.request, self.user) +        try: +            self.request.user +        except AttributeError as error: +            self.assertEqual(str(error), "'module' object has no attribute 'MISSPELLED_NAME_THAT_DOESNT_EXIST'") +        else: +            assert False, 'AttributeError not raised' + + +class TestAuthSetter(TestCase):      def test_auth_can_be_set(self):          request = Request(factory.get('/'))          request.auth = 'DUMMY' diff --git a/tests/test_response.py b/tests/test_response.py index f233ae33..4a9deaa2 100644 --- a/tests/test_response.py +++ b/tests/test_response.py @@ -38,8 +38,13 @@ class MockTextMediaRenderer(BaseRenderer):  DUMMYSTATUS = status.HTTP_200_OK  DUMMYCONTENT = 'dummycontent' -RENDERER_A_SERIALIZER = lambda x: ('Renderer A: %s' % x).encode('ascii') -RENDERER_B_SERIALIZER = lambda x: ('Renderer B: %s' % x).encode('ascii') + +def RENDERER_A_SERIALIZER(x): +    return ('Renderer A: %s' % x).encode('ascii') + + +def RENDERER_B_SERIALIZER(x): +    return ('Renderer B: %s' % x).encode('ascii')  class RendererA(BaseRenderer): diff --git a/tests/test_throttling.py b/tests/test_throttling.py index cc36a004..50a53b3e 100644 --- a/tests/test_throttling.py +++ b/tests/test_throttling.py @@ -188,7 +188,9 @@ class ScopedRateThrottleTests(TestCase):          class XYScopedRateThrottle(ScopedRateThrottle):              TIMER_SECONDS = 0              THROTTLE_RATES = {'x': '3/min', 'y': '1/min'} -            timer = lambda self: self.TIMER_SECONDS + +            def timer(self): +                return self.TIMER_SECONDS          class XView(APIView):              throttle_classes = (XYScopedRateThrottle,) @@ -290,7 +292,9 @@ class XffTestingBase(TestCase):          class Throttle(ScopedRateThrottle):              THROTTLE_RATES = {'test_limit': '1/day'}              TIMER_SECONDS = 0 -            timer = lambda self: self.TIMER_SECONDS + +            def timer(self): +                return self.TIMER_SECONDS          class View(APIView):              throttle_classes = (Throttle,) diff --git a/tests/test_versioning.py b/tests/test_versioning.py index 553463d1..90ad8afd 100644 --- a/tests/test_versioning.py +++ b/tests/test_versioning.py @@ -32,8 +32,13 @@ class RequestInvalidVersionView(APIView):  factory = APIRequestFactory() -dummy_view = lambda request: None -dummy_pk_view = lambda request, pk: None + +def dummy_view(request): +    pass + + +def dummy_pk_view(request, pk): +    pass  class TestRequestVersion: @@ -3,7 +3,7 @@ envlist =         py27-{flake8,docs},         {py26,py27}-django14,         {py26,py27,py32,py33,py34}-django{15,16}, -       {py27,py32,py33,py34}-django{17,18alpha,master} +       {py27,py32,py33,py34}-django{17,18alpha}  [testenv]  commands = ./runtests.py --fast @@ -15,7 +15,6 @@ deps =         django16: Django==1.6.3  # Should track minimum supported         django17: Django==1.7.2  # Should track maximum supported         django18alpha: https://www.djangoproject.com/download/1.8a1/tarball/ -       djangomaster: https://github.com/django/django/zipball/master         django-guardian==1.2.4         pytest-django==2.8.0         django-filter==0.9.2 | 
