Init commit
This commit is contained in:
parent
f05e13ff62
commit
6418a5206e
|
@ -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
|
|
@ -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.
|
|
@ -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]
|
|
@ -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),
|
||||
)
|
Binary file not shown.
After Width: | Height: | Size: 29 KiB |
|
@ -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'
|
|
@ -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')
|
|
@ -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]}
|
||||
)
|
|
@ -0,0 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import unicode_literals
|
|
@ -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>
|
|
@ -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>
|
|
@ -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')
|
|
@ -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')
|
|
@ -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',
|
||||
],
|
||||
)
|
Loading…
Reference in New Issue