diff --git a/.gitignore b/.gitignore index a5a65fb..aee201b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,12 +3,13 @@ *.pyc /.tox/ /.coverage -local_settings.py +/tests/test_project/host_settings.py build/ dist/ django_advanced_filters.egg-info/ -tests/db.sqlite3 +tests/db.sqlite* /.python-version /.cache/ /.eggs/ /htmlcov/ +/advanced_filters/.coverage diff --git a/.travis.yml b/.travis.yml index f7742d3..5bb7e6c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,22 +8,26 @@ python: - "3.4" - "pypy" matrix: - exclude: + include: + - python: "2.6" + env: DJANGO="Django>=1.5,<1.6" - python: "2.6" - env: DJANGO="Django>=1.7,<1.8" + env: DJANGO="Django>=1.6,<1.7" + exclude: - python: "2.6" - env: DJANGO="Django>=1.8,<1.9" + - python: "3.3" + env: DJANGO="Django>=1.9,<1.10" env: - DJANGO="Django>=1.5,<1.6" - DJANGO="Django>=1.6,<1.7" - DJANGO="Django>=1.7,<1.8" - DJANGO="Django>=1.8,<1.9" - # - DJANGO="Django>=1.9a" + - DJANGO="Django>=1.9,<1.10" install: - pip install $DJANGO - pip install -e .[test] script: - coverage run -m py.test advanced_filters - - pep8 --exclude=*urls.py advanced_filters -v + - pep8 --exclude=*urls.py --exclude=*migrations advanced_filters -v after_success: coveralls \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ad76a9..1d77994 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,28 @@ # Changelog +## 1.0.2 - A Better Future + +This release features better test coverage and support for Django 1.9. + +### Bugs + - stretch formset table to the modal container width + - toggle advanced `vendor/jquery` dir according to Django version + - retain support older Django versions + - clean up legacy tags in templates + +### Tests + - add admin views tests + - add Django 1.9 to test matrix + - other minor improvements + +### Docs + - Improve README with a newer screenshot and pretty tables for badges + +### Contributors: + - Pavel Savchenko + - Leonardo J. Caballero G + - Schuyler Duveen + ## 1.0.1 - A Public Release ### Bugs diff --git a/README.md b/README.md index 06b9754..71fc3cc 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,32 @@ ## Django Advanced Filters -[![PyPI](https://img.shields.io/pypi/pyversions/django-advanced-filters.svg)](https://pypi.python.org/pypi/django-advanced-filters) -[![Build Status](https://travis-ci.org/modlinltd/django-advanced-filters.svg?branch=master)](https://travis-ci.org/modlinltd/django-advanced-filters) -[![Coverage Status](https://coveralls.io/repos/modlinltd/django-advanced-filters/badge.svg)](https://coveralls.io/r/modlinltd/django-advanced-filters) -[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/modlinltd/django-advanced-filters?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +| Branch | Build | Coverage | PyPI | Gitter | +| ------ | ----- | -------- | ---- | ------ | +| Master | [![Build Status][5]][7] | [![Coverage Status][8]][10] | [![PyPI][1]][2] | [![Gitter][3]][4] | +| Develop | [![Build Status][6]][7] | [![Coverage Status][9]][11] | 🔺 | 🔺 | + + +[1]: https://img.shields.io/pypi/pyversions/django-advanced-filters.svg +[2]: https://pypi.python.org/pypi/django-advanced-filters +[3]: https://badges.gitter.im/Join%20Chat.svg +[4]: https://gitter.im/modlinltd/django-advanced-filters?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge +[5]: https://travis-ci.org/modlinltd/django-advanced-filters.svg?branch=master +[6]: https://travis-ci.org/modlinltd/django-advanced-filters.svg?branch=develop +[7]: https://travis-ci.org/modlinltd/django-advanced-filters +[8]: https://coveralls.io/repos/modlinltd/django-advanced-filters/badge.svg?branch=master +[9]: https://coveralls.io/repos/modlinltd/django-advanced-filters/badge.svg?branch=develop +[10]: https://coveralls.io/github/modlinltd/django-advanced-filters?branch=master +[11]: https://coveralls.io/github/modlinltd/django-advanced-filters?branch=develop + A django ModelAdmin mixin which adds advanced filtering abilities to the admin. Mimics the advanced search feature in [VTiger](https://www.vtiger.com/), [see here for more info](https://wiki.vtiger.com/index.php/Create_Custom_Filters) -For release notes, see [Changelog](https://raw.githubusercontent.com/modlinltd/django-advanced-filters/develop/CHANGELOG.md) +![screenshot](https://raw.githubusercontent.com/modlinltd/django-advanced-filters/develop/screenshot.png "Creating via a modal") +For release notes, see [Changelog](https://raw.githubusercontent.com/modlinltd/django-advanced-filters/develop/CHANGELOG.md) ## Requirements @@ -84,10 +99,6 @@ Regardless of the above, you can easily write your own template which uses context variables `{{ advanced_filters }}` and `{{ advanced_filters.formset }}`, to render the advanced filter creation form. -Here's a screenshot - -![alt text](https://raw.githubusercontent.com/modlinltd/django-advanced-filters/master/screenshot.png "Creating via a modal") - ## Structure Each advanced filter has only a couple of required fields when constructed @@ -185,8 +196,9 @@ list of valid field choices when creating/changing an `AdvancedFilter`. ## TODO -* Add more tests (specifically the admin and view parts) +* ~~Add more tests (specifically the admin and view parts)~~ * ~~Add packaging (setup.py, etc...)~~ * ~~Add edit/view functionality to the filters~~ * Add permission user/group selection functionality to the filter form * Allow toggling of predefined templates (grappelli / vanilla django admin), and front-end features. +* Support more (newer) python/django versions diff --git a/advanced_filters/__init__.py b/advanced_filters/__init__.py index cd7ca49..a6221b3 100644 --- a/advanced_filters/__init__.py +++ b/advanced_filters/__init__.py @@ -1 +1 @@ -__version__ = '1.0.1' +__version__ = '1.0.2' diff --git a/advanced_filters/admin.py b/advanced_filters/admin.py index 320fdaf..043d042 100644 --- a/advanced_filters/admin.py +++ b/advanced_filters/admin.py @@ -3,7 +3,12 @@ from django.contrib import admin, messages from django.http import HttpResponseRedirect from django.utils.translation import ugettext_lazy as _ -from django.contrib.admin.util import unquote + +try: + from django.contrib.admin.utils import unquote +except ImportError: + # django < 1.7 support + from django.contrib.admin.util import unquote from django.shortcuts import resolve_url from .forms import AdvancedFilterForm @@ -30,7 +35,15 @@ def lookups(self, request, model_admin): def queryset(self, request, queryset): if self.value(): - advfilter = AdvancedFilter.objects.filter(id=self.value()).first() + filters = AdvancedFilter.objects.filter(id=self.value()) + if hasattr(filters, 'first'): + advfilter = filters.first() + else: + # django == 1.5 support + try: + advfilter = filters.order_by()[0] + except IndexError: + advfilter = None if not advfilter: logger.error("AdvancedListFilters.queryset: Invalid filter id") return queryset @@ -61,7 +74,7 @@ def save_advanced_filter(self, request, form): request, messages.SUCCESS, _('Advanced filter added successfully.') ) - if '_save_goto' in request.REQUEST: + if '_save_goto' in (request.GET or request.POST): url = "{path}{qparams}".format( path=request.path, qparams="?_afilter={id}".format( id=afilter.id)) @@ -70,7 +83,7 @@ def save_advanced_filter(self, request, form): logger.info('Failed saving advanced filter, params: %s', form.data) def adv_filters_handle(self, request, extra_context={}): - data = request.POST if request.REQUEST.get( + data = request.POST if request.POST.get( 'action') == 'advanced_filters' else None adv_filters_form = self.advanced_filter_form( data=data, model_admin=self, extra_form=True) diff --git a/advanced_filters/forms.py b/advanced_filters/forms.py index 66df35d..06e2699 100644 --- a/advanced_filters/forms.py +++ b/advanced_filters/forms.py @@ -4,10 +4,24 @@ import operator from django import forms + +try: + from django.apps import apps + get_model = apps.get_model +except ImportError: + # django < 1.7 support + from django.db.models import get_model + from django.conf import settings from django.contrib import admin -from django.contrib.admin.util import get_fields_from_path -from django.db.models import Q, get_model, FieldDoesNotExist + +try: + from django.contrib.admin.utils import get_fields_from_path +except ImportError: + # django < 1.7 support + from django.contrib.admin.util import get_fields_from_path + +from django.db.models import Q, FieldDoesNotExist from django.db.models.fields import DateField from django.forms.formsets import formset_factory, BaseFormSet from django.templatetags.static import static @@ -16,12 +30,16 @@ from django.utils.six.moves import range, reduce from django.utils.text import capfirst +import django + from easy_select2.widgets import SELECT2_WIDGET_JS, SELECT2_CSS from .models import AdvancedFilter from .form_helpers import CleanWhiteSpacesMixin, VaryingTypeCharField +# django < 1.9 support +USE_VENDOR_DIR = django.VERSION >= (1, 9) logger = logging.getLogger('advanced_filters.forms') @@ -190,6 +208,7 @@ def empty_form(self): def _construct_forms(self): # not strictly required, but Django 1.5 calls this on init + # django == 1.5 support self.forms = [] for i in range(min(self.total_form_count(), self.absolute_max)): self.forms.append(self._construct_form( @@ -219,7 +238,8 @@ class Meta: fields = ('title',) class Media: - required_js = [static('admin/js/jquery.min.js'), + required_js = [static('admin/js/%sjquery.min.js' % + ('vendor/jquery/' if USE_VENDOR_DIR else '')), static('orig_inlines%s.js' % ('' if settings.DEBUG else '.min')), static('magnific-popup/jquery.magnific-popup.js'), @@ -274,7 +294,6 @@ def __init__(self, *args, **kwargs): self._filter_fields = filter_fields or getattr( model_admin, 'advanced_filter_fields', ()) - print(filter_fields, model_admin, self._filter_fields) super(AdvancedFilterForm, self).__init__(*args, **kwargs) diff --git a/advanced_filters/migrations/0001_initial.py b/advanced_filters/migrations/0001_initial.py new file mode 100644 index 0000000..2fd49d9 --- /dev/null +++ b/advanced_filters/migrations/0001_initial.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2016-03-07 23:02 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='AdvancedFilter', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=255)), + ('url', models.CharField(max_length=255)), + ('b64_query', models.CharField(max_length=2048)), + ('model', models.CharField(blank=True, max_length=64, null=True)), + ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='created_advanced_filters', to=settings.AUTH_USER_MODEL)), + ('groups', models.ManyToManyField(blank=True, to='auth.Group')), + ('users', models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name_plural': 'Advanced Filters', + 'verbose_name': 'Advanced Filter', + }, + ), + ] diff --git a/advanced_filters/migrations/__init__.py b/advanced_filters/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/advanced_filters/static/advanced-filters/advanced-filters.css b/advanced_filters/static/advanced-filters/advanced-filters.css index 494e9a0..d9aab07 100644 --- a/advanced_filters/static/advanced-filters/advanced-filters.css +++ b/advanced_filters/static/advanced-filters/advanced-filters.css @@ -1,3 +1,7 @@ +#form-group { + width: 100%; +} + .grp-object-tools li div { display: block; padding: 4px 15px; diff --git a/advanced_filters/templates/admin/advanced_filters.html b/advanced_filters/templates/admin/advanced_filters.html index dbb4e1c..b625cc0 100644 --- a/advanced_filters/templates/admin/advanced_filters.html +++ b/advanced_filters/templates/admin/advanced_filters.html @@ -1,6 +1,7 @@ {% extends "admin/change_list.html" %} {% load i18n static admin_modify %} -{% load cycle from future %} +{# django == 1.5 support #} +{# {% load cycle from future %} #} {% block extrastyle %} {{ advanced_filters.media.css }} diff --git a/advanced_filters/templates/admin/advanced_filters/change_form.html b/advanced_filters/templates/admin/advanced_filters/change_form.html index fe86d60..bcf7edd 100644 --- a/advanced_filters/templates/admin/advanced_filters/change_form.html +++ b/advanced_filters/templates/admin/advanced_filters/change_form.html @@ -1,7 +1,8 @@ -{% extends "admin/change_form_orig.html" %} +{% extends "admin/change_form.html" %} -{% load i18n admin_static admin_modify admin_urls grp_tags history compress %} -{% load cycle from future %} +{% load i18n admin_static admin_modify admin_urls %} +{# django == 1.5 support #} +{# {% load cycle from future %} #} {% block extrastyle %} {{ adminform.media.css }} diff --git a/advanced_filters/tests/test_admin.py b/advanced_filters/tests/test_admin.py new file mode 100644 index 0000000..6da2ced --- /dev/null +++ b/advanced_filters/tests/test_admin.py @@ -0,0 +1,185 @@ +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 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,)) + 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} + res = self.client.post(url, data=form_data) + assert res.status_code == 302 + # django == 1.5 support + if hasattr(res, 'url'): + assert res.url.endswith('admin/customers/client/?_afilter=1') + else: + 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') + 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 + + # django == 1.5 support + 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 + + # django == 1.5 support + created_filter = AdvancedFilter.objects.order_by('-pk')[0] + if hasattr(res, 'url'): + assert res.url.endswith('admin/customers/client/?_afilter=%s' % + created_filter.pk) + else: + 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 cl.filter_specs + # filter not applied due to user not being in list + if hasattr(cl, 'queryset'): + assert cl.queryset.count() == 10 + else: + # django == 1.5 support + assert cl.query_set.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 cl.filter_specs + if hasattr(cl, 'queryset'): + assert cl.queryset.count() == 2 + else: + # django == 1.5 support + assert cl.query_set.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 + else: + # django == 1.5 support + assert cl.query_set.count() == 2 diff --git a/advanced_filters/tests/test_forms.py b/advanced_filters/tests/test_forms.py index 85aa330..eab6b21 100644 --- a/advanced_filters/tests/test_forms.py +++ b/advanced_filters/tests/test_forms.py @@ -209,7 +209,6 @@ def test_remove_existing_query(self): updated_data = {'form-1-field': 'last_name', 'form-0-DELETE': True, 'form-INITIAL_FORMS': 1, 'form-TOTAL_FORMS': 2} data = self._create_query_form_data(form_number=1, **updated_data) - print(data) form = AdvancedFilterForm(data, instance=self.af, filter_fields=['first_name', 'last_name']) assert form.is_valid(), (form.errors, form.fields_formset.errors) diff --git a/advanced_filters/tests/test_models.py b/advanced_filters/tests/test_models.py index d1f0668..955cb16 100644 --- a/advanced_filters/tests/test_models.py +++ b/advanced_filters/tests/test_models.py @@ -1,7 +1,7 @@ from django.test import TestCase from django.db.models import Q -from advanced_filters.models import AdvancedFilter +from ..models import AdvancedFilter class AdvancedFilterPermissions(TestCase): diff --git a/advanced_filters/tests/test_q_serializer.py b/advanced_filters/tests/test_q_serializer.py index 314df6d..6f3f4c8 100644 --- a/advanced_filters/tests/test_q_serializer.py +++ b/advanced_filters/tests/test_q_serializer.py @@ -3,7 +3,7 @@ import django import json -from advanced_filters.q_serializer import QSerializer +from ..q_serializer import QSerializer NEWER_DJANGO = django.VERSION >= (1, 6) diff --git a/advanced_filters/tests/test_views.py b/advanced_filters/tests/test_views.py index 7a5f538..2e23c2b 100644 --- a/advanced_filters/tests/test_views.py +++ b/advanced_filters/tests/test_views.py @@ -5,10 +5,9 @@ from django.test import override_settings except ImportError: from django.test.utils import override_settings -import django from django.utils.encoding import force_text - from django.core.urlresolvers import reverse +import django from tests import factories @@ -18,7 +17,7 @@ class TestGetFieldChoicesView(TestCase): def setUp(self): self.user = factories.SalesRep() - self.assertTrue(self.client.login(username='user', password='test')) + assert self.client.login(username='user', password='test') def assert_json(self, response, expect): self.assertJSONEqual(force_text(response.content), expect) @@ -31,7 +30,7 @@ def assert_view_error(self, error, exception=None, **view_kwargs): exception, error, self.client.get, view_url) return res = self.client.get(view_url) - self.assertEqual(res.status_code, 400) + assert res.status_code == 400 self.assert_json(res, dict(error=error)) def test_invalid_args(self): diff --git a/advanced_filters/urls.py b/advanced_filters/urls.py index 4bea710..fbd196a 100644 --- a/advanced_filters/urls.py +++ b/advanced_filters/urls.py @@ -1,6 +1,6 @@ from django.conf.urls import patterns, url -from .views import GetFieldChoices +from advanced_filters.views import GetFieldChoices urlpatterns = patterns( # API diff --git a/advanced_filters/views.py b/advanced_filters/views.py index 76346a1..d3e3d35 100644 --- a/advanced_filters/views.py +++ b/advanced_filters/views.py @@ -1,8 +1,18 @@ from operator import itemgetter import logging +try: + from django.apps import apps + get_model = apps.get_model +except ImportError: + # django < 1.7 support + from django.db.models import get_model from django.conf import settings -from django.contrib.admin.util import get_fields_from_path +try: + from django.contrib.admin.utils import get_fields_from_path +except ImportError: + # django < 1.7 support + from django.contrib.admin.util import get_fields_from_path from django.db import models from django.db.models.fields import FieldDoesNotExist from django.utils.encoding import force_text @@ -33,7 +43,7 @@ def get(self, request, model=None, field_name=None): status=400) app_label, model_name = model.split('.', 1) try: - model_obj = models.get_model(app_label, model_name) + model_obj = get_model(app_label, model_name) field = get_fields_from_path(model_obj, field_name)[-1] model_obj = field.model # use new model if followed a ForeignKey except AttributeError as e: diff --git a/conftest.py b/conftest.py new file mode 100644 index 0000000..f88223a --- /dev/null +++ b/conftest.py @@ -0,0 +1,6 @@ +import django + +IGNORE_MIGRATIONS = django.VERSION < (1, 7) + +if IGNORE_MIGRATIONS: + collect_ignore = ["advanced_filters/migrations"] diff --git a/screenshot.png b/screenshot.png index d636707..d12d575 100644 Binary files a/screenshot.png and b/screenshot.png differ diff --git a/setup.py b/setup.py index 4770c9f..a9d4493 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ def finalize_options(self): self.test_suite = True def run_tests(self): - #import here, cause outside the eggs aren't loaded + # import here, cause outside the eggs aren't loaded import tox import shlex args = self.tox_args diff --git a/tests/customers/admin.py b/tests/customers/admin.py index bf61bf3..3f117c2 100644 --- a/tests/customers/admin.py +++ b/tests/customers/admin.py @@ -6,7 +6,7 @@ class ClientAdmin(AdminAdvancedFiltersMixin, admin.ModelAdmin): - advanced_filter_fields = ('language', 'first_name', 'rep', + advanced_filter_fields = ('language', 'first_name', ('assigned_to__email', 'Sales Rep.')) admin.site.register(Client, ClientAdmin) diff --git a/tests/customers/migrations/0001_initial.py b/tests/customers/migrations/0001_initial.py new file mode 100644 index 0000000..5cbf642 --- /dev/null +++ b/tests/customers/migrations/0001_initial.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2016-03-13 22:23 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Client', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('password', models.CharField(max_length=128, verbose_name='password')), + ('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')), + ('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')), + ('assigned_to', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/tests/customers/migrations/__init__.py b/tests/customers/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/reps/migrations/0001_initial.py b/tests/reps/migrations/0001_initial.py new file mode 100644 index 0000000..f6a2407 --- /dev/null +++ b/tests/reps/migrations/0001_initial.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2016-03-13 22:23 +from __future__ import unicode_literals + +import django.contrib.auth.models +import django.core.validators +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='SalesRep', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=30, unique=True, validators=[django.core.validators.RegexValidator('^[\\w.@+-]+$', 'Enter a valid username. This value may contain only letters, numbers and @/./+/-/_ characters.')], verbose_name='username')), + ('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')), + ('last_name', models.CharField(blank=True, max_length=30, verbose_name='last name')), + ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + ('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')), + ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), + ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), + ], + options={ + 'abstract': False, + 'verbose_name': 'user', + 'verbose_name_plural': 'users', + }, + ), + ] diff --git a/tests/reps/migrations/__init__.py b/tests/reps/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_project/urls.py b/tests/test_project/urls.py index ce13366..d8a0d6f 100644 --- a/tests/test_project/urls.py +++ b/tests/test_project/urls.py @@ -1,6 +1,8 @@ from django.conf.urls import patterns, include, url from django.contrib import admin +admin.autodiscover() # django < 1.7 support + urlpatterns = patterns( '', url(r'^admin/', include(admin.site.urls)), diff --git a/tox.ini b/tox.ini index 2230c90..e5123e0 100644 --- a/tox.ini +++ b/tox.ini @@ -1,9 +1,10 @@ [tox] envlist = py26-{d15,d16} - py27-{d15,d16,d17,d18} + py27-{d15,d16,d17,d18,d19} {py33,py34}-{d15,d16,d17,d18} - pypy-{d15,d16,d17,d18} + {py34}-{d18,d19} + pypy-{d15,d16,d17,d18,d19} [pep8] max-line-length = 120 @@ -14,11 +15,11 @@ deps = d16: Django>=1.6,<1.7 d17: Django>=1.7,<1.8 d18: Django>=1.8,<1.9 - d19: Django>=1.9a ; requires many changes + d19: Django>=1.9,<1.10 py26: unittest2 -rtest-reqs.txt commands = pip install -e .[test] coverage run -m py.test advanced_filters - pep8 --exclude=*urls.py -v advanced_filters + pep8 --exclude=*urls.py --exclude=*migrations -v advanced_filters