diff --git a/.github/workflows/pythonpublish.yml b/.github/workflows/pythonpublish.yml
new file mode 100644
index 0000000..4e1ef42
--- /dev/null
+++ b/.github/workflows/pythonpublish.yml
@@ -0,0 +1,31 @@
+# This workflows will upload a Python Package using Twine when a release is created
+# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries
+
+name: Upload Python Package
+
+on:
+ release:
+ types: [created]
+
+jobs:
+ deploy:
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v2
+ - name: Set up Python
+ uses: actions/setup-python@v2
+ with:
+ python-version: '3.x'
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install setuptools wheel twine
+ - name: Build and publish
+ env:
+ TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
+ TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
+ run: |
+ python setup.py sdist bdist_wheel
+ twine upload dist/*
diff --git a/.gitignore b/.gitignore
index 2bdfe29..c1f58df 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,3 +16,4 @@ tests/db.sqlite*
.DS_Store
.pytest_cache
/tests/local.db
+/.venv
diff --git a/.travis.yml b/.travis.yml
index 9c43239..b878951 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,63 +1,26 @@
language: python
sudo: false
+language: python
cache: pip
python:
- "2.7"
- - "3.3"
- - "3.4"
- "3.5"
- - "pypy"
-matrix:
- include:
- - python: "3.6"
- env: DJANGO="Django>=1.11,<1.12"
- - python: "3.6"
- env: DJANGO="Django>=2.0,<2.1"
- - python: "3.6"
- env: DJANGO="Django>=2.1,<2.2" PYTEST_DJANGO="pytest-django==3.4.2"
- # - python: "3.7"
- # env: DJANGO="Django>=2.0,<2.1"
- # - python: "3.7"
- # env: DJANGO="Django>=2.1,<2.2" PYTEST_DJANGO="pytest-django==3.4.2"
- exclude:
- - python: "2.7"
- env: DJANGO="Django>=2.0,<2.1"
- - python: "2.7"
- env: DJANGO="Django>=2.1,<2.2" PYTEST_DJANGO="pytest-django==3.4.2"
- - python: "3.5"
- env: DJANGO="Django>=1.7,<1.8"
- - python: "3.3"
- env: DJANGO="Django>=1.9,<1.10"
- - python: "3.3"
- env: DJANGO="Django>=1.10,<1.11"
- - python: "3.3"
- env: DJANGO="Django>=1.11,<1.12"
- - python: "3.3"
- env: DJANGO="Django>=2.0,<2.1"
- - python: "pypy"
- env: DJANGO="Django>=2.0,<2.1"
- - python: "3.3"
- env: DJANGO="Django>=2.1,<2.2" PYTEST_DJANGO="pytest-django==3.4.2"
- - python: "3.4"
- env: DJANGO="Django>=2.1,<2.2" PYTEST_DJANGO="pytest-django==3.4.2"
- - python: "pypy"
- env: DJANGO="Django>=2.1,<2.2" PYTEST_DJANGO="pytest-django==3.4.2"
+ - "3.6"
+ - "3.7"
+ - "3.8"
+ - "pypy3"
+
env:
- global:
- - PYTEST_DJANGO=pytest-django==2.9.1
matrix:
- - DJANGO="Django>=1.7,<1.8"
- - DJANGO="Django>=1.8,<1.9"
- - DJANGO="Django>=1.9,<1.10"
- - DJANGO="Django>=1.10,<1.11"
- - DJANGO="Django>=1.11,<1.12"
- - DJANGO="Django>=2.0,<2.1"
- - DJANGO="Django>=2.1,<2.2" PYTEST_DJANGO="pytest-django==3.4.2"
-install:
- - pip install $DJANGO
- - pip install -e .[test] $PYTEST_DJANGO
-script:
- - coverage run -m py.test advanced_filters
- - pep8 --exclude=*urls.py --exclude=*migrations advanced_filters -v
+ - DJANGO="1.9"
+ - DJANGO="1.10"
+ - DJANGO="1.11"
+ - DJANGO="2.0"
+ - DJANGO="2.1"
+ - DJANGO="2.2"
+ - DJANGO="3.0"
+
+install: pip install tox-travis coveralls
+script: tox
after_success:
coveralls
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 3a296d1..d23d00e 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,6 +1,61 @@
Changelog
=========
+1.2.0 - Django 3 and more
+-------------------------
+
+It's finally time to drop the dirty old rags and don some fresh colors.
+
+Thanks to effort from multiple contributors, this version includes support
+for newest Django version.
+
+Breaking Changes
+~~~~~~~~~~~~~~~~
+
+* Add support for Django 2.2 and 3.0
+* Drop support for Django < 1.9
+* Drop support for Python 3.3-3.4
+
+*django-advanced-filters now support only* **python 2.7, and 3.5 - 3.8.**
+
+Features
+~~~~~~~~
+
+- Switch deprecated force_text to force_str (Merge 0427d11)
+
+Bug fixes
+~~~~~~~~~
+
+- Avoid installing newer braces (Merge 0427d11)
+- Allow choices sort on None fields (Merge 142ecd0)
+
+Docs / Tests
+~~~~~~~~~~~~
+
+- Update dependencies stated in the README
+- Refactor some unittest test cases into pytest (Merge 41271b7)
+- Test the CleanWhiteSpacesMixin helper
+
+Misc
+~~~~
+
+- Update requirements for new test deps matrix (Merge 0427d11)
+- Replace deprecated assertEquals (Merge 41271b7)
+- Replace deprecated logger.warn with warning (Merge 41271b7)
+- Bump test dependencies (Merge 41271b7)
+- Update python and add Django classifiers
+
+
+Contributors
+~~~~~~~~~~~~
+
+- Petr DlouhĂ˝
+- Alon Raizman
+- Hugo Maingonnat
+- Arpit
+- Pavel Savchenko
+
+
1.1.1 - CHANGELOG rendering is hard
-----------------------------------
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index 9414602..a405398 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -26,10 +26,10 @@ Pull Request Process
other developer, or if you do not have permission to do that, you may
request the reviewer to merge it for you.
-Release process
----------------
+Manual Release process
+----------------------
-1. Prepeare the changelog and amend the CHANGELOG.rst
+1. Prepare the changelog and amend the CHANGELOG.rst
2. Increase the version numbers in any examples files and the README.rst
to the new version that this Pull Request would represent. The
versioning scheme we use is `SemVer `__.
diff --git a/README.rst b/README.rst
index db24ca4..1200ab2 100644
--- a/README.rst
+++ b/README.rst
@@ -14,7 +14,7 @@ admin.
Mimics the advanced search feature in
`VTiger `__, `see here for more
-info `__
+info `__
.. figure:: https://raw.githubusercontent.com/modlinltd/django-advanced-filters/develop/screenshot.png
:alt: Creating via a modal
@@ -26,16 +26,16 @@ For release notes, see `Changelog = 1.7 (Django 1.7 - 2.1 on Python 2/3/PyPy2)
-- django-braces == 1.4.0
-- simplejson == 3.6.5
+- Django >= 1.9 (Django 1.9 - 3.0 on Python 2/3/PyPy3)
+- django-braces >= 1.4, < 1.14.0
+- simplejson >= 3.6.5, < 4
Installation & Set up
=====================
1. Install from pypi: ``pip install django-advanced-filters``
-2. Add both ``'advanced_filters'`` to ``INSTALLED_APPS``.
+2. Add ``'advanced_filters'`` to ``INSTALLED_APPS``.
3. Add ``url(r'^advanced_filters/', include('advanced_filters.urls'))``
to your project's urlconf.
4. Run ``python manage.py syncdb`` or ``python manage.py migrate`` (for django >= 1.7)
diff --git a/advanced_filters/__init__.py b/advanced_filters/__init__.py
index b3ddbc4..58d478a 100644
--- a/advanced_filters/__init__.py
+++ b/advanced_filters/__init__.py
@@ -1 +1 @@
-__version__ = '1.1.1'
+__version__ = '1.2.0'
diff --git a/advanced_filters/form_helpers.py b/advanced_filters/form_helpers.py
index 7bb405b..681be77 100644
--- a/advanced_filters/form_helpers.py
+++ b/advanced_filters/form_helpers.py
@@ -3,11 +3,11 @@
from django import forms
-from django.utils import six
+import six
logger = logging.getLogger('advanced_filters.form_helpers')
-extra_spaces_pattern = re.compile('\s+')
+extra_spaces_pattern = re.compile(r'\s+')
class VaryingTypeCharField(forms.CharField):
diff --git a/advanced_filters/forms.py b/advanced_filters/forms.py
index 9068f74..726622b 100644
--- a/advanced_filters/forms.py
+++ b/advanced_filters/forms.py
@@ -15,7 +15,7 @@
from django.forms.formsets import formset_factory, BaseFormSet
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _
-from django.utils.six.moves import range, reduce
+from six.moves import range, reduce
from django.utils.text import capfirst
import django
@@ -253,17 +253,18 @@ def get_fields_from_model(self, model, fields):
"""
model_fields = {}
for field in fields:
- if isinstance(field, tuple) and len(field) == 2:
- field, verbose_name = field[0], field[1]
- else:
- try:
- model_field = get_fields_from_path(model, field)[-1]
- verbose_name = model_field.verbose_name
- except (FieldDoesNotExist, IndexError, TypeError) as e:
- logger.warn("AdvancedFilterForm: skip invalid field "
- "- %s", e)
- continue
- model_fields[field] = verbose_name
+ if isinstance(field, tuple) and len(field) == 2:
+ field, verbose_name = field[0], field[1]
+ else:
+ try:
+ model_field = get_fields_from_path(model, field)[-1]
+ verbose_name = model_field.verbose_name
+ except (FieldDoesNotExist, IndexError, TypeError) as e:
+ logger.warning(
+ "AdvancedFilterForm: skip invalid field - %s", e
+ )
+ continue
+ model_fields[field] = verbose_name
return model_fields
def __init__(self, *args, **kwargs):
diff --git a/advanced_filters/q_serializer.py b/advanced_filters/q_serializer.py
index a30a6a0..c0bc668 100644
--- a/advanced_filters/q_serializer.py
+++ b/advanced_filters/q_serializer.py
@@ -3,7 +3,7 @@
import base64
import time
-from django.utils import six
+import six
from django.db.models import Q
from django.core.serializers.base import SerializationError
diff --git a/advanced_filters/templates/admin/advanced_filters/change_form.html b/advanced_filters/templates/admin/advanced_filters/change_form.html
index e64920d..5504945 100644
--- a/advanced_filters/templates/admin/advanced_filters/change_form.html
+++ b/advanced_filters/templates/admin/advanced_filters/change_form.html
@@ -1,6 +1,6 @@
{% extends "admin/change_form.html" %}
-{% load i18n admin_static admin_modify admin_urls %}
+{% load i18n static admin_modify admin_urls %}
{% block extrastyle %}
{{ adminform.media.css }}
diff --git a/advanced_filters/tests/__init__.py b/advanced_filters/tests/__init__.py
index 8d26a51..e69de29 100644
--- a/advanced_filters/tests/__init__.py
+++ b/advanced_filters/tests/__init__.py
@@ -1,3 +0,0 @@
-from .test_models import *
-from .test_q_serializer import *
-from .test_views import *
diff --git a/advanced_filters/tests/conftest.py b/advanced_filters/tests/conftest.py
new file mode 100644
index 0000000..6db2510
--- /dev/null
+++ b/advanced_filters/tests/conftest.py
@@ -0,0 +1,13 @@
+import pytest
+from tests.factories import SalesRepFactory
+
+
+@pytest.fixture
+def user(db):
+ return SalesRepFactory()
+
+
+@pytest.fixture()
+def client(client, user):
+ client.force_login(user)
+ return client
diff --git a/advanced_filters/tests/factories.py b/advanced_filters/tests/factories.py
new file mode 100644
index 0000000..f551702
--- /dev/null
+++ b/advanced_filters/tests/factories.py
@@ -0,0 +1,10 @@
+import factory
+
+from tests.factories import SalesRepFactory
+
+
+class AdvancedFilterFactory(factory.django.DjangoModelFactory):
+ model = 'customers.Client'
+
+ class Meta:
+ model = 'advanced_filters.AdvancedFilter'
diff --git a/advanced_filters/tests/test_admin.py b/advanced_filters/tests/test_admin.py
deleted file mode 100644
index 320d5ff..0000000
--- a/advanced_filters/tests/test_admin.py
+++ /dev/null
@@ -1,176 +0,0 @@
-try:
- from django.urls import reverse
-except ImportError: # Django < 2.0
- from django.core.urlresolvers import reverse
-from django.contrib.auth.models import Permission
-from django.db.models import Q
-from django.test import TestCase
-
-from ..models import AdvancedFilter
-from ..admin import AdvancedListFilters
-from tests import factories
-
-
-class ChageFormAdminTest(TestCase):
- """ Test the AdvancedFilter admin change page """
- def setUp(self):
- self.user = factories.SalesRep()
- assert self.client.login(username='user', password='test')
- self.a = AdvancedFilter(title='test', url='test', created_by=self.user,
- model='customers.Client')
- self.a.query = Q(email__iexact='a@a.com')
- self.a.save()
-
- def test_change_page_requires_perms(self):
- url = reverse('admin:advanced_filters_advancedfilter_change',
- args=(self.a.pk,))
- res = self.client.get(url)
- assert res.status_code == 403
-
- def test_change_page_renders(self):
- self.user.user_permissions.add(Permission.objects.get(
- codename='change_advancedfilter'))
- url = reverse('admin:advanced_filters_advancedfilter_change',
- args=(self.a.pk,))
-
- with self.settings(ADVANCED_FILTER_EDIT_BY_USER=False):
- res = self.client.get(url)
- assert res.status_code == 200
-
- def test_change_and_goto(self):
- self.user.user_permissions.add(Permission.objects.get(
- codename='change_advancedfilter'))
- url = reverse('admin:advanced_filters_advancedfilter_change',
- args=(self.a.pk,))
- form_data = {'form-TOTAL_FORMS': 1, 'form-INITIAL_FORMS': 0,
- '_save_goto': 1}
- with self.settings(ADVANCED_FILTER_EDIT_BY_USER=False):
- res = self.client.post(url, data=form_data)
- assert res.status_code == 302
- url = res['location']
- assert url.endswith('admin/customers/client/?_afilter=1')
-
- def test_create_page_disabled(self):
- self.user.user_permissions.add(Permission.objects.get(
- codename='add_advancedfilter'))
- url = reverse('admin:advanced_filters_advancedfilter_add')
- res = self.client.get(url)
- assert res.status_code == 403
-
-
-class AdvancedFilterCreationTest(TestCase):
- """ Test creation of AdvancedFilter in target model changelist """
- form_data = {'form-TOTAL_FORMS': 1, 'form-INITIAL_FORMS': 0,
- 'action': 'advanced_filters'}
- good_data = {'title': 'Test title', 'form-0-field': 'language',
- 'form-0-operator': 'iexact', 'form-0-value': 'ru', }
- query = ['language__iexact', 'ru']
-
- def setUp(self):
- self.user = factories.SalesRep()
- assert self.client.login(username='user', password='test')
-
- def test_changelist_includes_form(self):
- self.user.user_permissions.add(Permission.objects.get(
- codename='change_client'))
- url = reverse('admin:customers_client_changelist')
- with self.settings(ADVANCED_FILTER_EDIT_BY_USER=False):
- res = self.client.get(url)
- assert res.status_code == 200
- title = ['Create advanced filter']
- fields = ['First name', 'Language', 'Sales Rep']
- # python >= 3.3 support
- response_content = res.content.decode('utf-8')
- for part in title + fields:
- assert part in response_content
-
- def test_create_form_validation(self):
- self.user.user_permissions.add(Permission.objects.get(
- codename='change_client'))
- url = reverse('admin:customers_client_changelist')
- form_data = self.form_data.copy()
- res = self.client.post(url, data=form_data)
- assert res.status_code == 200
- form = res.context_data['advanced_filters']
- assert 'title' in form.errors
- assert '__all__' in form.errors
- assert form.errors['title'] == ['This field is required.']
- assert form.errors['__all__'] == ['Error validating filter forms']
-
- def test_create_form_valid(self):
- self.user.user_permissions.add(Permission.objects.get(
- codename='change_client'))
- url = reverse('admin:customers_client_changelist')
- form_data = self.form_data.copy()
- form_data.update(self.good_data)
- res = self.client.post(url, data=form_data)
- assert res.status_code == 200
- form = res.context_data['advanced_filters']
- assert form.is_valid()
- assert AdvancedFilter.objects.count() == 1
-
- created_filter = AdvancedFilter.objects.order_by('-pk')[0]
-
- assert created_filter.title == self.good_data['title']
- assert list(created_filter.query.children[0]) == self.query
-
- # save with redirect to filter
- form_data['_save_goto'] = 1
- res = self.client.post(url, data=form_data)
- assert res.status_code == 302
- assert AdvancedFilter.objects.count() == 2
-
- created_filter = AdvancedFilter.objects.order_by('-pk')[0]
- url = res['location']
- assert url.endswith('admin/customers/client/?_afilter=%s' %
- created_filter.pk)
-
- assert list(created_filter.query.children[0]) == self.query
-
-
-class AdvancedFilterUsageTest(TestCase):
- """ Test filter visibility and actual filtering of a changelist """
- def setUp(self):
- self.user = factories.SalesRep()
- assert self.client.login(username='user', password='test')
- factories.Client.create_batch(8, assigned_to=self.user, language='en')
- factories.Client.create_batch(2, assigned_to=self.user, language='ru')
- self.user.user_permissions.add(Permission.objects.get(
- codename='change_client'))
- self.a = AdvancedFilter(title='Russian speakers', url='foo',
- created_by=self.user, model='customers.Client')
- self.a.query = Q(language='ru')
- self.a.save()
-
- def test_filters_not_available(self):
- url = reverse('admin:customers_client_changelist')
- res = self.client.get(url, data={'_afilter': self.a.pk})
- assert res.status_code == 200
- cl = res.context_data['cl']
- assert not any(isinstance(f, AdvancedListFilters)
- for f in cl.filter_specs)
- # filter not applied due to user not being in list
- if hasattr(cl, 'queryset'):
- assert cl.queryset.count() == 10
-
- def test_filters_available_to_users(self):
- self.a.users.add(self.user)
- url = reverse('admin:customers_client_changelist')
- res = self.client.get(url, data={'_afilter': self.a.pk})
- assert res.status_code == 200
- cl = res.context_data['cl']
- assert any(isinstance(f, AdvancedListFilters)
- for f in cl.filter_specs)
- if hasattr(cl, 'queryset'):
- assert cl.queryset.count() == 2
-
- def test_filters_available_to_groups(self):
- group = self.user.groups.create()
- self.a.groups.add(group)
- url = reverse('admin:customers_client_changelist')
- res = self.client.get(url, data={'_afilter': self.a.pk})
- assert res.status_code == 200
- cl = res.context_data['cl']
- assert cl.filter_specs
- if hasattr(cl, 'queryset'):
- assert cl.queryset.count() == 2
diff --git a/advanced_filters/tests/test_admin_change_form.py b/advanced_filters/tests/test_admin_change_form.py
new file mode 100644
index 0000000..540376e
--- /dev/null
+++ b/advanced_filters/tests/test_admin_change_form.py
@@ -0,0 +1,57 @@
+import pytest
+from django.contrib.auth.models import Permission
+from django.db.models import Q
+
+from ..models import AdvancedFilter
+from .factories import AdvancedFilterFactory
+
+try:
+ from django.urls import reverse
+except ImportError: # Django < 2.0
+ from django.core.urlresolvers import reverse
+
+URL_NAME_CHANGE = "admin:advanced_filters_advancedfilter_change"
+URL_NAME_ADD = "admin:advanced_filters_advancedfilter_add"
+URL_NAME_CLIENT_CHANGELIST = "admin:customers_client_changelist"
+
+
+@pytest.fixture
+def advanced_filter(user):
+ af = AdvancedFilterFactory.build(created_by=user)
+ af.query = Q(email__iexact="a@a.com")
+ af.save()
+ return af
+
+
+def test_change_page_requires_perms(client, advanced_filter):
+ url = reverse(URL_NAME_CHANGE, args=(advanced_filter.pk,))
+ res = client.get(url)
+ assert res.status_code == 403
+
+
+def test_change_page_renders(client, user, settings, advanced_filter):
+ user.user_permissions.add(Permission.objects.get(codename="change_advancedfilter"))
+ url = reverse(URL_NAME_CHANGE, args=(advanced_filter.pk,))
+
+ settings.ADVANCED_FILTER_EDIT_BY_USER = False
+ res = client.get(url)
+ assert res.status_code == 200
+
+
+def test_change_and_goto(client, user, settings, advanced_filter):
+ user.user_permissions.add(Permission.objects.get(codename="change_advancedfilter"))
+ url = reverse(URL_NAME_CHANGE, args=(advanced_filter.pk,))
+ form_data = {"form-TOTAL_FORMS": 1, "form-INITIAL_FORMS": 0, "_save_goto": 1}
+ settings.ADVANCED_FILTER_EDIT_BY_USER = False
+ res = client.post(url, data=form_data)
+ assert res.status_code == 302
+ url = res["location"]
+ assert url.endswith("%s?_afilter=1" % reverse(URL_NAME_CLIENT_CHANGELIST))
+
+
+def test_create_page_disabled(client, user):
+ user.user_permissions.add(Permission.objects.get(codename="add_advancedfilter"))
+ url = reverse(URL_NAME_ADD)
+ res = client.get(url)
+ assert res.status_code == 403
+ assert AdvancedFilter.objects.count() == 0
diff --git a/advanced_filters/tests/test_creation.py b/advanced_filters/tests/test_creation.py
new file mode 100644
index 0000000..de3cab0
--- /dev/null
+++ b/advanced_filters/tests/test_creation.py
@@ -0,0 +1,88 @@
+import pytest
+from django.contrib.auth.models import Permission
+
+from ..models import AdvancedFilter
+
+try:
+ from django.urls import reverse_lazy
+except ImportError: # Django < 2.0
+ from django.core.urlresolvers import reverse_lazy
+
+URL_CLIENT_CHANGELIST = reverse_lazy("admin:customers_client_changelist")
+
+
+def test_changelist_includes_form(user, settings, client):
+ user.user_permissions.add(Permission.objects.get(codename="change_client"))
+ settings.ADVANCED_FILTER_EDIT_BY_USER = False
+ res = client.get(URL_CLIENT_CHANGELIST)
+ assert res.status_code == 200
+ title = ["Create advanced filter"]
+ fields = ["First name", "Language", "Sales Rep"]
+ response_content = res.content.decode("utf-8")
+ for part in title + fields:
+ assert part in response_content
+
+
+@pytest.fixture
+def form_data():
+ return {
+ "form-TOTAL_FORMS": 1,
+ "form-INITIAL_FORMS": 0,
+ "action": "advanced_filters",
+ }
+
+
+def test_create_form_validation(user, client, form_data):
+ user.user_permissions.add(Permission.objects.get(codename="change_client"))
+ res = client.post(URL_CLIENT_CHANGELIST, data=form_data)
+ assert res.status_code == 200
+ form = res.context_data["advanced_filters"]
+ assert "title" in form.errors
+ assert "__all__" in form.errors
+ assert form.errors["title"] == ["This field is required."]
+ assert form.errors["__all__"] == ["Error validating filter forms"]
+
+
+@pytest.fixture()
+def good_data(form_data):
+ form_data.update(
+ {
+ "title": "Test title",
+ "form-0-field": "language",
+ "form-0-operator": "iexact",
+ "form-0-value": "ru",
+ }
+ )
+ return form_data
+
+
+@pytest.fixture()
+def query():
+ return ["language__iexact", "ru"]
+
+
+def test_create_form_valid(user, client, good_data, query):
+ assert AdvancedFilter.objects.count() == 0
+ user.user_permissions.add(Permission.objects.get(codename="change_client"))
+ res = client.post(URL_CLIENT_CHANGELIST, data=good_data)
+ assert res.status_code == 200
+ form = res.context_data["advanced_filters"]
+ assert form.is_valid()
+ assert AdvancedFilter.objects.count() == 1
+
+ created_filter = AdvancedFilter.objects.order_by("pk").last()
+
+ assert created_filter.title == good_data["title"]
+ assert list(created_filter.query.children[0]) == query
+
+ # save with redirect to filter
+ good_data["_save_goto"] = 1
+ res = client.post(URL_CLIENT_CHANGELIST, data=good_data)
+ assert res.status_code == 302
+ assert AdvancedFilter.objects.count() == 2
+
+ created_filter = AdvancedFilter.objects.order_by("pk").last()
+ url = res["location"]
+ assert url.endswith("%s?_afilter=%s" % (URL_CLIENT_CHANGELIST, created_filter.pk))
+
+ assert list(created_filter.query.children[0]) == query
diff --git a/advanced_filters/tests/test_get_field_choices_view.py b/advanced_filters/tests/test_get_field_choices_view.py
new file mode 100644
index 0000000..28947b3
--- /dev/null
+++ b/advanced_filters/tests/test_get_field_choices_view.py
@@ -0,0 +1,170 @@
+import json
+import sys
+from datetime import timedelta
+from operator import attrgetter
+
+import django
+import factory
+import pytest
+from django.utils import timezone
+from django.utils.encoding import force_str
+from tests.factories import ClientFactory
+
+try:
+ from django.urls import reverse
+except ImportError: # Django < 2.0
+ from django.core.urlresolvers import reverse
+
+
+URL_NAME = "afilters_get_field_choices"
+
+
+def assert_json(content, expect):
+ assert json.loads(force_str(content)) == expect
+
+
+def assert_view_error(client, error, exception=None, **view_kwargs):
+ """ Ensure view either raises exception or returns a 400 json error """
+ view_url = reverse(URL_NAME, kwargs=view_kwargs)
+
+ if exception is not None:
+ with pytest.raises(exception) as excinfo:
+ client.get(view_url)
+ assert error == str(excinfo.value)
+ return
+
+ response = client.get(view_url)
+ assert response.status_code == 400
+ assert_json(response.content, dict(error=error))
+
+
+NO_APP_INSTALLED_ERROR = "No installed app with label 'foo'."
+
+if django.VERSION < (1, 11):
+ NO_MODEL_ERROR = "App 'reps' doesn't have a 'foo' model."
+else:
+ NO_MODEL_ERROR = "App 'reps' doesn't have a 'Foo' model."
+
+
+if sys.version_info >= (3, 5):
+ ARGUMENT_LENGTH_ERROR = "not enough values to unpack (expected 2, got 1)"
+else:
+ ARGUMENT_LENGTH_ERROR = "need more than 1 value to unpack"
+
+if sys.version_info < (3,) and django.VERSION < (1, 11):
+ MISSING_FIELD_ERROR = "SalesRep has no field named u'baz'"
+else:
+ MISSING_FIELD_ERROR = "SalesRep has no field named 'baz'"
+
+
+def test_invalid_view_kwargs(client):
+ assert_view_error(client, "GetFieldChoices view requires 2 arguments")
+ assert_view_error(
+ client, ARGUMENT_LENGTH_ERROR, model="a", field_name="b", exception=ValueError
+ )
+ assert_view_error(
+ client, NO_APP_INSTALLED_ERROR, model="foo.test", field_name="baz"
+ )
+ assert_view_error(client, NO_MODEL_ERROR, model="reps.Foo", field_name="b")
+ assert_view_error(
+ client, MISSING_FIELD_ERROR, model="reps.SalesRep", field_name="baz"
+ )
+
+
+def test_field_with_choices(client):
+ view_url = reverse(
+ URL_NAME, kwargs=dict(model="customers.Client", field_name="language")
+ )
+ response = client.get(view_url)
+ assert_json(
+ response.content,
+ {
+ "results": [
+ {"id": "en", "text": "English"},
+ {"id": "it", "text": "Italian"},
+ {"id": "sp", "text": "Spanish"},
+ ]
+ },
+ )
+
+
+@pytest.fixture
+def three_clients(user):
+ return ClientFactory.create_batch(3, assigned_to=user)
+
+
+def test_disabled_field(three_clients, client, settings):
+ settings.ADVANCED_FILTERS_DISABLE_FOR_FIELDS = ("email",)
+ view_url = reverse(
+ URL_NAME, kwargs=dict(model="customers.Client", field_name="email")
+ )
+ response = client.get(view_url)
+ assert_json(response.content, {"results": []})
+
+
+def test_disabled_field_types(three_clients, client):
+ view_url = reverse(
+ URL_NAME, kwargs=dict(model="customers.Client", field_name="is_active")
+ )
+ response = client.get(view_url)
+ assert_json(response.content, {"results": []})
+
+
+def test_database_choices(three_clients, client):
+ view_url = reverse(
+ URL_NAME, kwargs=dict(model="customers.Client", field_name="email")
+ )
+ response = client.get(view_url)
+ assert_json(
+ response.content,
+ {"results": [dict(id=e.email, text=e.email) for e in three_clients]},
+ )
+
+
+def test_more_than_max_database_choices(user, client, settings):
+ settings.ADVANCED_FILTERS_MAX_CHOICES = 4
+ ClientFactory.create_batch(5, assigned_to=user)
+ view_url = reverse(URL_NAME, kwargs=dict(model="customers.Client", field_name="id"))
+ response = client.get(view_url)
+ assert_json(response.content, {"results": []})
+
+
+def test_distinct_database_choices(user, client, settings):
+ settings.ADVANCED_FILTERS_MAX_CHOICES = 4
+ ClientFactory.create_batch(5, assigned_to=user, email="foo@bar.com")
+ view_url = reverse(
+ URL_NAME, kwargs=dict(model="customers.Client", field_name="email")
+ )
+ response = client.get(view_url)
+ assert_json(
+ response.content, {"results": [{"id": "foo@bar.com", "text": "foo@bar.com"}]}
+ )
+
+
+def test_choices_no_date_fields_support(user, client, settings):
+ settings.ADVANCED_FILTERS_MAX_CHOICES = 4
+ logins = [timezone.now(), timezone.now() - timedelta(days=1), None]
+ ClientFactory.create_batch(
+ 3, assigned_to=user, email="foo@bar.com", last_login=factory.Iterator(logins)
+ )
+ view_url = reverse(
+ URL_NAME, kwargs=dict(model="customers.Client", field_name="last_login")
+ )
+ response = client.get(view_url)
+ assert_json(response.content, {"results": []})
+
+
+def test_choices_has_null(user, client, settings):
+ settings.ADVANCED_FILTERS_MAX_CHOICES = 4
+ named_users = ClientFactory.create_batch(2, assigned_to=user)
+ names = [None] + sorted(set([nu.first_name for nu in named_users]))
+ assert len(named_users) == 2
+ ClientFactory.create_batch(2, assigned_to=user, first_name=None)
+ view_url = reverse(
+ URL_NAME, kwargs=dict(model="customers.Client", field_name="first_name")
+ )
+ response = client.get(view_url)
+ assert_json(
+ response.content,
+ {"results": [{"id": name, "text": str(name)} for name in names]},
+ )
diff --git a/advanced_filters/tests/test_helpers.py b/advanced_filters/tests/test_helpers.py
new file mode 100644
index 0000000..7e75669
--- /dev/null
+++ b/advanced_filters/tests/test_helpers.py
@@ -0,0 +1,17 @@
+from ..form_helpers import CleanWhiteSpacesMixin
+
+import django.forms
+
+
+class FormToTest(CleanWhiteSpacesMixin, django.forms.Form):
+ some_field = django.forms.CharField()
+
+
+def test_spaces_removed():
+ form = FormToTest(data={'some_field': ' a weird value '})
+ assert form.is_valid()
+ assert form.cleaned_data == {'some_field': 'a weird value'}
+
+ form = FormToTest(data={'some_field': ' \n\r \n '})
+ assert not form.is_valid()
+ assert form.cleaned_data == {}
diff --git a/advanced_filters/tests/test_models.py b/advanced_filters/tests/test_models.py
index 955cb16..f6c777d 100644
--- a/advanced_filters/tests/test_models.py
+++ b/advanced_filters/tests/test_models.py
@@ -25,21 +25,21 @@ def setUp(self):
def test_filter_by_user_empty(self):
qs = AdvancedFilter.objects.filter_by_user(user=self.user)
- self.assertEquals(qs.count(), 0)
+ self.assertEqual(qs.count(), 0)
def test_filter_by_user_users(self):
self.advancedfilter.users.add(self.user)
qs = AdvancedFilter.objects.filter_by_user(user=self.user)
- self.assertEquals(qs.count(), 1)
+ self.assertEqual(qs.count(), 1)
def test_filter_by_user_groups(self):
self.advancedfilter.groups.add(self.group)
qs = AdvancedFilter.objects.filter_by_user(user=self.user)
- self.assertEquals(qs.count(), 1)
+ self.assertEqual(qs.count(), 1)
def test_list_fields(self):
self.advancedfilter.query = Q(some_field__iexact='some_value')
diff --git a/advanced_filters/tests/test_q_serializer.py b/advanced_filters/tests/test_q_serializer.py
index a098e8d..41bae32 100644
--- a/advanced_filters/tests/test_q_serializer.py
+++ b/advanced_filters/tests/test_q_serializer.py
@@ -19,7 +19,7 @@ def setUp(self):
def test_serialize_q(self):
res = self.s.serialize(self.query_a)
- self.assertEquals(res, self.correct_query)
+ self.assertEqual(res, self.correct_query)
def test_jsondump_q(self):
jres = self.s.dumps(self.query_a)
diff --git a/advanced_filters/tests/test_usage.py b/advanced_filters/tests/test_usage.py
new file mode 100644
index 0000000..0155093
--- /dev/null
+++ b/advanced_filters/tests/test_usage.py
@@ -0,0 +1,79 @@
+import pytest
+from django.contrib.auth.models import Permission
+from django.db.models import Q
+from tests.factories import ClientFactory, SalesRepFactory
+
+from ..admin import AdvancedListFilters
+from ..models import AdvancedFilter
+from .factories import AdvancedFilterFactory
+
+try:
+ from django.urls import reverse
+except ImportError: # Django < 2.0
+ from django.core.urlresolvers import reverse
+
+
+URL_NAME_CLIENT_CHANGELIST = "admin:customers_client_changelist"
+
+
+@pytest.fixture
+def user(db):
+ user = SalesRepFactory()
+ user.user_permissions.add(Permission.objects.get(codename="change_client"))
+ return user
+
+
+@pytest.fixture()
+def client(client, user):
+ client.force_login(user)
+ return client
+
+
+@pytest.fixture
+def advanced_filter(user):
+ af = AdvancedFilterFactory.build(
+ title="Russian speakers", url="foo", model="customers.Client", created_by=user
+ )
+ af.query = Q(language="ru")
+ af.save()
+ return af
+
+
+@pytest.fixture(autouse=True)
+def clients(user):
+ ClientFactory.create_batch(8, assigned_to=user, language="en")
+ ClientFactory.create_batch(2, assigned_to=user, language="ru")
+
+
+def test_filters_not_available(client, advanced_filter):
+ url = reverse(URL_NAME_CLIENT_CHANGELIST)
+ res = client.get(url, data={"_afilter": advanced_filter.pk})
+ assert res.status_code == 200
+ cl = res.context_data["cl"]
+ assert not any(isinstance(f, AdvancedListFilters) for f in cl.filter_specs)
+ # filter not applied due to user not being in list
+ if hasattr(cl, "queryset"):
+ assert cl.queryset.count() == 10
+
+
+def test_filters_available_to_users(client, user, advanced_filter):
+ advanced_filter.users.add(user)
+ url = reverse(URL_NAME_CLIENT_CHANGELIST)
+ res = client.get(url, data={"_afilter": advanced_filter.pk})
+ assert res.status_code == 200
+ cl = res.context_data["cl"]
+ assert any(isinstance(f, AdvancedListFilters) for f in cl.filter_specs)
+ if hasattr(cl, "queryset"):
+ assert cl.queryset.count() == 2
+
+
+def test_filters_available_to_groups(client, user, advanced_filter):
+ group = user.groups.create()
+ advanced_filter.groups.add(group)
+ url = reverse(URL_NAME_CLIENT_CHANGELIST)
+ res = client.get(url, data={"_afilter": advanced_filter.pk})
+ assert res.status_code == 200
+ cl = res.context_data["cl"]
+ assert cl.filter_specs
+ if hasattr(cl, "queryset"):
+ assert cl.queryset.count() == 2
diff --git a/advanced_filters/tests/test_views.py b/advanced_filters/tests/test_views.py
deleted file mode 100644
index f264a59..0000000
--- a/advanced_filters/tests/test_views.py
+++ /dev/null
@@ -1,125 +0,0 @@
-import sys
-
-from django.test import TestCase
-try:
- from django.test import override_settings
-except ImportError:
- from django.test.utils import override_settings
-from django.utils.encoding import force_text
-try:
- from django.urls import reverse
-except ImportError: # Django < 2.0
- from django.core.urlresolvers import reverse
-import django
-
-from tests import factories
-
-
-class TestGetFieldChoicesView(TestCase):
- url_name = 'afilters_get_field_choices'
-
- def setUp(self):
- self.user = factories.SalesRep()
- assert self.client.login(username='user', password='test')
-
- def assert_json(self, response, expect):
- self.assertJSONEqual(force_text(response.content), expect)
-
- def assert_view_error(self, error, exception=None, **view_kwargs):
- """ Ensure view either raises exception or returns a 400 json error """
- view_url = reverse(self.url_name, kwargs=view_kwargs)
- if exception is not None:
- self.assertRaisesMessage(
- exception, error, self.client.get, view_url)
- return
- res = self.client.get(view_url)
- assert res.status_code == 400
- self.assert_json(res, dict(error=error))
-
- def test_invalid_args(self):
- self.assert_view_error("GetFieldChoices view requires 2 arguments")
- if 'PyPy' in getattr(sys, 'subversion', ()):
- self.assert_view_error(
- 'expected length 2, got 1',
- model='a', field_name='b', exception=ValueError)
- elif sys.version_info >= (3, 5):
- self.assert_view_error(
- 'not enough values to unpack (expected 2, got 1)', model='a',
- field_name='b', exception=ValueError)
- else:
- self.assert_view_error(
- 'need more than 1 value to unpack', model='a',
- field_name='b', exception=ValueError)
- if django.VERSION >= (1, 11):
- self.assert_view_error("No installed app with label 'Foo'.",
- model='Foo.test', field_name='baz')
- self.assert_view_error("App 'reps' doesn't have a 'Foo' model.",
- model='reps.Foo', field_name='b')
- elif django.VERSION >= (1, 7):
- self.assert_view_error("No installed app with label 'foo'.",
- model='foo.test', field_name='baz')
- self.assert_view_error("App 'reps' doesn't have a 'foo' model.",
- model='reps.Foo', field_name='b')
- else:
- self.assert_view_error("No installed app/model: foo.test",
- model='foo.test', field_name='baz')
- self.assert_view_error("No installed app/model: reps.Foo",
- model='reps.Foo', field_name='b')
- if sys.version_info >= (3, 3) or django.VERSION >= (1, 11):
- expected_exception = "SalesRep has no field named 'baz'"
- else:
- expected_exception = "SalesRep has no field named u'baz'"
- self.assert_view_error(expected_exception,
- model='reps.SalesRep', field_name='baz')
-
- def test_field_with_choices(self):
- view_url = reverse(self.url_name, kwargs=dict(
- model='customers.Client', field_name='language'))
- res = self.client.get(view_url)
- self.assert_json(res, {
- 'results': [
- {'id': 'en', 'text': 'English'},
- {'id': 'it', 'text': 'Italian'},
- {'id': 'sp', 'text': 'Spanish'}
- ]
- })
-
- @override_settings(ADVANCED_FILTERS_DISABLE_FOR_FIELDS=('email',))
- def test_disabled_field(self):
- factories.Client.create_batch(3, assigned_to=self.user)
- view_url = reverse(self.url_name, kwargs=dict(
- model='customers.Client', field_name='email'))
- res = self.client.get(view_url)
- self.assert_json(res, {'results': []})
-
- def test_disabled_field_types(self):
- factories.Client.create_batch(3, assigned_to=self.user)
- view_url = reverse(self.url_name, kwargs=dict(
- model='customers.Client', field_name='is_active'))
- res = self.client.get(view_url)
- self.assert_json(res, {'results': []})
-
- def test_database_choices(self):
- clients = factories.Client.create_batch(3, assigned_to=self.user)
- view_url = reverse(self.url_name, kwargs=dict(
- model='customers.Client', field_name='email'))
- res = self.client.get(view_url)
- self.assert_json(res, {
- 'results': [dict(id=e.email, text=e.email) for e in clients]
- })
-
- @override_settings(ADVANCED_FILTERS_MAX_CHOICES=4)
- def test_more_than_max_database_choices(self):
- factories.Client.create_batch(5, assigned_to=self.user)
- view_url = reverse(self.url_name, kwargs=dict(
- model='customers.Client', field_name='id'))
- res = self.client.get(view_url)
- self.assert_json(res, {'results': []})
-
- @override_settings(ADVANCED_FILTERS_MAX_CHOICES=4)
- def test_distinct_database_choices(self):
- factories.Client.create_batch(5, assigned_to=self.user, email="foo@bar.com")
- view_url = reverse(self.url_name, kwargs=dict(
- model='customers.Client', field_name='email'))
- res = self.client.get(view_url)
- self.assert_json(res, {'results': [{'id': 'foo@bar.com', 'text': 'foo@bar.com'}]})
diff --git a/advanced_filters/views.py b/advanced_filters/views.py
index feb265f..72e41f9 100644
--- a/advanced_filters/views.py
+++ b/advanced_filters/views.py
@@ -1,4 +1,3 @@
-from operator import itemgetter
import logging
from django.apps import apps
@@ -6,7 +5,7 @@
from django.contrib.admin.utils import get_fields_from_path
from django.db import models
from django.db.models.fields import FieldDoesNotExist
-from django.utils.encoding import force_text
+from django.utils.encoding import force_str
from django.views.generic import View
from braces.views import (CsrfExemptMixin, StaffuserRequiredMixin,
@@ -44,7 +43,7 @@ def get(self, request, model=None, field_name=None):
except (LookupError, FieldDoesNotExist) as e:
logger.debug("Invalid kwargs passed to view: %s", e)
return self.render_json_response(
- {'error': force_text(e)}, status=400)
+ {'error': force_str(e)}, status=400)
choices = field.choices
# if no choices, populate with distinct values from instances
@@ -71,7 +70,7 @@ def get(self, request, model=None, field_name=None):
else:
choices = []
- results = [{'id': c[0], 'text': force_text(c[1])} for c in sorted(
- choices, key=itemgetter(0))]
+ results = [{'id': c[0], 'text': force_str(c[1])} for c in sorted(
+ choices, key=lambda x: (x[0] is not None, x[0]))]
return self.render_json_response({'results': results})
diff --git a/setup.py b/setup.py
index 1eaff0c..6fa3bf3 100644
--- a/setup.py
+++ b/setup.py
@@ -64,7 +64,7 @@ def get_full_description():
packages=find_packages(exclude=['tests*', 'tests.*', '*.tests']),
include_package_data=True,
install_requires=[
- 'django-braces>=1.4.0,<2',
+ 'django-braces>=1.4.0,<1.14.0',
'simplejson>=3.6.5,<4',
],
extras_require=dict(test=TEST_REQS),
@@ -81,8 +81,18 @@ def get_full_description():
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
- 'Programming Language :: Python :: 3.2',
- 'Programming Language :: Python :: 3.3',
+ 'Programming Language :: Python :: 3.5',
+ 'Programming Language :: Python :: 3.6',
+ 'Programming Language :: Python :: 3.7',
+ 'Programming Language :: Python :: 3.8',
+ 'Framework :: Django',
+ 'Framework :: Django :: 1.9',
+ 'Framework :: Django :: 1.10',
+ 'Framework :: Django :: 1.11',
+ 'Framework :: Django :: 2.0',
+ 'Framework :: Django :: 2.1',
+ 'Framework :: Django :: 2.2',
+ 'Framework :: Django :: 3.0',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
],
diff --git a/test-reqs.txt b/test-reqs.txt
index 05f2926..e11e70d 100644
--- a/test-reqs.txt
+++ b/test-reqs.txt
@@ -1,3 +1,4 @@
-coveralls==0.5
-factory-boy==2.5.2
-pep8==1.6.2
+coveralls
+factory-boy==2.12.0
+pycodestyle==2.5.0
+pytest-django==3.9.0
diff --git a/tests/customers/migrations/0001_initial.py b/tests/customers/migrations/0001_initial.py
index 5cbf642..e6f6867 100644
--- a/tests/customers/migrations/0001_initial.py
+++ b/tests/customers/migrations/0001_initial.py
@@ -25,7 +25,7 @@ class Migration(migrations.Migration):
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('language', models.CharField(choices=[(b'en', b'English'), (b'sp', b'Spanish'), (b'it', b'Italian')], default=b'en', max_length=8)),
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
- ('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')),
+ ('first_name', models.CharField(null=True, max_length=30, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=30, verbose_name='last name')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
diff --git a/tests/customers/models.py b/tests/customers/models.py
index 295191b..680242f 100644
--- a/tests/customers/models.py
+++ b/tests/customers/models.py
@@ -15,7 +15,7 @@ class Client(AbstractBaseUser):
language = models.CharField(max_length=8, choices=VALID_LANGUAGES,
default='en')
email = models.EmailField(_('email address'), blank=True)
- first_name = models.CharField(_('first name'), max_length=30, blank=True)
+ first_name = models.CharField(_('first name'), max_length=30, null=True)
last_name = models.CharField(_('last name'), max_length=30, blank=True)
is_active = models.BooleanField(
_('active'), default=True,
diff --git a/tests/factories.py b/tests/factories.py
index b977a14..8fb039d 100644
--- a/tests/factories.py
+++ b/tests/factories.py
@@ -1,7 +1,7 @@
import factory
-class SalesRep(factory.django.DjangoModelFactory):
+class SalesRepFactory(factory.django.DjangoModelFactory):
class Meta:
model = 'reps.SalesRep'
django_get_or_create = ('username',)
@@ -15,7 +15,7 @@ class Meta:
@classmethod
def _prepare(cls, create, **kwargs):
password = kwargs.pop('password', None)
- user = super(SalesRep, cls)._prepare(create, **kwargs)
+ user = super(SalesRepFactory, cls)._prepare(create, **kwargs)
if password:
user.set_password(password)
if create:
@@ -23,8 +23,9 @@ def _prepare(cls, create, **kwargs):
return user
-class Client(factory.django.DjangoModelFactory):
+class ClientFactory(factory.django.DjangoModelFactory):
class Meta:
model = 'customers.Client'
+ first_name = factory.faker.Faker('first_name')
email = factory.Sequence(lambda n: 'c%d@foo.com' % n)
diff --git a/tox.ini b/tox.ini
index 58d2dc5..13c3216 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,29 +1,47 @@
[tox]
envlist =
- py27-d{17,18,19,110}
- py34-d{17,18,19,110,111,20}
- py35-d{18,19,110,111,20,21}
- py36-d{111,20,21}
- py37-d{20,21}
- pypy-d{17,18,19,110,111}
+ py27-django{19,110,111}
+ py35-django{19,110,111,20,21,22}
+ py36-django{111,20,21,22,30}
+ py37-django{111,20,21,22,30}
+ py38-django{22,30}
+ pypy3-django{19,110,111,20,21,22,30}
-[pep8]
+[pycodestyle]
max-line-length = 120
[testenv]
+passenv = TRAVIS TRAVIS_*
deps =
- d17: Django>=1.7,<1.8
- d18: Django>=1.8,<1.9
- d19: Django>=1.9,<1.10
- d110: Django>=1.10,<1.11
- d111: Django>=1.11,<1.12
- d20: Django>=2.0,<2.1
- d21: Django>=2.1,<2.2
- !d21: pytest-django==2.9.1
- d21: pytest-django==3.4.2
-rtest-reqs.txt
+ django19: Django>=1.9,<1.10
+ django110: Django>=1.10,<1.11
+ django111: Django>=1.11,<1.12
+ django20: Django>=2.0,<2.1
+ django21: Django>=2.1,<2.2
+ django22: Django>=2.1,<2.2
+ django30: Django>=2.1,<2.2
commands =
pip install -e .
- coverage run -m py.test advanced_filters
- pep8 --exclude=urls.py,migrations,.ropeproject -v advanced_filters
+ coverage run -m pytest advanced_filters
+ pycodestyle --exclude=urls.py,migrations,.ropeproject -v advanced_filters
+
+[travis]
+python =
+ 2.7: py27
+ 3.5: py35
+ 3.6: py36
+ 3.7: py37
+ 3.8: py38
+ pypy3: pypy3
+
+[travis:env]
+DJANGO =
+ 1.9: django19
+ 1.10: django110
+ 1.11: django111
+ 2.0: django20
+ 2.1: django21
+ 2.2: django22
+ 3.0: django30