Init commit

This commit is contained in:
Dmitriy Sokolov 2016-06-24 13:17:49 +03:00
parent f05e13ff62
commit 6418a5206e
15 changed files with 597 additions and 0 deletions

38
.travis.yml Normal file
View File

@ -0,0 +1,38 @@
sudo: false
language: python
python:
- 2.7
- 3.2
- 3.3
- 3.4
- 3.5
install:
- pip install Django${DJANGO_VERSION}
env:
matrix:
- DJANGO_VERSION=">=1.6,<1.7"
- DJANGO_VERSION=">=1.7,<1.8"
- DJANGO_VERSION=">=1.8,<1.9"
- DJANGO_VERSION=">=1.9,<1.10"
matrix:
exclude:
- python: 3.5
env: DJANGO_VERSION=">=1.6,<1.7"
- python: 3.5
env: DJANGO_VERSION=">=1.7,<1.8"
- python: 3.3
env: DJANGO_VERSION=">=1.9,<1.10"
- python: 3.2
env: DJANGO_VERSION=">=1.9,<1.10"
branches:
only:
- master
script:
- python runtests.py

2
CHANGES Executable file
View File

@ -0,0 +1,2 @@
0.1.0 (2016-06-24)
- Initial release

20
LICENSE Executable file
View File

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2014 Dmitriy Sokolov
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

5
MANIFEST.in Executable file
View File

@ -0,0 +1,5 @@
include LICENSE CHANGES README.rst
recursive-include rangefilter/static *.js *.css *.png *.eot *.svg *.ttf *.woff
recursive-include rangefilter/templates *.html
recursive-exclude * __pycache__
recursive-exclude * *.py[co]

58
README.rst Executable file
View File

@ -0,0 +1,58 @@
.. image:: https://travis-ci.org/silentsokolov/django-admin-rangefilter.png?branch=master
:target: https://travis-ci.org/silentsokolov/django-admin-rangefilter
django-admin-rangefilter
========================
django-admin-rangefilter app, add the filter by a custom date range on the admin UI.
.. image:: https://raw.githubusercontent.com/silentsokolov/django-admin-rangefilter/master/docs/images/screenshot.png
Requirements
------------
* Python 2.7+ or Python 3.2+
* Django 1.7+
Installation
------------
Use your favorite Python package manager to install the app from PyPI, e.g.
Example:
``pip install django-admin-rangefilter``
Add ``rangefilter`` to ``INSTALLED_APPS``:
Example:
.. code:: python
INSTALLED_APPS = (
...
'rangefilter',
...
)
Example usage
-------------
In admin
~~~~~~~~
.. code:: python
from django.contrib import admin
from rangefilter.filtres import DateRangeFilter
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
list_filter = (
('created_at', DateRangeFilter), ('updated_at', DateRangeFilter),
)

BIN
docs/images/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

10
rangefilter/__init__.py Normal file
View File

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
__author__ = 'Dmitriy Sokolov'
__version__ = '0.1.0'
default_app_config = 'rangefilter.apps.RangeFilterConfig'

11
rangefilter/apps.py Normal file
View File

@ -0,0 +1,11 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.apps import AppConfig
from django.utils.translation import ugettext_lazy as _
class RangeFilterConfig(AppConfig):
name = 'rangefilter'
verbose_name = _('Range Filter')

127
rangefilter/filter.py Normal file
View File

@ -0,0 +1,127 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import datetime
import django
from collections import OrderedDict
from django import forms
from django.conf import settings
from django.contrib import admin
from django.template.defaultfilters import slugify
from django.utils.translation import ugettext as _
try:
import pytz
except ImportError:
pytz = None
try:
from suit.widgets import SuitDateWidget as AdminDateWidget
except ImportError:
from django.contrib.admin.widgets import AdminDateWidget
def make_dt_aware(dt):
if pytz is not None and settings.USE_TZ:
timezone = pytz.timezone(settings.TIME_ZONE)
if dt.tzinfo is not None:
dt = timezone.normalize(dt)
else:
dt = timezone.localize(dt)
return dt
class DateRangeFilter(admin.filters.FieldListFilter):
def __init__(self, field, request, params, model, model_admin, field_path):
self.lookup_kwarg_gte = '{}__gte'.format(field_path)
self.lookup_kwarg_lte = '{}__lte'.format(field_path)
super(DateRangeFilter, self).__init__(field, request, params, model, model_admin, field_path)
self.form = self.get_form(request)
def choices(self, cl):
yield {
'system_name': slugify(self.title),
'query_string': cl.get_query_string(
{}, remove=[self.lookup_kwarg_gte, self.lookup_kwarg_lte]
)
}
def expected_parameters(self):
return [self.lookup_kwarg_gte, self.lookup_kwarg_lte]
def queryset(self, request, queryset):
if self.form.is_valid():
filter_params = dict(filter(lambda f: f[1] is not None, self.form.cleaned_data.items()))
if filter_params:
_filter = {
'{0}__range'.format(self.field_path): (
make_dt_aware(datetime.datetime.combine(
filter_params[self.lookup_kwarg_gte], datetime.time.min
)),
make_dt_aware(datetime.datetime.combine(
filter_params[self.lookup_kwarg_lte], datetime.time.max
))
)
}
return queryset.filter(**_filter)
return queryset
def get_template(self):
if django.VERSION >= (1, 9):
return 'admin/daterange_filter19.html'
return 'admin/daterange_filter.html'
template = property(get_template)
def get_form(self, request):
form_class = self._get_form_class()
return form_class(self.used_parameters)
def _get_form_class(self):
fields = self._get_form_fields()
form_class = type(
str('DateRangeForm'),
(forms.BaseForm,),
{'base_fields': fields}
)
form_class.media = self._get_media()
return form_class
def _get_form_fields(self):
return OrderedDict((
(self.lookup_kwarg_gte, forms.DateField(
label='',
widget=AdminDateWidget(attrs={'placeholder': _('From date')}),
localize=True,
required=False
)),
(self.lookup_kwarg_lte, forms.DateField(
label='',
widget=AdminDateWidget(attrs={'placeholder': _('To date')}),
localize=True,
required=False
)),
))
@staticmethod
def _get_media():
js = [
'calendar.js',
'admin/DateTimeShortcuts.js',
]
css = [
'widgets.css',
]
return forms.Media(
js=['admin/js/%s' % url for url in js],
css={'all': ['admin/css/%s' % path for path in css]}
)

3
rangefilter/models.py Normal file
View File

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

View File

@ -0,0 +1,70 @@
{% load i18n admin_static %}
<h3>{% blocktrans with filter_title=title %} By {{ filter_title }} {% endblocktrans %}</h3>
<style>
.button, input[type=reset] {
background: #79aec8;
padding: 10px 15px;
border: none;
border-radius: 4px;
color: #fff;
cursor: pointer;
}
.admindatefilter {
padding-left: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #eaeaea;
}
.admindatefilter p {
padding-left: 0px;
line-height: 0;
}
.admindatefilter .timezonewarning {
display: none;
}
.admindatefilter .datetimeshortcuts a:first-child {
margin-right: 4px;
display: none;
}
.calendarbox, .clockbox {
z-index: 1100;
margin-left: -16em !important;
margin-top: 9em !important;
}
.admindatefilter .datetimeshortcuts {
font-size: 0;
float: right;
position: absolute;
padding-top: 4px;
}
.admindatefilter a {
color: #999;
position: absolute;
padding-top: 3px;
padding-left: 4px;
}
</style>
<script>
function datefilter_apply(event, qs_name, form_name){
event.preventDefault();
var query_string = django.jQuery('input#'+qs_name).val();
var form_data = django.jQuery('#'+form_name).serialize();
window.location = window.location.pathname + query_string + '&' + form_data;
}
function datefilter_reset(qs_name){
var query_string = django.jQuery('input#'+qs_name).val();
window.location = window.location.pathname + query_string;
}
</script>
<div class="admindatefilter">
<form method="GET" action="." id="{{ choices.0.system_name }}-form">
{{ spec.form.media }}
{{ spec.form.as_p }}
{% for choice in choices %}
<input type="hidden" id="{{ choice.system_name }}-query-string" value="{{ choice.query_string }}">
{% endfor %}
<div class="controls">
<input type="submit" value="{% trans "Search" %}" onclick="datefilter_apply(event, '{{ choices.0.system_name }}-query-string', '{{ choices.0.system_name }}-form')">
<input type="reset" class="button" value="{% trans "Reset" %}" onclick="datefilter_reset('{{ choices.0.system_name }}-query-string')">
</div>
</form>
</div>

View File

@ -0,0 +1,62 @@
{% load i18n admin_static %}
<h3>{% blocktrans with filter_title=title %} By {{ filter_title }} {% endblocktrans %}</h3>
<style>
.button, input[type=reset] {
padding: 3px 5px;
color: black;
border: 1px solid #bbb;
border-color: #ddd #aaa #aaa #ddd;
}
.admindatefilter {
padding-left: 10px;
}
.admindatefilter .timezonewarning {
display: none;
}
.admindatefilter .datetimeshortcuts a:first-child {
margin-right: 4px;
display: none;
}
.calendarbox, .clockbox {
z-index: 1100;
margin-left: -16em !important;
margin-top: 9em !important;
}
.admindatefilter .datetimeshortcuts {
font-size: 0;
}
.admindatefilter a {
color: #999;
position: absolute;
padding-top: 3px;
padding-left: 4px;
}
.admindatefilter br {
content: ""
}
</style>
<script>
function datefilter_apply(event, qs_name, form_name){
event.preventDefault();
var query_string = django.jQuery('input#'+qs_name).val();
var form_data = django.jQuery('#'+form_name).serialize();
window.location = window.location.pathname + query_string + '&' + form_data;
}
function datefilter_reset(qs_name){
var query_string = django.jQuery('input#'+qs_name).val();
window.location = window.location.pathname + query_string;
}
</script>
<div class="admindatefilter">
<form method="GET" action="." id="{{ choices.0.system_name }}-form">
{{ spec.form.media }}
{{ spec.form }}
{% for choice in choices %}
<input type="hidden" id="{{ choice.system_name }}-query-string" value="{{ choice.query_string }}">
{% endfor %}
<div class="controls">
<input type="submit" value="{% trans "Search" %}" onclick="datefilter_apply(event, '{{ choices.0.system_name }}-query-string', '{{ choices.0.system_name }}-form')">
<input type="reset" class="button" value="{% trans "Reset" %}" onclick="datefilter_reset('{{ choices.0.system_name }}-query-string')">
</div>
</form>
</div>

105
rangefilter/tests.py Normal file
View File

@ -0,0 +1,105 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import datetime
from django.utils import timezone
from django.test import RequestFactory, TestCase
from django.test.utils import override_settings
from django.db import models
from django.contrib.admin import ModelAdmin, site
from django.contrib.admin.views.main import ChangeList
from django.utils.encoding import force_text
from .filter import make_dt_aware, DateRangeFilter
class MyModel(models.Model):
created_at = models.DateTimeField()
class Meta:
ordering = ('created_at',)
class MyModelAdmin(ModelAdmin):
list_filter = (('created_at', DateRangeFilter),)
ordering = ('-id',)
def select_by(dictlist):
return [x for x in dictlist][0]
class DateFuncTestCase(TestCase):
def test_make_dt_aware_without_pytz(self):
with override_settings(USE_TZ=False):
now = datetime.datetime.now()
date = make_dt_aware(now)
self.assertEqual(date.tzinfo, None)
self.assertTrue(timezone.is_naive(date))
def test_make_dt_aware_with_pytz(self):
local_tz = timezone.get_current_timezone()
now = datetime.datetime.now()
date = make_dt_aware(now)
self.assertEqual(date.tzinfo.zone, local_tz.zone)
self.assertTrue(timezone.is_aware(date))
now = timezone.now()
date = make_dt_aware(now)
self.assertEqual(date.tzinfo.zone, local_tz.zone)
self.assertTrue(timezone.is_aware(date))
class DateRangeFilterTestCase(TestCase):
def setUp(self):
self.today = datetime.date.today()
self.tomorrow = self.today + datetime.timedelta(days=1)
self.one_week_ago = self.today - datetime.timedelta(days=7)
self.django_book = MyModel.objects.create(created_at=self.today)
self.djangonaut_book = MyModel.objects.create(created_at=self.one_week_ago)
def get_changelist(self, request, model, modeladmin):
return ChangeList(
request, model, modeladmin.list_display,
modeladmin.list_display_links, modeladmin.list_filter,
modeladmin.date_hierarchy, modeladmin.search_fields,
modeladmin.list_select_related, modeladmin.list_per_page,
modeladmin.list_max_show_all, modeladmin.list_editable, modeladmin,
)
def test_datefilter(self):
self.request_factory = RequestFactory()
modeladmin = MyModelAdmin(MyModel, site)
request = self.request_factory.get('/')
changelist = self.get_changelist(request, MyModel, modeladmin)
queryset = changelist.get_queryset(request)
self.assertEqual(list(queryset), [self.djangonaut_book, self.django_book])
filterspec = changelist.get_filters(request)[0][0]
self.assertEqual(force_text(filterspec.title), 'created at')
def test_datefilter_filtered(self):
self.request_factory = RequestFactory()
modeladmin = MyModelAdmin(MyModel, site)
request = self.request_factory.get('/', {'created_at__gte': self.today,
'created_at__lte': self.tomorrow})
changelist = self.get_changelist(request, MyModel, modeladmin)
queryset = changelist.get_queryset(request)
self.assertEqual(list(queryset), [self.django_book])
filterspec = changelist.get_filters(request)[0][0]
self.assertEqual(force_text(filterspec.title), 'created at')
choice = select_by(filterspec.choices(changelist))
self.assertEqual(choice['query_string'], '?')
self.assertEqual(choice['system_name'], 'created-at')

35
runtests.py Executable file
View File

@ -0,0 +1,35 @@
#!/usr/bin/env python
from __future__ import unicode_literals
import django
from django.conf import settings, global_settings
from django.core.management import call_command
settings.configure(
MIDDLEWARE_CLASSES=global_settings.MIDDLEWARE_CLASSES,
INSTALLED_APPS=(
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sites',
'django.contrib.admin',
'django.contrib.sessions',
'rangefilter',
),
DATABASES={
'default': {'ENGINE': 'django.db.backends.sqlite3'}
},
TEST_RUNNER='django.test.runner.DiscoverRunner',
USE_TZ=True
)
from django.test.utils import setup_test_environment
setup_test_environment()
if django.VERSION > (1, 7):
django.setup()
if __name__ == '__main__':
call_command('test', 'rangefilter')

51
setup.py Executable file
View File

@ -0,0 +1,51 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
from setuptools import setup
def get_packages(package):
return [dirpath for dirpath, dirnames, filenames in os.walk(package)
if os.path.exists(os.path.join(dirpath, '__init__.py'))]
def get_package_data(package):
walk = [(dirpath.replace(package + os.sep, '', 1), filenames)
for dirpath, dirnames, filenames in os.walk(package)
if not os.path.exists(os.path.join(dirpath, '__init__.py'))]
filepaths = []
for base, filenames in walk:
filepaths.extend([os.path.join(base, filename)
for filename in filenames])
return {package: filepaths}
setup(
name='django-admin-rangefilter',
version='0.1.0',
url='https://github.com/silentsokolov/django-admin-rangefilter',
license='MIT',
author='Dmitriy Sokolov',
author_email='silentsokolov@gmail.com',
description='django-admin-rangefilter app, add the filter by a custom date range on the admin UI.',
zip_safe=False,
include_package_data=True,
platforms='any',
packages=get_packages('rangefilter'),
package_data=get_package_data('rangefilter'),
install_requires=[],
classifiers=[
'Development Status :: 4 - Beta',
'Development Status :: 5 - Production/Stable',
'Environment :: Web Environment',
'Framework :: Django',
'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Programming Language :: Python :: 3',
'Topic :: Utilities',
],
)