From 6418a5206e0c1a0dce5bc11202a7e49da5d1e817 Mon Sep 17 00:00:00 2001 From: Dmitriy Sokolov Date: Fri, 24 Jun 2016 13:17:49 +0300 Subject: [PATCH] Init commit --- .travis.yml | 38 ++++++ CHANGES | 2 + LICENSE | 20 +++ MANIFEST.in | 5 + README.rst | 58 ++++++++ docs/images/screenshot.png | Bin 0 -> 29641 bytes rangefilter/__init__.py | 10 ++ rangefilter/apps.py | 11 ++ rangefilter/filter.py | 127 ++++++++++++++++++ rangefilter/models.py | 3 + .../templates/rangefilter/date_filter.html | 70 ++++++++++ .../rangefilter/date_filter_1_8.html | 62 +++++++++ rangefilter/tests.py | 105 +++++++++++++++ runtests.py | 35 +++++ setup.py | 51 +++++++ 15 files changed, 597 insertions(+) create mode 100644 .travis.yml create mode 100755 CHANGES create mode 100755 LICENSE create mode 100755 MANIFEST.in create mode 100755 README.rst create mode 100644 docs/images/screenshot.png create mode 100644 rangefilter/__init__.py create mode 100644 rangefilter/apps.py create mode 100644 rangefilter/filter.py create mode 100644 rangefilter/models.py create mode 100644 rangefilter/templates/rangefilter/date_filter.html create mode 100644 rangefilter/templates/rangefilter/date_filter_1_8.html create mode 100644 rangefilter/tests.py create mode 100755 runtests.py create mode 100755 setup.py diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..489dd16 --- /dev/null +++ b/.travis.yml @@ -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 diff --git a/CHANGES b/CHANGES new file mode 100755 index 0000000..a035477 --- /dev/null +++ b/CHANGES @@ -0,0 +1,2 @@ +0.1.0 (2016-06-24) + - Initial release diff --git a/LICENSE b/LICENSE new file mode 100755 index 0000000..dbb5726 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100755 index 0000000..1785173 --- /dev/null +++ b/MANIFEST.in @@ -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] diff --git a/README.rst b/README.rst new file mode 100755 index 0000000..b2072a7 --- /dev/null +++ b/README.rst @@ -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), + ) diff --git a/docs/images/screenshot.png b/docs/images/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..b0acd6303c1c6926b524096b9787e85ae93a055d GIT binary patch literal 29641 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 z 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 (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]} + ) diff --git a/rangefilter/models.py b/rangefilter/models.py new file mode 100644 index 0000000..4a574b3 --- /dev/null +++ b/rangefilter/models.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from __future__ import unicode_literals diff --git a/rangefilter/templates/rangefilter/date_filter.html b/rangefilter/templates/rangefilter/date_filter.html new file mode 100644 index 0000000..9652834 --- /dev/null +++ b/rangefilter/templates/rangefilter/date_filter.html @@ -0,0 +1,70 @@ +{% load i18n admin_static %} +

{% blocktrans with filter_title=title %} By {{ filter_title }} {% endblocktrans %}

+ + +
+
+ {{ spec.form.media }} + {{ spec.form.as_p }} + {% for choice in choices %} + + {% endfor %} +
+ + +
+
+
diff --git a/rangefilter/templates/rangefilter/date_filter_1_8.html b/rangefilter/templates/rangefilter/date_filter_1_8.html new file mode 100644 index 0000000..8b9ab2c --- /dev/null +++ b/rangefilter/templates/rangefilter/date_filter_1_8.html @@ -0,0 +1,62 @@ +{% load i18n admin_static %} +

{% blocktrans with filter_title=title %} By {{ filter_title }} {% endblocktrans %}

+ + +
+
+ {{ spec.form.media }} + {{ spec.form }} + {% for choice in choices %} + + {% endfor %} +
+ + +
+
+
diff --git a/rangefilter/tests.py b/rangefilter/tests.py new file mode 100644 index 0000000..a5a0ba6 --- /dev/null +++ b/rangefilter/tests.py @@ -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') diff --git a/runtests.py b/runtests.py new file mode 100755 index 0000000..001df88 --- /dev/null +++ b/runtests.py @@ -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') diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..986be03 --- /dev/null +++ b/setup.py @@ -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', + ], +)