diff --git a/.gitignore b/.gitignore index 4f2b0ddf4..3d5f1043d 100644 --- a/.gitignore +++ b/.gitignore @@ -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 28ebfc00f..4f9297853 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 diff --git a/README.md b/README.md index a03517f47..eec809779 100644 --- a/README.md +++ b/README.md @@ -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 9c33974f9..f113bb232 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 592f7d66f..222b6cd25 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 b09dfc9e9..bbf92c6ce 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 24e69c2de..59fe779ca 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 6f4df2886..e26e3a2fa 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 80e869ea6..ceb23a020 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 4ca4e2643..e2c173d6e 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 2841f03e9..740a4ce21 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 cb0449589..bf4611792 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 a75cd30cd..f0702286c 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 325435b3f..21de1acf4 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 b3658acad..496500ba5 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 86fb1ef12..081ace236 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 7f90bae10..c21c60a2e 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 7235d8c51..c60574d4f 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 a969836fd..699ea897f 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 0f10136d6..261fc2463 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) diff --git a/setup.py b/setup.py index efe39d8d4..4cdcfa86e 100755 --- a/setup.py +++ b/setup.py @@ -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 04c5782ea..91e49f9d8 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 aede61d2b..33b09713a 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[0-9]+)/$', dummy_view, name='dummy-url'), diff --git a/tests/test_renderers.py b/tests/test_renderers.py index f68405f09..60a082250 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 02a9b1e27..c274ab69d 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 + + 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 f233ae332..4a9deaa29 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 cc36a004c..50a53b3eb 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 553463d19..90ad8afd2 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: diff --git a/tox.ini b/tox.ini index 129752f3c..76f4f09b3 100644 --- a/tox.ini +++ b/tox.ini @@ -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