From fcaf879d264eaa63e5b23f5a778ae915a37ab8d9 Mon Sep 17 00:00:00 2001 From: Will Barton Date: Fri, 9 Mar 2018 14:58:17 -0500 Subject: [PATCH 1/5] Add compatibility with Django 2.0 and Wagtail 2.0 --- .travis.yml | 10 +- flags/conditions.py | 6 +- flags/migrations/0001_initial.py | 4 +- flags/migrations/0002_auto_20151030_1401.py | 4 +- flags/migrations/0007_unique_flag_site.py | 4 +- flags/templatetags/feature_flags.py | 12 +- flags/tests/settings.py | 15 ++- flags/tests/test_conditions.py | 8 +- flags/tests/test_state.py | 8 +- flags/tests/test_template_functions.py | 8 +- .../tests/test_templatetags_feature_flags.py | 6 +- flags/tests/test_urls.py | 101 ++++++++++------ flags/tests/test_views.py | 11 +- flags/urls.py | 110 ++++++++++++++---- flags/wagtail_hooks.py | 21 +++- setup.py | 4 +- tox.ini | 5 +- 17 files changed, 239 insertions(+), 98 deletions(-) diff --git a/.travis.yml b/.travis.yml index 45a6a05..5471614 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,30 +5,24 @@ matrix: include: - env: TOXENV=lint python: 3.6 - - env: TOXENV=py27-dj18-wag110 - python: 2.7 - env: TOXENV=py27-dj18-wag112 python: 2.7 - env: TOXENV=py27-dj18-wag113 python: 2.7 - - env: TOXENV=py27-dj111-wag110 - python: 2.7 - env: TOXENV=py27-dj111-wag112 python: 2.7 - env: TOXENV=py27-dj111-wag113 python: 2.7 - - env: TOXENV=py36-dj18-wag110 - python: 3.6 - env: TOXENV=py36-dj18-wag112 python: 3.6 - env: TOXENV=py36-dj18-wag113 python: 3.6 - - env: TOXENV=py36-dj111-wag110 - python: 3.6 - env: TOXENV=py36-dj111-wag112 python: 3.6 - env: TOXENV=py36-dj111-wag113 python: 3.6 + - env: TOXENV=py36-dj20-wag20 + python: 3.6 install: pip install tox coveralls diff --git a/flags/conditions.py b/flags/conditions.py index 81e70ad..882f76f 100644 --- a/flags/conditions.py +++ b/flags/conditions.py @@ -1,5 +1,6 @@ import re +import django from django.apps import apps from django.core.exceptions import ObjectDoesNotExist from django.utils import dateparse, timezone @@ -82,7 +83,10 @@ def anonymous_condition(boolean_value, request=None, **kwargs): raise RequiredForCondition("request is required for condition " "'anonymous'") - return bool(boolean_value) == bool(request.user.is_anonymous()) + if django.VERSION[0] >= 2: + return bool(boolean_value) == bool(request.user.is_anonymous) + else: + return bool(boolean_value) == bool(request.user.is_anonymous()) @register('parameter') diff --git a/flags/migrations/0001_initial.py b/flags/migrations/0001_initial.py index defe53a..ba425f0 100644 --- a/flags/migrations/0001_initial.py +++ b/flags/migrations/0001_initial.py @@ -17,7 +17,7 @@ class Migration(migrations.Migration): ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('key', models.CharField(max_length=500)), ('enabled', models.BooleanField()), - ('site', models.ForeignKey(to='wagtailcore.Site')), + ('site', models.ForeignKey(to='wagtailcore.Site', on_delete=models.CASCADE)), ], ), migrations.CreateModel( @@ -25,7 +25,7 @@ class Migration(migrations.Migration): fields=[ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('default', models.BooleanField()), - ('site', models.ForeignKey(to='wagtailcore.Site')), + ('site', models.ForeignKey(to='wagtailcore.Site', on_delete=models.CASCADE)), ], ), ] diff --git a/flags/migrations/0002_auto_20151030_1401.py b/flags/migrations/0002_auto_20151030_1401.py index 9cf9a8b..53dd540 100644 --- a/flags/migrations/0002_auto_20151030_1401.py +++ b/flags/migrations/0002_auto_20151030_1401.py @@ -46,11 +46,11 @@ class Migration(migrations.Migration): migrations.AddField( model_name='flagstate', name='flag', - field=models.ForeignKey(to='flags.Flag'), + field=models.ForeignKey(to='flags.Flag', on_delete=models.CASCADE), ), migrations.AddField( model_name='flagstate', name='site', - field=models.ForeignKey(to='wagtailcore.Site'), + field=models.ForeignKey(to='wagtailcore.Site', on_delete=models.CASCADE), ), ] diff --git a/flags/migrations/0007_unique_flag_site.py b/flags/migrations/0007_unique_flag_site.py index 8112356..700caf6 100644 --- a/flags/migrations/0007_unique_flag_site.py +++ b/flags/migrations/0007_unique_flag_site.py @@ -14,12 +14,12 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='flagstate', name='flag', - field=models.ForeignKey(related_name='states', to='flags.Flag'), + field=models.ForeignKey(related_name='states', to='flags.Flag', on_delete=models.CASCADE), ), migrations.AlterField( model_name='flagstate', name='site', - field=models.ForeignKey(related_name='flag_states', to='wagtailcore.Site'), + field=models.ForeignKey(related_name='flag_states', to='wagtailcore.Site', on_delete=models.CASCADE), ), migrations.AlterUniqueTogether( name='flagstate', diff --git a/flags/templatetags/feature_flags.py b/flags/templatetags/feature_flags.py index a97877c..ba5b7f1 100644 --- a/flags/templatetags/feature_flags.py +++ b/flags/templatetags/feature_flags.py @@ -1,3 +1,4 @@ +import django from django import template from flags.state import ( @@ -8,16 +9,19 @@ register = template.Library() +if django.VERSION >= (1, 9): + simple_tag = register.simple_tag +else: + simple_tag = register.assignment_tag -# @register.simple_tag(takes_context=True) -@register.assignment_tag(takes_context=True) + +@simple_tag(takes_context=True) def flag_enabled(context, flag_name): request = context['request'] return base_flag_enabled(flag_name, request=request) -# @register.simple_tag(takes_context=True) -@register.assignment_tag(takes_context=True) +@simple_tag(takes_context=True) def flag_disabled(context, flag_name): request = context['request'] return base_flag_disabled(flag_name, request=request) diff --git a/flags/tests/settings.py b/flags/tests/settings.py index 376b56c..8b8bbec 100644 --- a/flags/tests/settings.py +++ b/flags/tests/settings.py @@ -2,6 +2,8 @@ import os +import wagtail + SECRET_KEY = 'not needed' @@ -25,7 +27,18 @@ INSTALLED_APPS = ( 'django.contrib.auth', 'django.contrib.contenttypes', - 'wagtail.wagtailcore', +) + +if wagtail.VERSION[0] >= 2: + INSTALLED_APPS += ( + 'wagtail.core', + ) +else: + INSTALLED_APPS += ( + 'wagtail.wagtailcore', + ) + +INSTALLED_APPS += ( 'flags', ) diff --git a/flags/tests/test_conditions.py b/flags/tests/test_conditions.py index 6f434e0..3523d6d 100644 --- a/flags/tests/test_conditions.py +++ b/flags/tests/test_conditions.py @@ -7,8 +7,6 @@ from django.test import TestCase from django.utils import timezone -from wagtail.wagtailcore.models import Site - from flags.conditions import ( CONDITIONS, RequiredForCondition, @@ -25,6 +23,12 @@ from flags.models import FlagState +try: + from wagtail.core.models import Site +except ImportError: + from wagtail.wagtailcore.models import Site + + class ConditionRegistryTestCase(TestCase): def test_register_decorator(self): diff --git a/flags/tests/test_state.py b/flags/tests/test_state.py index d4da06b..de44735 100644 --- a/flags/tests/test_state.py +++ b/flags/tests/test_state.py @@ -1,12 +1,16 @@ from django.http import HttpRequest from django.test import TestCase -from wagtail.wagtailcore.models import Site - from flags.models import FlagState from flags.state import flag_disabled, flag_enabled, flag_state +try: + from wagtail.core.models import Site +except ImportError: + from wagtail.wagtailcore.models import Site + + class FlagStateTestCase(TestCase): def setUp(self): self.site = Site.objects.get(is_default_site=True) diff --git a/flags/tests/test_template_functions.py b/flags/tests/test_template_functions.py index 922d516..16c5ad6 100644 --- a/flags/tests/test_template_functions.py +++ b/flags/tests/test_template_functions.py @@ -1,11 +1,15 @@ from django.http import HttpRequest from django.test import TestCase -from wagtail.wagtailcore.models import Site - from flags.template_functions import flag_disabled, flag_enabled +try: + from wagtail.core.models import Site +except ImportError: + from wagtail.wagtailcore.models import Site + + class TemplateFunctionsTestCase(TestCase): def setUp(self): self.site = Site.objects.get(is_default_site=True) diff --git a/flags/tests/test_templatetags_feature_flags.py b/flags/tests/test_templatetags_feature_flags.py index fe036b7..1135f11 100644 --- a/flags/tests/test_templatetags_feature_flags.py +++ b/flags/tests/test_templatetags_feature_flags.py @@ -2,7 +2,11 @@ from django.template import Context, Template from django.test import TestCase -from wagtail.wagtailcore.models import Site + +try: + from wagtail.core.models import Site +except ImportError: + from wagtail.wagtailcore.models import Site class FlagsTemplateTagsTestCase(TestCase): diff --git a/flags/tests/test_urls.py b/flags/tests/test_urls.py index 991a4c5..18b3375 100644 --- a/flags/tests/test_urls.py +++ b/flags/tests/test_urls.py @@ -1,9 +1,23 @@ -from django.conf.urls import include, url -from django.core.urlresolvers import resolve +from unittest import skipIf + from django.http import Http404, HttpResponse from django.test import RequestFactory, TestCase, override_settings -from flags.urls import flagged_url, flagged_urls + +try: + from django.urls import include, path, resolve, re_path +except ImportError: + from django.core.urlresolvers import resolve + from django.conf.urls import include, url as re_path + path = None + +try: + from flags.urls import flagged_path, flagged_re_path, flagged_re_paths +except ImportError: + from flags.urls import ( + flagged_url as flagged_re_path, + flagged_urls as flagged_re_paths + ) def view(request): @@ -15,55 +29,62 @@ def fallback(request): extra_patterns = [ - url(r'^included-url$', view), - url(r'^included-url-with-fallback$', view), + re_path(r'^included-url$', view), + re_path(r'^included-url-with-fallback$', view), ] fallback_patterns = [ - url(r'^included-url-with-fallback$', fallback), - url(r'^other-included-url$', fallback), + re_path(r'^included-url-with-fallback$', fallback), + re_path(r'^other-included-url$', fallback), ] urlpatterns = [ - flagged_url('FLAGGED_URL', r'^url-true-no-fallback$', view, - name='some-view', state=True), - flagged_url('FLAGGED_URL', r'^url-false-no-fallback$', view, - name='some-view', state=False), - flagged_url('FLAGGED_URL', r'^url-true-fallback$', view, - name='some-view', state=True, fallback=fallback), - flagged_url('FLAGGED_URL', r'^url-false-fallback$', view, - name='some-view', state=False, fallback=fallback), - - flagged_url('FLAGGED_URL', r'^include/', include(extra_patterns), - state=True), - flagged_url('FLAGGED_URL', r'^include-false/', include(extra_patterns), - state=False), - flagged_url('FLAGGED_URL', r'^include-fallback/', include(extra_patterns), - state=True, fallback=fallback), - flagged_url('FLAGGED_URL', r'^include-false-fallback/', - include(extra_patterns), state=True, fallback=fallback), - flagged_url('FLAGGED_URL', r'^include-fallback-include/', - include(extra_patterns), - state=True, fallback=include(fallback_patterns)), + flagged_re_path('FLAGGED_URL', r'^url-true-no-fallback$', view, + name='some-view', state=True), + flagged_re_path('FLAGGED_URL', r'^url-false-no-fallback$', view, + name='some-view', state=False), + flagged_re_path('FLAGGED_URL', r'^url-true-fallback$', view, + name='some-view', state=True, fallback=fallback), + flagged_re_path('FLAGGED_URL', r'^url-false-fallback$', view, + name='some-view', state=False, fallback=fallback), + + flagged_re_path('FLAGGED_URL', r'^include/', include(extra_patterns), + state=True), + flagged_re_path('FLAGGED_URL', r'^include-false/', include(extra_patterns), + state=False), + flagged_re_path('FLAGGED_URL', r'^include-fallback/', + include(extra_patterns), state=True, fallback=fallback), + flagged_re_path('FLAGGED_URL', r'^include-false-fallback/', + include(extra_patterns), state=True, fallback=fallback), + flagged_re_path('FLAGGED_URL', r'^include-fallback-include/', + include(extra_patterns), + state=True, fallback=include(fallback_patterns)), ] -with flagged_urls('FLAGGED_URL') as url: +with flagged_re_paths('FLAGGED_URL') as re_path: flagged_patterns_true_no_fallback = [ - url(r'^patterns-true-no-fallback$', view, name='some-view'), + re_path(r'^patterns-true-no-fallback$', view, name='some-view'), ] urlpatterns = urlpatterns + flagged_patterns_true_no_fallback -with flagged_urls('FLAGGED_URL', state=False) as url: +with flagged_re_paths('FLAGGED_URL', state=False) as re_path: flagged_patterns_false_no_fallback = [ - url(r'^patterns-false-no-fallback$', view, name='some-view'), + re_path(r'^patterns-false-no-fallback$', view, name='some-view'), ] urlpatterns = urlpatterns + flagged_patterns_false_no_fallback -with flagged_urls('FLAGGED_URL', fallback=fallback) as url: +with flagged_re_paths('FLAGGED_URL', fallback=fallback) as re_path: flagged_patterns_true_fallback = [ - url(r'^patterns-true-fallback$', view, name='some-view'), + re_path(r'^patterns-true-fallback$', view, name='some-view'), ] urlpatterns = urlpatterns + flagged_patterns_true_fallback +if path: + path_patterns = [ + flagged_path('FLAGGED_URL', 'path-true-no-fallback', view, + name='some-view', state=True), + ] + urlpatterns = urlpatterns + path_patterns + @override_settings( ROOT_URLCONF=__name__, @@ -89,6 +110,18 @@ def test_flagged_url_true_no_fallback_false(self): with self.assertRaises(Http404): self.get_url_response('/url-true-no-fallback') + @skipIf(not path, "Skipping test for Django 2.0 path() patterns") + @override_settings(FLAGS={'FLAGGED_URL': {'boolean': True}}) + def test_flagged_path_true_no_fallback(self): + response = self.get_url_response('/path-true-no-fallback') + self.assertContains(response, 'view') + + @skipIf(not path, "Skipping test for Django 2.0 path() patterns") + @override_settings(FLAGS={'FLAGGED_URL': {'boolean': False}}) + def test_flagged_path_true_no_fallback_false(self): + with self.assertRaises(Http404): + self.get_url_response('/path-true-no-fallback') + @override_settings(FLAGS={'FLAGGED_URL': {'boolean': False}}) def test_flagged_url_false_no_fallback(self): response = self.get_url_response('/url-false-no-fallback') @@ -176,7 +209,7 @@ def test_flagged_url_false_include_fallback_include_nonmatching_url(self): def test_flagged_url_not_callable(self): with self.assertRaises(TypeError): - flagged_url('MY_FLAG', r'^my_url/$', 'string') + flagged_re_path('MY_FLAG', r'^my_url/$', 'string') @override_settings(FLAGS={'FLAGGED_URL': {'boolean': True}}) def test_flagged_urls_cm_true_no_fallback(self): diff --git a/flags/tests/test_views.py b/flags/tests/test_views.py index 2619477..45e6d61 100644 --- a/flags/tests/test_views.py +++ b/flags/tests/test_views.py @@ -3,12 +3,17 @@ from django.test import TestCase, override_settings from django.views.generic import View -from wagtail.wagtailcore.models import Page, Site -from wagtail.wagtailcore.views import serve as wagtail_serve - from flags.views import FlaggedViewMixin +try: + from wagtail.core.models import Page, Site + from wagtail.core.views import serve as wagtail_serve +except ImportError: + from wagtail.wagtailcore.models import Page, Site + from wagtail.wagtailcore.views import serve as wagtail_serve + + class TestView(FlaggedViewMixin, View): def get(self, request, *args, **kwargs): return HttpResponse('ok') diff --git a/flags/urls.py b/flags/urls.py index 8fee032..a2c2c2b 100644 --- a/flags/urls.py +++ b/flags/urls.py @@ -1,21 +1,26 @@ from contextlib import contextmanager +from functools import partial + +import django from flags.decorators import flag_check try: - from django.urls import ( - RegexURLPattern, - RegexURLResolver + from django.urls.resolvers import ( + RegexPattern, + RoutePattern, + URLPattern, + URLResolver ) except ImportError: from django.core.urlresolvers import ( - RegexURLPattern, - RegexURLResolver + RegexURLPattern as URLPattern, + RegexURLResolver as URLResolver ) -class FlaggedURLResolver(RegexURLResolver): +class FlaggedURLResolver(URLResolver): def __init__(self, flag_name, regex, urlconf_name, default_kwargs=None, app_name=None, namespace=None, state=True, fallback=None): @@ -27,9 +32,10 @@ def __init__(self, flag_name, regex, urlconf_name, self.state = state self.fallback = fallback self.fallback_patterns = [] + if isinstance(self.fallback, (list, tuple)): urlconf_module, app_name, namespace = self.fallback - self.fallback_patterns = RegexURLResolver( + self.fallback_patterns = URLResolver( regex, urlconf_module, None, app_name=app_name, namespace=namespace ).url_patterns @@ -45,44 +51,90 @@ def url_patterns(self): # the list of fallback patterns. fallback = self.fallback if isinstance(self.fallback, (list, tuple)): - fallback = next((p.callback for p in self.fallback_patterns - if p.regex == pattern.regex), None) + if django.VERSION[0] >= 2: + fallback = next(( + p.callback for p in self.fallback_patterns + if p.pattern.describe() == pattern.pattern.describe() + ), None) + else: + fallback = next(( + p.callback for p in self.fallback_patterns + if p.regex == pattern.regex + ), None) flag_decorator = flag_check(self.flag_name, self.state, fallback=fallback) - flagged_pattern = RegexURLPattern( - pattern.regex.pattern, flag_decorator(pattern.callback), + + if django.VERSION[0] >= 2: + route_pattern = pattern.pattern + else: + route_pattern = pattern.regex.pattern + + flagged_pattern = URLPattern( + route_pattern, flag_decorator(pattern.callback), pattern.default_args, pattern.name) + url_patterns.append(flagged_pattern) # Next, add "negatively" flagged URLs, where the flag does not match # the defined state, for any remaining fallback patterns that didn't # match other url patterns. - url_pattern_regexes = [p.regex for p in url_patterns] - for pattern in (p for p in self.fallback_patterns - if p.regex not in url_pattern_regexes): + if django.VERSION[0] >= 2: + # Django >= 2.0 + described_patterns = [p.pattern.describe() for p in url_patterns] + negative_patterns = ( + p for p in self.fallback_patterns + if p.pattern.describe() not in described_patterns + ) + else: + described_patterns = [p.regex for p in url_patterns] + negative_patterns = ( + p for p in self.fallback_patterns + if p.regex not in described_patterns + ) + + for pattern in negative_patterns: flag_decorator = flag_check(self.flag_name, not self.state) - flagged_pattern = RegexURLPattern( - pattern.regex.pattern, flag_decorator(pattern.callback), + + if django.VERSION[0] >= 2: + route_pattern = pattern.pattern + else: + route_pattern = pattern.regex.pattern + + flagged_pattern = URLPattern( + route_pattern, flag_decorator(pattern.callback), pattern.default_args, pattern.name) + url_patterns.append(flagged_pattern) return url_patterns -def flagged_url(flag_name, regex, view, kwargs=None, name=None, - state=True, fallback=None): +def _flagged_path(flag_name, route, view, kwargs=None, name=None, + state=True, fallback=None, Pattern=None): """ Make a URL depend on the state of a feature flag """ if callable(view): flagged_view = flag_check(flag_name, state, fallback=fallback)(view) - return RegexURLPattern(regex, flagged_view, kwargs, name) + + if Pattern: + route_pattern = Pattern(route, name=name, is_endpoint=True) + else: + route_pattern = route + + return URLPattern(route_pattern, flagged_view, kwargs, name) elif isinstance(view, (list, tuple)): urlconf_module, app_name, namespace = view + + if Pattern: + route_pattern = Pattern(route, name=name, is_endpoint=True) + else: + route_pattern = route + return FlaggedURLResolver( - flag_name, regex, urlconf_module, kwargs, + flag_name, route_pattern, urlconf_module, kwargs, app_name=app_name, namespace=namespace, state=state, fallback=fallback) @@ -91,10 +143,20 @@ def flagged_url(flag_name, regex, view, kwargs=None, name=None, @contextmanager -def flagged_urls(flag_name, state=True, fallback=None): +def _flagged_paths(flag_name, state=True, fallback=None, Pattern=None): """ Flag multiple URLs in the same context Returns a url()-compatible wrapper for flagged_url() """ - def flagged_url_wrapper(regex, view, kwargs=None, name=None): - return flagged_url(flag_name, regex, view, kwargs=kwargs, name=name, - state=state, fallback=fallback) + def flagged_url_wrapper(route, view, kwargs=None, name=None): + return _flagged_path(flag_name, route, view, kwargs=kwargs, name=name, + state=state, fallback=fallback, Pattern=Pattern) yield flagged_url_wrapper + + +if django.VERSION[0] >= 2: + flagged_path = partial(_flagged_path, Pattern=RoutePattern) + flagged_re_path = partial(_flagged_path, Pattern=RegexPattern) + flagged_paths = partial(_flagged_paths, Pattern=RoutePattern) + flagged_re_paths = partial(_flagged_paths, Pattern=RegexPattern) +else: + flagged_url = partial(_flagged_path, Pattern=None) + flagged_urls = partial(_flagged_paths, Pattern=None) diff --git a/flags/wagtail_hooks.py b/flags/wagtail_hooks.py index 18cfc34..3b1acdb 100644 --- a/flags/wagtail_hooks.py +++ b/flags/wagtail_hooks.py @@ -1,12 +1,21 @@ from django.conf.urls import include, url -from django.core.urlresolvers import reverse - -from wagtail.wagtailadmin.menu import MenuItem -from wagtail.wagtailcore import hooks from flags import views +try: + from django.urls import reverse +except ImportError: + from django.core.urlresolvers import reverse + +try: + from wagtail.admin.menu import MenuItem + from wagtail.core import hooks +except ImportError: + from wagtail.wagtailadmin.menu import MenuItem + from wagtail.wagtailcore import hooks + + @hooks.register('register_settings_menu_item') def register_flags_menu(): return MenuItem('Flags', reverse('flagadmin:list'), @@ -17,10 +26,10 @@ def register_flags_menu(): def register_flag_admin_urls(): return [ url(r'^flags/', - include([ + include(([ url(r'^$', views.index, name='list'), url(r'^(\d+)/delete/$', views.delete, name='delete'), url(r'^create/$', views.create, name='create'), - ], namespace='flagadmin')) + ], 'flags'), namespace='flagadmin')) ] diff --git a/setup.py b/setup.py index f5f5e1c..ca5538f 100644 --- a/setup.py +++ b/setup.py @@ -8,8 +8,8 @@ install_requires = [ - 'Django>=1.8,<1.12', - 'wagtail>=1.10,<1.14', + 'Django>=1.8,<2.1', + 'wagtail>=1.10,<2.1', ] diff --git a/tox.ini b/tox.ini index 1063f07..e5778a9 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] skipsdist=True -envlist=py{27,36}-dj{18,111}-wag{110,112,113},lint +envlist=lint,py{27,36}-dj{18,111}-wag{112,113},py{36}-dj{20}-wag{20} [testenv] install_command=pip install -e ".[testing]" -U {opts} {packages} @@ -17,9 +17,10 @@ basepython= deps= dj18: Django>=1.8,<1.9 dj111: Django>=1.11,<1.12 - wag110: wagtail>=1.10,<1.11 + dj20: Django>=2.0,<2.1 wag112: wagtail>=1.12,<1.13 wag113: wagtail>=1.13,<1.14 + wag20: wagtail>=2.0,<2.1 [testenv:lint] basepython=python3.6 From 9a719a5443411733503d574b3bff0a221f6bb8cd Mon Sep 17 00:00:00 2001 From: Will Barton Date: Fri, 9 Mar 2018 15:58:48 -0500 Subject: [PATCH 2/5] Document Django 2.0/Wagtail 2.0 compatibility --- README.md | 95 +++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 71 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index f7cfb2f..e34fe2f 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Feature flags allow you to toggle functionality in both Django settings and the - [API](#api) - [Flag state](#flag-state) - [Flag decorators](#flag-decorators) - - [Flagged URLs](#flagged-urls) + - [Flagged URL patterns](#flagged-url-patterns) - [Django templates](#django-templates) - [Jinja2 templates](#jinja2-templates) - [Conditions](#conditions) @@ -29,13 +29,13 @@ Feature flags allow you to toggle functionality in both Django settings and the ## Dependencies -- Django 1.8+ -- Wagtail 1.10+ +- Django 1.8+ (including Django 2.0) +- Wagtail 1.10+ (including Wagtail 2.0) - Python 2.7+, 3.6+ ## Installation -1. Install wagtail-flags using pip: +1. Install wagtail-flags: ```shell pip install wagtail-flags @@ -86,12 +86,25 @@ Then use the flag in a Django template (`mytemplate.html`): Configure a URL for that template (`urls.py`): +Django 2.0: + +```python +from django.urls import path +from django.views.generic import TemplateView + +urlpatterns = [ + path(r'mypage/', TemplateView.as_view(template_name='mytemplate.html')), +] +``` + +Django 1.x: + ```python from django.conf.urls import url -from django.views.generic.base import TemplateView +from django.views.generic import TemplateView urlpatterns = [ - url(r'^/mypage$', TemplateView.as_view(template_name='mytemplate.html')), + url(r'^mypage/$', TemplateView.as_view(template_name='mytemplate.html')), ] ``` @@ -99,7 +112,6 @@ Then in the Wagtail admin add conditions for the flag in "Settings", "Flags": ![Creating conditions in the Wagtail admin](https://raw.githubusercontent.com/cfpb/wagtail-flags/master/screenshot_create.png) - Then visiting the URL `/mypage?enable_my_flag=True` should show you the flagged `
` in the template. ### Adding flags @@ -155,10 +167,20 @@ Jinja2 templates (after [adding `flag_enabled` to the Jinja2 environment](#jinja {% endif %} ``` -And Django `urls.py`: +Django 2.0 `urls.py`: ```python -from flags.urls import flagged_url, flagged_urls +from flags.urls import flagged_path + +urlpatterns = [ + flagged_path('MY_FLAG', 'an-url/', view_requiring_flag, state=True), +] +``` + +And Django 1.x `urls.py`: + +```python +from flags.urls import flagged_url urlpatterns = [ flagged_url('MY_FLAG', r'^an-url$', view_requiring_flag, state=True), @@ -168,7 +190,6 @@ urlpatterns = [ See the [API documentation below](#api) for more details and examples. - #### Built-in conditions Wagtail-Flags comes with the following conditions built-in: @@ -318,17 +339,33 @@ def view_with_fallback(request): return HttpResponse('flag was set') ``` -### Flagged URLs +### Flagged URL patterns + +Flagged URL patterns are an alternative to [flagging views with decorators](https://github.com/cfpb/wagtail-flags#flag_checkflag_name-state-fallbacknone-kwargs). + +Django 2.0+: ```python -from flags.urls import flagged_url, flagged_urls +from flags.urls import flagged_path, flagged_paths, flagged_re_path, flagged_re_paths ``` -Flagged URLs are an alternative to [flagging views with decorators](https://github.com/cfpb/wagtail-flags#flag_checkflag_name-state-fallbacknone-kwargs). +Django 1.x: + +```python +from flags.urls import flagged_url, flagged_urls +``` +#### `flagged_path(flag_name, route, view, kwargs=None, name=None, state=True, fallback=None)` +#### `flagged_re_path(flag_name, route, view, kwargs=None, name=None, state=True, fallback=None)` #### `flagged_url(flag_name, regex, view, kwargs=None, name=None, state=True, fallback=None)` -Make a URL depend on the state of a feature flag. `flagged_url()` can be used in place of Django's `url()`. +Make a URL depend on the state of a feature flag. + +`flagged_path()` can be used in place of [Django's `path()`](https://docs.djangoproject.com/en/2.0/ref/urls/#django.urls.path). + +`flagged_re_path()` can be used in place of [Django's `re_path()`](https://docs.djangoproject.com/en/2.0/ref/urls/#django.urls.re_path). + +`flagged_url()` *is only available with Django 1.x* and can be used in place of [Django's `url()`](https://docs.djangoproject.com/en/1.11/ref/urls/#django.conf.urls.url). The `view` and the `fallback` can both be a set of `include()`ed patterns but any matching URL patterns in the includes must match *exactly* in terms of regular expression, keyword arguments, and name, otherwise a `404` may be unexpectedly raised. @@ -336,24 +373,34 @@ If a `fallback` is not given the flagged url will raise a `404` if the flag stat ```python urlpatterns = [ - flagged_url('MY_FLAG', r'^an-url$', view_requiring_flag, state=True), - flagged_url('MY_FLAG_WITH_FALLBACK', r'^another-url$', view_with_fallback, - state=True, fallback=other_view) - flagged_url('MY_FLAGGED_INCLUDE', r'^myapp$', include('myapp.urls'), - state=True, fallback=other_view) - flagged_url('MY_NEW_APP_FLAG', r'^mynewapp$', include('mynewapp.urls'), - state=True, fallback=include('myoldapp.urls')) + flagged_path('MY_FLAG', r'an-url/', view_requiring_flag, state=True), + flagged_re_path('MY_FLAG_WITH_FALLBACK', r'^another-url$', + view_with_fallback, state=True, fallback=other_view) + flagged_path('MY_FLAGGED_INCLUDE', 'myapp/', include('myapp.urls'), + state=True, fallback=other_view) + flagged_re_path('MY_NEW_APP_FLAG', r'^mynewapp$', include('mynewapp.urls'), + state=True, fallback=include('myoldapp.urls')) ] ``` +#### `flagged_paths(flag_name, state=True, fallback=None)` +#### `flagged_re_paths(flag_name, state=True, fallback=None)` #### `flagged_urls(flag_name, state=True, fallback=None)` -Flag multiple URLs in the same context. Returns function that can be used in place of Django's `url()` that wraps `flagged_url()`. Can take an optional fallback view that will apply to all urls. +Flag multiple URLs in the same context with a context manager. + +`flagged_paths()` returns a function that takes the same arguments as [Django's `path()`](https://docs.djangoproject.com/en/2.0/ref/urls/#django.urls.path) and which will flag the pattern's view. + +`flagged_re_paths()` returns a function that takes the same arguments as [Django's `re_path()`](https://docs.djangoproject.com/en/2.0/ref/urls/#django.urls.re_path) and which will flag the pattern's view. + +`flagged_urls()` *is only available with Django 1.x* and returns a function that takes the same arguments as [Django's `url()`](https://docs.djangoproject.com/en/1.11/ref/urls/#django.conf.urls.url). + +Returns function that can be used in place of Django's `url()` that wraps `flagged_url()`. Can take an optional fallback view that will apply to all urls. ```python -with flagged_urls('MY_FLAG') as url: +with flagged_paths('MY_FLAG') as path: flagged_url_patterns = [ - url(r'^an-url$', view_requiring_flag), + path('an-url/', view_requiring_flag), ] urlpatterns = urlpatterns + flagged_url_patterns From cc89741269c8b0920671a0f2428a739c777ef731 Mon Sep 17 00:00:00 2001 From: Will Barton Date: Mon, 12 Mar 2018 09:37:28 -0400 Subject: [PATCH 3/5] Bump version to 2.1.0 and add classifiers --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ca5538f..44be416 100644 --- a/setup.py +++ b/setup.py @@ -27,7 +27,7 @@ description='Feature flags for Wagtail sites', long_description=long_description, license='CC0', - version='2.0.8', + version='2.1.0', include_package_data=True, packages=find_packages(), install_requires=install_requires, @@ -38,6 +38,7 @@ 'Framework :: Django', 'Framework :: Django :: 1.11', 'Framework :: Django :: 1.8', + 'Framework :: Django :: 2.0', 'License :: CC0 1.0 Universal (CC0 1.0) Public Domain Dedication', 'License :: Public Domain', 'Programming Language :: Python', From b9fb165e24251cd7ba175a65c1f1ed269e8a57d4 Mon Sep 17 00:00:00 2001 From: Will Barton Date: Wed, 25 Apr 2018 11:23:23 -0400 Subject: [PATCH 4/5] Replace abominable grammar --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e34fe2f..f9e479a 100644 --- a/README.md +++ b/README.md @@ -173,7 +173,7 @@ Django 2.0 `urls.py`: from flags.urls import flagged_path urlpatterns = [ - flagged_path('MY_FLAG', 'an-url/', view_requiring_flag, state=True), + flagged_path('MY_FLAG', 'a-url/', view_requiring_flag, state=True), ] ``` @@ -183,7 +183,7 @@ And Django 1.x `urls.py`: from flags.urls import flagged_url urlpatterns = [ - flagged_url('MY_FLAG', r'^an-url$', view_requiring_flag, state=True), + flagged_url('MY_FLAG', r'^a-url$', view_requiring_flag, state=True), ] ``` @@ -373,7 +373,7 @@ If a `fallback` is not given the flagged url will raise a `404` if the flag stat ```python urlpatterns = [ - flagged_path('MY_FLAG', r'an-url/', view_requiring_flag, state=True), + flagged_path('MY_FLAG', r'a-url/', view_requiring_flag, state=True), flagged_re_path('MY_FLAG_WITH_FALLBACK', r'^another-url$', view_with_fallback, state=True, fallback=other_view) flagged_path('MY_FLAGGED_INCLUDE', 'myapp/', include('myapp.urls'), @@ -400,7 +400,7 @@ Returns function that can be used in place of Django's `url()` that wraps `flagg ```python with flagged_paths('MY_FLAG') as path: flagged_url_patterns = [ - path('an-url/', view_requiring_flag), + path('a-url/', view_requiring_flag), ] urlpatterns = urlpatterns + flagged_url_patterns From 426adbd5774eb213c3951b347ab5c16ae29d3f8c Mon Sep 17 00:00:00 2001 From: Will Barton Date: Wed, 25 Apr 2018 12:07:07 -0400 Subject: [PATCH 5/5] Add Wagtail trove classifiers --- setup.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/setup.py b/setup.py index 44be416..3c61382 100644 --- a/setup.py +++ b/setup.py @@ -39,6 +39,9 @@ 'Framework :: Django :: 1.11', 'Framework :: Django :: 1.8', 'Framework :: Django :: 2.0', + 'Framework :: Wagtail', + 'Framework :: Wagtail :: 1', + 'Framework :: Wagtail :: 2', 'License :: CC0 1.0 Universal (CC0 1.0) Public Domain Dedication', 'License :: Public Domain', 'Programming Language :: Python',