Skip to content

Commit

Permalink
Merge branch 'release/1.0.1'
Browse files Browse the repository at this point in the history
  • Loading branch information
Pavel Savchenko committed Dec 10, 2015
2 parents 6e3eba7 + 1745761 commit 4312f6a
Show file tree
Hide file tree
Showing 23 changed files with 718 additions and 135 deletions.
25 changes: 25 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
[run]
branch = True
omit = */test*
source = advanced_filters

[report]
# Regexes for lines to exclude from consideration
exclude_lines =
# Have to re-enable the standard pragma
pragma: no cover

# Don't complain about missing debug-only code:
def __repr__
if self\.debug

# Don't complain if tests don't hit defensive assertion code:
raise AssertionError
raise NotImplementedError

# Don't complain if non-runnable code isn't run:
if 0:
if __name__ == .__main__.:

ignore_errors = True

6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
*.log
*.pot
*.pyc
/.tox/
/.coverage
local_settings.py
build/
dist/
django_advanced_filters.egg-info/
tests/db.sqlite3
/.python-version
/.cache/
/.eggs/
/htmlcov/
32 changes: 14 additions & 18 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,33 +1,29 @@
language: python
sudo: false
cache: pip
python:
- "2.6"
- "2.7"
- "3.3"
- "3.4"
- "pypy"
matrix:
exclude:
- python: "2.6"
env: DJANGO=1.7
- python: "3.3"
- python: "3.4"

include:
- python: "3.3"
env: DJANGO=1.6
- python: "3.3"
env: DJANGO=1.7
- python: "3.4"
env: DJANGO=1.7
env: DJANGO="Django>=1.7,<1.8"
- python: "2.6"
env: DJANGO="Django>=1.8,<1.9"
env:
- DJANGO=1.5
- DJANGO=1.6
- DJANGO=1.7
- 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"
install:
- pip install django==$DJANGO --use-mirrors
- pip install pep8 coveralls --use-mirrors
- pip install -e . --use-mirrors
- pip install $DJANGO
- pip install -e .[test]
script:
- coverage run --source=advanced_filters setup.py test
- coverage run -m py.test advanced_filters
- pep8 --exclude=*urls.py advanced_filters -v
after_success:
coveralls
22 changes: 18 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
# Changelog

## 1.0.1 - A Public Release

### Bugs
- proper support for py26 and py3X and different Django releases
- avoid querying all instances for choices
- resolve settings inside view and refine error handling

### Tests
- add doctests to the `form_helpers`
- add tests for `forms`
- add test case `views.TestGetFieldChoicesView`
- setup.py/travis: add `test-reqs.txt` as extras_require
- refactor testing to use `py.test` and run `tox` from `setup.py`
- travis: use latest version of each Django release

### Docs:
- `README`: explain what we test against

## 1.0 - First contact

#### Major changes
Expand All @@ -19,7 +37,3 @@ dynamically populate [`field` options](README.md#fields).
* Hide `QSerializer` calling logic in the model
* Allow modifying `advanced_filter_form` property (defaults to `AdvancedFilterForm`)
* Correct documentation regarding position of mixin in subclass (issue #1)

## 0.1 - Beta / concept initial version

* Initial commits
32 changes: 16 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
## 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)
Expand All @@ -9,30 +10,23 @@ 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](CHANGELOG.md)

### TODO

* Add more tests (specifically the form 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.
For release notes, see [Changelog](https://raw.githubusercontent.com/modlinltd/django-advanced-filters/develop/CHANGELOG.md)


## Requirements

* Django >= 1.5 (tested in 1.5 and 1.7, should work in 1.6 too)
* Django >= 1.5 (Django 1.5 - 1.8 on Python 2/3/PyPy2)
* django-easy-select2 == 1.2.5
* django-braces == 1.4.0
* simplejson == 3.6.5


## Set up
## Installation & Set up

1. Add both `'advanced_filters'` and `'easy_select2'` to `INSTALLED_APPS`.
2. Add `url(r'^advanced_filters/', include('advanced_filters.urls'))` to your project's urlconf.
3. Run `python manage.py syncdb`
1. Install from pypi: `pip install django-advanced-filters`
2. Add both `'advanced_filters'` and `'easy_select2'` to `INSTALLED_APPS`.
3. Add `url(r'^advanced_filters/', include('advanced_filters.urls'))` to your project's urlconf.
4. Run `python manage.py syncdb`

## Integration Example

Expand Down Expand Up @@ -91,7 +85,8 @@ context variables `{{ advanced_filters }}` and
`{{ advanced_filters.formset }}`, to render the advanced filter creation form.

Here's a screenshot
![alt text](screenshot.png "Creating via a modal")

![alt text](https://raw.githubusercontent.com/modlinltd/django-advanced-filters/master/screenshot.png "Creating via a modal")

## Structure

Expand Down Expand Up @@ -188,5 +183,10 @@ The GetFieldChoices view is required to dynamically (using javascript) fetch a
list of valid field choices when creating/changing an `AdvancedFilter`.


[![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/modlinltd/django-advanced-filters/trend.png)](https://bitdeli.com/free "Bitdeli Badge")
## TODO

* 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.
2 changes: 1 addition & 1 deletion advanced_filters/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '1.0.1b'
__version__ = '1.0.1'
54 changes: 49 additions & 5 deletions advanced_filters/form_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from django import forms

from django.utils import six
from easy_select2.widgets import Select2TextInput

logger = logging.getLogger('advanced_filters.form_helpers')
Expand All @@ -11,22 +12,53 @@


class VaryingTypeCharField(forms.CharField):
"""
This CharField subclass returns a regex OR patterns from a
comma separated list value.
"""
_default_separator = ","

def to_python(self, value):
"""
Split a string value by separator (default to ",") into a
list; then, returns a regex pattern string that ORs the values
in the resulting list.
>>> field = VaryingTypeCharField()
>>> assert field.to_python('') == ''
>>> assert field.to_python('test') == 'test'
>>> assert field.to_python('and,me') == '(and|me)'
>>> assert field.to_python('and,me;too') == '(and|me;too)'
"""
res = super(VaryingTypeCharField, self).to_python(value)
split_res = res.split(",")
split_res = res.split(self._default_separator)
if not res or len(split_res) < 2:
return res.strip()

# create a regex string out of the list of choices passed, i.e: (a|b)
res = r"({})".format("|".join(map(lambda x: x.strip(), split_res)))
res = r"({pattern})".format(pattern="|".join(
map(lambda x: x.strip(), split_res)))
return res


class CleanWhiteSpacesMixin(object):
"""
This mixin, when added to any form subclass, adds a clean method which
strips repeating spaces in and around each string value of "clean_data".
"""
def clean(self):
""" Strip char fields """
"""
>>> import django.forms
>>> class MyForm(CleanWhiteSpacesMixin, django.forms.Form):
... some_field = django.forms.CharField()
>>>
>>> form = MyForm({'some_field': ' a weird value '})
>>> assert form.is_valid()
>>> assert form.cleaned_data == {'some_field': 'a weird value'}
"""
cleaned_data = super(CleanWhiteSpacesMixin, self).clean()
for k in self.cleaned_data:
if isinstance(self.cleaned_data[k], basestring):
if isinstance(self.cleaned_data[k], six.string_types):
cleaned_data[k] = re.sub(extra_spaces_pattern, ' ',
self.cleaned_data[k] or '').strip()
return cleaned_data
Expand All @@ -39,12 +71,24 @@ def get_select2textinput_widget(choices=None):
For more info on this custom widget, see doc here:
http://django-easy-select2.readthedocs.org/en/latest/index.html
>>> from django.utils.translation import ugettext_lazy as _
>>> widget = get_select2textinput_widget()
>>> args, kwargs = ('test_field', None,), dict(attrs=dict(id='id_test_field'))
>>> widget = get_select2textinput_widget([
... (1, 'first option'),
... (2, _('test me')),
... ])
>>> data = {'data': [{'id': 1, 'text': 'first option'},
... {'id': 2, 'text': 'test me'}],
... 'width': '250px'}
>>> assert widget.select2attrs == data
"""
attributes = {
# select2 script takes data in json values such as:
# 'data': [ {'id': 'value', 'text': 'description'}, ... ],
}
if choices:
attributes['data'] = [{'id': c[0], 'text': unicode(c[1])}
attributes['data'] = [{'id': c[0], 'text': six.text_type(c[1])}
for c in choices]
return Select2TextInput(select2attrs=attributes)
42 changes: 26 additions & 16 deletions advanced_filters/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from django.templatetags.static import static
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _
from django.utils.six.moves import range, reduce
from django.utils.text import capfirst

from easy_select2.widgets import SELECT2_WIDGET_JS, SELECT2_CSS
Expand Down Expand Up @@ -62,12 +63,14 @@ def _build_field_choices(self, fields):
key=lambda f: f[1].lower())
) + self.FIELD_CHOICES

def _build_query_dict(self, formdata):
def _build_query_dict(self, formdata=None):
"""
Take submitted data from form and create a query dict to be
used in a Q object (or filter)
"""
key = "{}__{}".format(formdata['field'], formdata['operator'])
if self.is_valid() and formdata is None:
formdata = self.cleaned_data
key = "{field}__{operator}".format(**formdata)
if formdata['operator'] == "isnull":
return {key: None}
elif formdata['operator'] == "istrue":
Expand Down Expand Up @@ -138,7 +141,7 @@ def set_range_value(self, data):

def clean(self):
cleaned_data = super(AdvancedFilterQueryForm, self).clean()
if cleaned_data['operator'] == "range":
if cleaned_data.get('operator') == "range":
if ('value_from' in cleaned_data and
'value_to' in cleaned_data):
self.set_range_value(cleaned_data)
Expand All @@ -154,7 +157,7 @@ def make_query(self, *args, **kwargs):
query = query & Q(**query_dict)
return query

def __init__(self, model_fields, *args, **kwargs):
def __init__(self, model_fields={}, *args, **kwargs):
super(AdvancedFilterQueryForm, self).__init__(*args, **kwargs)
self.FIELD_CHOICES = self._build_field_choices(model_fields)
self.fields['field'].choices = self.FIELD_CHOICES
Expand Down Expand Up @@ -185,16 +188,18 @@ def empty_form(self):
self.add_fields(form, None)
return form

def _construct_forms(self):
# not strictly required, but Django 1.5 calls this on init
self.forms = []
for i in range(min(self.total_form_count(), self.absolute_max)):
self.forms.append(self._construct_form(
i, model_fields=self.model_fields))

@cached_property
def forms(self):
"""
Instantiate forms at first property access.
Change: Allow passing of additional kwargs to form instance upon init
"""
# DoS protection is included in total_form_count()
forms = [self._construct_form(i, **{'model_fields': self.model_fields})
for i in xrange(self.total_form_count())]
# override the original property to include `model_fields` argument
forms = [self._construct_form(i, model_fields=self.model_fields)
for i in range(self.total_form_count())]
forms.append(self.empty_form) # add initial empty form
return forms

Expand Down Expand Up @@ -252,19 +257,24 @@ def __init__(self, *args, **kwargs):
model_admin = kwargs.pop('model_admin', None)
instance = kwargs.get('instance')
extra_form = kwargs.pop('extra_form', False)
# TODO: allow all fields to be determined by model
filter_fields = kwargs.pop('filter_fields', None)
if model_admin:
self._model = model_admin.model
elif instance and instance.model:
# get existing instance model
self._model = get_model(*instance.model.split('.'))
model_admin = admin.site._registry[self._model]
try:
model_admin = admin.site._registry[self._model]
except KeyError:
logger.debug('No ModelAdmin registered for %s', self._model)
else:
raise Exception('Adding new AdvancedFilter from admin is '
'not supported')

self._filter_fields = getattr(
model_admin,
'advanced_filter_fields', ())
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)

Expand Down
3 changes: 2 additions & 1 deletion advanced_filters/q_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import base64
import time

from django.utils import six
from django.db.models import Q
from django.core.serializers.base import SerializationError

Expand Down Expand Up @@ -120,7 +121,7 @@ def dumps(self, obj):
raise SerializationError
string = json.dumps(self.serialize(obj), default=dt2ts)
if self.b64_enabled:
return base64.b64encode(string)
return base64.b64encode(six.b(string))
return string

def loads(self, string, raw=False):
Expand Down
Loading

0 comments on commit 4312f6a

Please sign in to comment.