diff --git a/.gitignore b/.gitignore index bde1b9d..6a693eb 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,5 @@ rest_framework_docs/static/rest_framework_docs/js/dist.min.js rest_framework_docs/static/node_modules/ rest_framework_docs/static/rest_framework_docs/js/dist.min.js.map + +.tox/ \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 8a61d83..949a576 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,10 +6,17 @@ python: - "2.7" - "3.4" - "3.5" + - "3.6" env: - - DJANGO_VERSION=1.8 - - DJANGO_VERSION=1.9 + - DJANGO_VERSION=1.10 + - DJANGO_VERSION=1.11 + - DJANGO_VERSION=2.0 + +matrix: + exclude: + - env: DJANGO_VERSION=2.0 + python: "2.7" cache: - pip diff --git a/README.md b/README.md index 3bfde01..7dd0425 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,9 @@ Document Web APIs made with Django Rest Framework. [View Demo](http://demo.drfdo ### Supports - - Python (2.7, 3.3, 3.4, 3.5) - - Django (1.8, 1.9) - - Django Rest Framework (3+) + - Python (2.7, 3.4, 3.5, 3.6) + - Django (1.10, 1.11, 2.0) + - Django Rest Framework (3.4+) ### Documentation - Table of contents diff --git a/demo/project/accounts/urls.py b/demo/project/accounts/urls.py index 1486675..d396169 100644 --- a/demo/project/accounts/urls.py +++ b/demo/project/accounts/urls.py @@ -1,6 +1,7 @@ from django.conf.urls import url from project.accounts import views +app_name = "accounts" urlpatterns = [ url(r'^test/$', views.TestView.as_view(), name="test-view"), diff --git a/demo/project/organisations/models.py b/demo/project/organisations/models.py index fcb0cba..6d3f7c4 100644 --- a/demo/project/organisations/models.py +++ b/demo/project/organisations/models.py @@ -29,7 +29,7 @@ class Meta: id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) joined = models.DateTimeField(auto_now_add=True) - organisation = models.ForeignKey(Organisation) - user = models.ForeignKey(User) + organisation = models.ForeignKey(Organisation, on_delete=models.CASCADE) + user = models.ForeignKey(User, on_delete=models.CASCADE) role = models.CharField(choices=MEMBER_ROLES, max_length=20, default="USER") is_owner = models.BooleanField(default=False) diff --git a/demo/project/organisations/urls.py b/demo/project/organisations/urls.py index 4d0311e..d9e9ee9 100644 --- a/demo/project/organisations/urls.py +++ b/demo/project/organisations/urls.py @@ -1,6 +1,7 @@ from django.conf.urls import url from project.organisations import views +app_name = "organisations" urlpatterns = [ diff --git a/demo/project/urls.py b/demo/project/urls.py index d8e049f..48f9711 100644 --- a/demo/project/urls.py +++ b/demo/project/urls.py @@ -17,7 +17,7 @@ from django.contrib import admin urlpatterns = [ - url(r'^admin/', include(admin.site.urls)), + url(r'^admin/', admin.site.urls), url(r'^docs/', include('rest_framework_docs.urls')), # API diff --git a/requirements.txt b/requirements.txt index f449bbb..98cbdba 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ -Django==1.8.7 -djangorestframework==3.3.2 -coverage==4.0.3 +Django==1.11 +djangorestframework==3.7.7 +coverage==4.4.2 flake8==2.5.1 mkdocs==0.15.3 diff --git a/rest_framework_docs/__init__.py b/rest_framework_docs/__init__.py index ad3cf1d..b56a50b 100644 --- a/rest_framework_docs/__init__.py +++ b/rest_framework_docs/__init__.py @@ -1 +1 @@ -__version__ = '0.0.11' +__version__ = '0.0.12b' diff --git a/rest_framework_docs/api_docs.py b/rest_framework_docs/api_docs.py index d14fae4..9291134 100644 --- a/rest_framework_docs/api_docs.py +++ b/rest_framework_docs/api_docs.py @@ -1,10 +1,12 @@ from importlib import import_module from django.conf import settings -from django.core.urlresolvers import RegexURLResolver, RegexURLPattern + from django.utils.module_loading import import_string from rest_framework.views import APIView from rest_framework_docs.api_endpoint import ApiEndpoint +from .compat import get_regex_pattern, is_url_pattern, is_url_resolver + class ApiDocumentation(object): @@ -23,10 +25,10 @@ def __init__(self, drf_router=None): def get_all_view_names(self, urlpatterns, parent_regex=''): for pattern in urlpatterns: - if isinstance(pattern, RegexURLResolver): - regex = '' if pattern._regex == "^" else pattern._regex + if is_url_resolver(pattern): + regex = '' if get_regex_pattern(pattern) == "^" else get_regex_pattern(pattern) self.get_all_view_names(urlpatterns=pattern.url_patterns, parent_regex=parent_regex + regex) - elif isinstance(pattern, RegexURLPattern) and self._is_drf_view(pattern) and not self._is_format_endpoint(pattern): + elif is_url_pattern(pattern) and self._is_drf_view(pattern) and not self._is_format_endpoint(pattern): api_endpoint = ApiEndpoint(pattern, parent_regex, self.drf_router) self.endpoints.append(api_endpoint) @@ -40,7 +42,7 @@ def _is_format_endpoint(self, pattern): """ Exclude endpoints with a "format" parameter """ - return '?P' in pattern._regex + return '?P' in get_regex_pattern(pattern) def get_endpoints(self): return self.endpoints diff --git a/rest_framework_docs/api_endpoint.py b/rest_framework_docs/api_endpoint.py index 953f9a0..c691850 100644 --- a/rest_framework_docs/api_endpoint.py +++ b/rest_framework_docs/api_endpoint.py @@ -7,6 +7,8 @@ from rest_framework.viewsets import ModelViewSet from rest_framework.serializers import BaseSerializer +from .compat import get_regex_pattern + VIEWSET_METHODS = { 'List': ['get', 'post'], 'Instance': ['get', 'put', 'patch', 'delete'], @@ -35,9 +37,10 @@ def __init__(self, pattern, parent_regex=None, drf_router=None): self.permissions = self.__get_permissions_class__() def __get_path__(self, parent_regex): + regex = get_regex_pattern(self.pattern) if parent_regex: - return "/{0}{1}".format(self.name_parent, simplify_regex(self.pattern.regex.pattern)) - return simplify_regex(self.pattern.regex.pattern) + return "/{0}{1}".format(self.name_parent, simplify_regex(regex)) + return simplify_regex(regex) def is_method_allowed(self, callback_cls, method_name): has_attr = hasattr(callback_cls, method_name) @@ -69,7 +72,7 @@ def __get_allowed_methods__(self): lookup=lookup, trailing_slash=self.drf_router.trailing_slash ) - if self.pattern.regex.pattern == regex: + if get_regex_pattern(self.pattern) == regex: funcs, viewset_methods = zip( *[(mapping[m], m.upper()) for m in self.callback.cls.http_method_names diff --git a/rest_framework_docs/compat.py b/rest_framework_docs/compat.py new file mode 100644 index 0000000..1d0ab96 --- /dev/null +++ b/rest_framework_docs/compat.py @@ -0,0 +1,33 @@ +try: + from django.urls import ( + URLPattern, + URLResolver, + ) +except ImportError: + # Will be removed in Django 2.0 + from django.urls import ( + RegexURLPattern as URLPattern, + RegexURLResolver as URLResolver, + ) + + +# This is from the similarly named compat.py file of django-rest-framework 3.7 +def get_regex_pattern(urlpattern): + """ + Get the raw regex out of the urlpattern's RegexPattern or RoutePattern. + This is always a regular expression, unlike get_original_route above. + """ + if hasattr(urlpattern, 'pattern'): + # Django 2.0 + return urlpattern.pattern.regex.pattern + else: + # Django < 2.0 + return urlpattern.regex.pattern + + +def is_url_resolver(instance): + return isinstance(instance, URLResolver) + + +def is_url_pattern(instance): + return isinstance(instance, URLPattern) diff --git a/setup.py b/setup.py index b85340a..3e51d91 100644 --- a/setup.py +++ b/setup.py @@ -23,6 +23,7 @@ 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5' + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6' ], ) diff --git a/tests/models.py b/tests/models.py index 9489337..68d00df 100644 --- a/tests/models.py +++ b/tests/models.py @@ -43,7 +43,7 @@ class Meta: id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) joined = models.DateTimeField(auto_now_add=True) - organisation = models.ForeignKey(Organisation) - user = models.ForeignKey(User) + organisation = models.ForeignKey(Organisation, on_delete=models.CASCADE) + user = models.ForeignKey(User, on_delete=models.CASCADE) role = models.CharField(choices=MEMBER_ROLES, max_length=20, default="USER") is_owner = models.BooleanField(default=False) diff --git a/tests/settings.py b/tests/settings.py index d80e9f2..c3fa306 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -39,3 +39,20 @@ # https://docs.djangoproject.com/en/1.8/howto/static-files/ STATIC_URL = '/static/' + + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] diff --git a/tests/tests.py b/tests/tests.py index f94736c..4d21db2 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -1,4 +1,9 @@ -from django.core.urlresolvers import reverse +try: + from django.urls import reverse_lazy +except ImportError: + # Will be removed in Django 2.0 + from django.core.urlresolvers import reverse_lazy + from django.test import TestCase, override_settings from rest_framework_docs.settings import DRFSettings @@ -24,7 +29,7 @@ def test_index_view_with_endpoints(self): Should load the drf docs view with all the endpoints. NOTE: Views that do **not** inherit from DRF's "APIView" are not included. """ - response = self.client.get(reverse('drfdocs')) + response = self.client.get(reverse_lazy('drfdocs')) self.assertEqual(response.status_code, 200) self.assertEqual(len(response.context["endpoints"]), 15) @@ -50,7 +55,7 @@ def test_index_view_with_endpoints(self): self.assertEqual(str(response.context["endpoints"][9].errors), "'test_value'") def test_index_search_with_endpoints(self): - response = self.client.get("%s?search=reset-password" % reverse("drfdocs")) + response = self.client.get("%s?search=reset-password" % reverse_lazy("drfdocs")) self.assertEqual(response.status_code, 200) self.assertEqual(len(response.context["endpoints"]), 2) @@ -63,13 +68,13 @@ def test_index_view_docs_hidden(self): Should prevent the docs from loading the "HIDE_DOCS" is set to "True" or undefined under settings """ - response = self.client.get(reverse('drfdocs')) + response = self.client.get(reverse_lazy('drfdocs')) self.assertEqual(response.status_code, 404) self.assertEqual(response.reason_phrase.upper(), "NOT FOUND") def test_model_viewset(self): - response = self.client.get(reverse('drfdocs')) + response = self.client.get(reverse_lazy('drfdocs')) self.assertEqual(response.status_code, 200) diff --git a/tests/urls.py b/tests/urls.py index abdf71b..a11fd23 100644 --- a/tests/urls.py +++ b/tests/urls.py @@ -30,12 +30,12 @@ router.register('organisation-model-viewsets', views.TestModelViewSet, base_name='organisation') urlpatterns = [ - url(r'^admin/', include(admin.site.urls)), + url(r'^admin/', admin.site.urls), url(r'^docs/', DRFDocsView.as_view(drf_router=router), name='drfdocs'), # API - url(r'^accounts/', view=include(accounts_urls, namespace='accounts')), - url(r'^organisations/', view=include(organisations_urls, namespace='organisations')), + url(r'^accounts/', view=include((accounts_urls, "accounts"), namespace='accounts')), + url(r'^organisations/', view=include((organisations_urls, "organisations"), namespace='organisations')), url(r'^', include(router.urls)), # Endpoints without parents/namespaces diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..e3bd399 --- /dev/null +++ b/tox.ini @@ -0,0 +1,18 @@ +[tox] +envlist = + py{27,34,36}-django{1.10,1.11}-drf{3.4,3.5,3.6,3.7} + py{34,36}-django2.0-drf3.7 + +[testenv] +deps = + py{27,34,36}: coverage == 4.4.2 + flake8 == 2.5.1 + django1.10: Django>=1.10,<1.11 + django1.11: Django>=1.11,<1.12 + django2.0: Django>=2.0,<2.1 + drf3.4: djangorestframework>=3.4,<3.5 + drf3.5: djangorestframework>=3.5,<3.6 + drf3.6: djangorestframework>=3.6,<3.7 + drf3.7: djangorestframework>=3.7,<3.8 +commands = + python runtests.py \ No newline at end of file