27 |
28 | {% endblock %}
29 |
30 | {% block javascript_code %}
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
72 |
73 | {% endblock %}
74 |
--------------------------------------------------------------------------------
/django_admin_filters/multi_choice.py:
--------------------------------------------------------------------------------
1 | """Django admin multi choice filter with checkboxes for db fields with choices option."""
2 | from .base import Filter as BaseFilter, FilterSimple as BaseFilterSimple
3 |
4 |
5 | class Choices:
6 | """Multi choice options filter."""
7 |
8 | template = None
9 | selected = []
10 | lookup_choices = []
11 |
12 | FILTER_LABEL = "By choices"
13 | CHOICES_SEPARATOR = ','
14 |
15 | def set_selected(self, val, title):
16 | """Init choices according request parameter string."""
17 | self.template = 'multi_choice.html'
18 | title.update({
19 | 'choices_separator': self.CHOICES_SEPARATOR,
20 | })
21 |
22 | if isinstance(val, str):
23 | val = val.split(self.CHOICES_SEPARATOR)
24 |
25 | self.selected = val or []
26 |
27 | def choices(self, _changelist):
28 | """Define filter checkboxes."""
29 | for lookup, title in self.lookup_choices:
30 | yield {
31 | 'selected': lookup in self.selected,
32 | 'value': lookup,
33 | 'display': title,
34 | }
35 |
36 |
37 | class Filter(BaseFilter, Choices):
38 | """Multi choice options filter.
39 |
40 | For CharField and IntegerField fields with 'choices' option.
41 |
42 | https://stackoverflow.com/questions/39790087/is-multi-choice-django-admin-filters-possible
43 | https://stackoverflow.com/questions/38508672/django-admin-filter-multiple-select
44 | https://github.com/ctxis/django-admin-multiple-choice-list-filter
45 | https://github.com/modlinltd/django-advanced-filters
46 | """
47 |
48 | parameter_name_mask = 'mchoice_'
49 |
50 | def __init__(self, field, request, params, model, model_admin, field_path):
51 | """Extend base functionality."""
52 | super().__init__(field, request, params, model, model_admin, field_path)
53 | self.set_selected(self.value(), self.title)
54 | if self.field.get_internal_type() in ['IntegerField']:
55 | self.selected = [int(i) for i in self.selected]
56 | self.lookup_choices = self.field.flatchoices
57 |
58 | def choices(self, changelist):
59 | """Call shared implementation."""
60 | return Choices.choices(self, changelist)
61 |
62 | def queryset(self, request, queryset):
63 | """Return the filtered by selected options queryset."""
64 | if self.selected:
65 | params = {
66 | "{}__in".format(self.field_path): self.selected,
67 | }
68 | return queryset.filter(**params)
69 |
70 | return queryset
71 |
72 |
73 | class FilterExt(BaseFilterSimple, Choices):
74 | """Allows filtering by custom defined properties."""
75 |
76 | options = []
77 |
78 | def __init__(self, request, params, model, model_admin):
79 | """Combine parents init."""
80 | super().__init__(request, params, model, model_admin)
81 | self.set_selected(self.value(), self.title)
82 |
83 | def choices(self, changelist):
84 | """Call shared implementation."""
85 | return Choices.choices(self, changelist)
86 |
87 | def lookups(self, request, model_admin):
88 | """Return filter choices."""
89 | return [i[:2] for i in self.options]
90 |
91 | def queryset(self, request, queryset):
92 | """Return the filtered by selected options queryset."""
93 | if not self.selected:
94 | return queryset
95 |
96 | filters = {i[0]: i[2] for i in self.options}
97 | qflt = filters[self.selected[0]]
98 | for item in self.selected[1:]:
99 | qflt |= filters[item]
100 |
101 | return queryset.filter(qflt)
102 |
--------------------------------------------------------------------------------
/tests/test/test_daterange.py:
--------------------------------------------------------------------------------
1 | """Daterange filter tests.
2 |
3 | make test T=test_daterange.py
4 | """
5 | from datetime import datetime
6 | from . import TestBase
7 |
8 |
9 | class TestsDaterange(TestBase):
10 | """Daterange filter tests.
11 |
12 | https://github.com/django/django/blob/main/tests/admin_filters/tests.py
13 | """
14 |
15 | def setUp(self):
16 | """Set up Daterange filter tests."""
17 | super().setUp()
18 | from django_admin_filters import DateRange
19 | from example.models import Log
20 |
21 | self.log = Log(text="text1")
22 | self.log.save()
23 | self.field_path = 'timestamp1'
24 | self.pname = DateRange.parameter_name_mask + self.field_path
25 |
26 | @staticmethod
27 | def test_to_dtime():
28 | """Method to_dtime."""
29 | from django_admin_filters import DateRange
30 |
31 | assert DateRange.to_dtime('xxx') is None
32 | assert DateRange.to_dtime('2022-09-01 00:00') == datetime(2022, 9, 1)
33 |
34 | def test_is_null_option(self):
35 | """Filter with is_null_option option."""
36 | request = self.admin_get({})
37 | changelist = self.modeladmin.get_changelist_instance(request)
38 |
39 | flt = changelist.get_filters(request)[0][1]
40 |
41 | flt.is_null_option = True
42 | assert len(list(flt.choices(changelist))) == 5
43 | flt.is_null_option = False
44 | assert len(list(flt.choices(changelist))) == 4
45 |
46 | def test_queryset_null(self):
47 | """Filter queryset null option."""
48 | from django_admin_filters import DateRange
49 |
50 | request = self.admin_get({self.pname: DateRange.option_null})
51 | changelist = self.modeladmin.get_changelist_instance(request)
52 | flt_null = changelist.get_filters(request)[0][0]
53 | flt_null.is_null_option = True
54 | assert flt_null.queryset(request, self.queryset)
55 |
56 | def test_queryset_option(self):
57 | """Filter queryset shortcut option."""
58 | from example import admin
59 |
60 | admin.DateRange.options = (
61 | ('1h', "1 hour", 60 * 60),
62 | )
63 | request = self.admin_get({self.pname: '1h'})
64 |
65 | changelist = self.modeladmin.get_changelist_instance(request)
66 | flt_future = changelist.get_filters(request)[0][1]
67 | assert not flt_future.queryset(request, self.queryset)
68 |
69 | admin.DateRange.options = (
70 | ('1h', "-1 hour", -60 * 60),
71 | )
72 | changelist = self.modeladmin.get_changelist_instance(request)
73 | flt_past = changelist.get_filters(request)[0][1]
74 | assert flt_past.queryset(request, self.queryset) is not None
75 |
76 | def test_queryset_custom(self):
77 | """Filter queryset custom option."""
78 | from example import admin
79 |
80 | request = self.admin_get({
81 | self.pname: admin.Timestamp1Filter.option_custom,
82 | admin.Timestamp1Filter.parameter_start_mask.format(self.field_path): '2022-01-01 00:00',
83 | admin.Timestamp1Filter.parameter_end_mask.format(self.field_path): '2022-01-02 00:00',
84 | })
85 |
86 | changelist = self.modeladmin.get_changelist_instance(request)
87 |
88 | flt_custom = changelist.get_filters(request)[0][1]
89 | assert not flt_custom.queryset(request, self.queryset)
90 |
91 | def test_queryset_custom_wrong(self):
92 | """Filter queryset wrong custom option."""
93 | from example import admin
94 |
95 | request = self.admin_get({
96 | admin.Timestamp1Filter.parameter_start_mask.format(self.field_path): 'xxx',
97 | admin.Timestamp1Filter.parameter_end_mask.format(self.field_path): 'xxx',
98 | self.pname: admin.Timestamp1Filter.option_custom,
99 | })
100 |
101 | changelist = self.modeladmin.get_changelist_instance(request)
102 | flt = changelist.get_filters(request)[0][0]
103 | assert flt.queryset(request, self.queryset)
104 |
105 | def test_queryset_custom_empty(self):
106 | """Filter queryset empty custom option."""
107 | from example import admin
108 |
109 | request = self.admin_get({self.pname: admin.DateRange.option_custom})
110 | changelist = self.modeladmin.get_changelist_instance(request)
111 | flt = changelist.get_filters(request)[0][0]
112 | assert flt.queryset(request, self.queryset)
113 |
--------------------------------------------------------------------------------
/django_admin_filters/daterange.py:
--------------------------------------------------------------------------------
1 | """Django admin daterange filters with shortcuts."""
2 | from datetime import datetime, timedelta
3 | from django.conf import settings
4 | from .base import Filter as BaseFilter
5 |
6 | HOUR_SECONDS = 60 * 60
7 | DAY_SECONDS = HOUR_SECONDS * 24
8 |
9 | KEY_SELECTED = 'selected'
10 | KEY_QUERY = 'query_string'
11 | KEY_DISPLAY = 'display'
12 |
13 |
14 | class Filter(BaseFilter):
15 | """Date range filter with input fields."""
16 |
17 | FILTER_LABEL = "Data range"
18 | BUTTON_LABEL = "Set range"
19 |
20 | FROM_LABEL = "From"
21 | TO_LABEL = "To"
22 | ALL_LABEL = 'All'
23 | CUSTOM_LABEL = "custom range"
24 | NULL_LABEL = "no date"
25 | DATE_FORMAT = "YYYY-MM-DD HH:mm"
26 | INITIAL_START = ''
27 | INITIAL_END = ''
28 |
29 | WRONG_OPTION_VALUE = -DAY_SECONDS
30 | is_null_option = True
31 |
32 | options = (
33 | ('1da', "24 hours ahead", DAY_SECONDS),
34 | ('1dp', "24 hours in the past", -DAY_SECONDS),
35 | )
36 |
37 | template = 'daterange.html'
38 | parameter_name_mask = 'range_'
39 | parameter_start_mask = "start_{}"
40 | parameter_end_mask = "end_{}"
41 | option_custom = 'custom'
42 | option_null = 'empty'
43 |
44 | def __init__(self, field, request, params, model, model_admin, field_path):
45 | """Customize BaseFilter functionality."""
46 | self.parameter_start = self.parameter_start_mask.format(field_path)
47 | self.parameter_end = self.parameter_end_mask.format(field_path)
48 | super().__init__(field, request, params, model, model_admin, field_path)
49 |
50 | self.lookup_choices = list(self.lookups(request, model_admin))
51 | self.interval = {i[0]: i[2] for i in self.options}
52 | self.title.update({
53 | 'parameter_start': self.parameter_start,
54 | 'parameter_end': self.parameter_end,
55 | 'option_custom': self.option_custom,
56 | 'start_label': self.FROM_LABEL,
57 | 'end_label': self.TO_LABEL,
58 | 'date_format': self.DATE_FORMAT,
59 | 'start_val': request.GET.get(self.parameter_start, self.INITIAL_START),
60 | 'end_val': request.GET.get(self.parameter_end, self.INITIAL_END),
61 | })
62 |
63 | @staticmethod
64 | def to_dtime(text):
65 | """Convert string to datetime."""
66 | if isinstance(text, list):
67 | text = text[0]
68 |
69 | try:
70 | return datetime.fromisoformat(text)
71 | except ValueError:
72 | return None
73 |
74 | def expected_parameters(self):
75 | """Parameter list for filter."""
76 | return [self.parameter_name, self.parameter_start, self.parameter_end]
77 |
78 | def lookups(self, request, _model_admin):
79 | """Return a list of tuples.
80 |
81 | The first element in each tuple is the coded value for the option that will appear in the URL query.
82 | The second element is the human-readable name for the option that will appear in the right sidebar.
83 | """
84 | return [i[:2] for i in self.options]
85 |
86 | def queryset(self, request, queryset):
87 | """Return the filtered queryset.
88 |
89 | Based on the value provided in the query string and retrievable via `self.value()`.
90 | """
91 | value = self.value()
92 |
93 | if value is None:
94 | return queryset
95 |
96 | if value == self.option_custom:
97 |
98 | if self.parameter_start in self.used_parameters:
99 | dtime = self.to_dtime(self.used_parameters[self.parameter_start])
100 | if dtime:
101 | queryset = queryset.filter(**{self.field_path + "__gte": dtime})
102 |
103 | if self.parameter_end in self.used_parameters:
104 | dtime = self.to_dtime(self.used_parameters[self.parameter_end])
105 | if dtime:
106 | queryset = queryset.filter(**{self.field_path + "__lt": dtime})
107 |
108 | return queryset
109 |
110 | if value == self.option_null:
111 | return queryset.filter(**{self.field_path + "__isnull": True})
112 |
113 | now = datetime.utcnow()
114 | delta = self.interval.get(value, self.WRONG_OPTION_VALUE)
115 |
116 | if delta < 0: # in past
117 | params = {
118 | self.field_path + "__gte": now + timedelta(seconds=delta),
119 | self.field_path + "__lt": now,
120 | }
121 | else: # in future
122 | params = {
123 | self.field_path + "__lte": now + timedelta(seconds=delta),
124 | self.field_path + "__gt": now,
125 | }
126 |
127 | return queryset.filter(**params)
128 |
129 | def choices(self, changelist):
130 | """Define filter shortcuts."""
131 | yield {
132 | KEY_SELECTED: self.value() is None,
133 | KEY_QUERY: changelist.get_query_string(remove=[self.parameter_name]),
134 | KEY_DISPLAY: self.ALL_LABEL,
135 | }
136 |
137 | for lookup, title in self.lookup_choices:
138 | yield {
139 | KEY_SELECTED: self.value() == str(lookup),
140 | KEY_QUERY: changelist.get_query_string({self.parameter_name: lookup}),
141 | KEY_DISPLAY: title,
142 | }
143 |
144 | if self.is_null_option:
145 | yield {
146 | KEY_SELECTED: self.value() == self.option_null,
147 | KEY_QUERY: changelist.get_query_string({self.parameter_name: self.option_null}),
148 | KEY_DISPLAY: self.NULL_LABEL,
149 | }
150 |
151 | yield {
152 | KEY_SELECTED: self.value() == self.option_custom,
153 | KEY_QUERY: changelist.get_query_string({self.parameter_name: self.option_custom}),
154 | KEY_DISPLAY: self.CUSTOM_LABEL,
155 | }
156 |
157 |
158 | class FilterPicker(Filter):
159 | """Date range filter with js datetime picker widget."""
160 |
161 | template = 'daterange_picker.html'
162 |
163 | INITIAL_START = 'now'
164 | INITIAL_END = 'now'
165 |
166 | WIDGET_LOCALE = settings.LANGUAGE_CODE
167 | WIDGET_BUTTON_LABEL = "Set"
168 | WIDGET_WITH_TIME = True
169 |
170 | WIDGET_START_TITLE = 'Start date'
171 | WIDGET_START_TOP = -350
172 | WIDGET_START_LEFT = -400 if WIDGET_WITH_TIME else -100
173 |
174 | WIDGET_END_TITLE = 'End date'
175 | WIDGET_END_TOP = -350
176 | WIDGET_END_LEFT = -400 if WIDGET_WITH_TIME else -100
177 |
178 | def __init__(self, field, request, params, model, model_admin, field_path):
179 | """Apply js widget settings."""
180 | super().__init__(field, request, params, model, model_admin, field_path)
181 | self.title.update({
182 | 'widget_locale': self.WIDGET_LOCALE,
183 | 'widget_button_label': self.WIDGET_BUTTON_LABEL,
184 | 'widget_with_time': 'true' if self.WIDGET_WITH_TIME else 'false',
185 | 'widget_start_title': self.WIDGET_START_TITLE,
186 | 'widget_start_top': self.WIDGET_START_TOP,
187 | 'widget_start_left': self.WIDGET_START_LEFT,
188 | 'widget_end_title': self.WIDGET_END_TITLE,
189 | 'widget_end_top': self.WIDGET_END_TOP,
190 | 'widget_end_left': self.WIDGET_END_LEFT,
191 | })
192 |
--------------------------------------------------------------------------------
/READMEru.md:
--------------------------------------------------------------------------------
1 | # Библиотека DjangoAdminFilters
2 |
3 | [На английском](README.md)
4 |
5 | Бесплатная, с открытым исходным кодом библиотека DjangoAdminFilters позволяет использовать несколько дополнительных фильтров в таблицах админки Django.
6 |
7 | - `MultiChoice`: множественный выбор с чекбоксами для полей типа CharField и IntegerField, имеющих опцию 'choices'
8 | - `MultiChoiceExt`: другая версия предыдущего фильтра, который позволяет фильтровать по заданным пользователем свойствам
9 | - `DateRange`: позволяет задавать пользовательский интервал дат с использованием полей `input`
10 | - `DateRangePicker`: позволяет задавать пользовательский интервал дат с использованием javascript виджета выбора даты/времени из календаря
11 |
12 | MultiChoice и MultiChoiceExt | DateRange | DateRangePicker
13 | :------------:|:-------------:|:------------:
14 |  |  | 
15 |
16 | Для javascript виджета в фильтре DateRangePicker используется код [проекта date-and-time-picker](https://github.com/polozin/date-and-time-picker) с внедренным [пул-реквестом](https://github.com/polozin/date-and-time-picker/pull/4/files), позволяющем выбирать в этом виджете даты ранее текущей.
17 |
18 | ## Установка
19 |
20 | ```bash
21 | pip install django-admin-list-filters
22 | ```
23 |
24 | Для подключения библиотеки к проекту нужно добавить `django_admin_filters` в список `INSTALLED_APPS` в файле `settings.py`.
25 |
26 | ```python
27 |
28 | INSTALLED_APPS = (
29 |
30 | ...
31 |
32 | 'django_admin_filters',
33 | )
34 | ```
35 |
36 | Затем подключите статические файлы библиотеки.
37 |
38 | ```bash
39 | manage.py collectstatic
40 | ```
41 |
42 | ## Исходные данные
43 |
44 | Допустим, у нас в БД имеется таблица, записи которой содержат следующие поля.
45 |
46 | ```python
47 | # models.py
48 |
49 | from django.db import models
50 |
51 | STATUS_CHOICES = (
52 | ('P', 'Pending'),
53 | ('A', 'Approved'),
54 | ('R', 'Rejected'),
55 | )
56 |
57 | class Log(models.Model):
58 | text = models.CharField(max_length=100)
59 |
60 | timestamp1 = models.DateTimeField(default=None, null=True)
61 | timestamp2 = models.DateTimeField(default=None, null=True)
62 |
63 | status = models.CharField(max_length=1, default='P', choices=STATUS_CHOICES)
64 |
65 | is_online = models.BooleanField(default=False)
66 | is_trouble1 = models.BooleanField(default=False)
67 | is_trouble2 = models.BooleanField(default=False)
68 | ```
69 |
70 | ## Общие настройки для всех фильтров библиотеки
71 |
72 | Вы можете настроить внешний вид и поведение фильтров под свои требования путем наследования классов фильтров из библиотеки и переопределения некоторых атрибутов.
73 | Все фильтры библиотеки поддерживают следующие атрибуты.
74 |
75 | ```python
76 | from django_admin_filters import MultiChoice
77 |
78 | class MyChoicesFilter(MultiChoice):
79 | FILTER_LABEL = "Выберите опции"
80 | BUTTON_LABEL = "Применить"
81 | ```
82 |
83 | - FILTER_LABEL: Заголовок фильтра
84 | - BUTTON_LABEL: Заголовок кнопки применения фильтра
85 |
86 | ## Фильтр MultiChoice
87 |
88 | Для полей модели типа `CharField` или `IntegerField`, определенных с использованием параметра `choices` (например, поле 'status' в модели `Log`), можно использовать фильтр MultiChoice.
89 | Значения из параметра `choices` будут отображаться в виде чекбоксов.
90 |
91 | Для использования фильтра MultiChoice, укажите его в атрибуте `list_filter` соответствующего класса файла `admin.py`.
92 |
93 | ```python
94 | # admin.py
95 |
96 | from django.contrib import admin
97 | from django_admin_filters import MultiChoice
98 | from .models import Log
99 |
100 | class StatusFilter(MultiChoice):
101 | FILTER_LABEL = "По статусу"
102 | BUTTON_LABEL = "Применить"
103 |
104 | class Admin(admin.ModelAdmin):
105 | list_display = ['text', 'status']
106 | list_filter = [('status', StatusFilter)]
107 |
108 | admin.site.register(Log, Admin)
109 | ```
110 |
111 | В админке Django отметьте нужные чекбоксы в фильтре и нажмите кнопку "Применить".
112 | Если пометка снята со всех чекбоксов фильтра и нажата кнопка применения фильтра, то фильтр не будет действовать и отобразятся все записи.
113 |
114 | ## Фильтр MultiChoiceExt
115 |
116 | Иногда нужно фильтровать данные по виртуальному свойству, которому не соответствует единственное поле модели.
117 |
118 | Например, в модели `Log` исходных данных есть три булевых поля.
119 |
120 | ```python
121 | is_online = models.BooleanField(default=False)
122 | is_trouble1 = models.BooleanField(default=False)
123 | is_trouble2 = models.BooleanField(default=False)
124 | ```
125 |
126 | Для этой модели мы определяем свойство `color` следующим образом.
127 |
128 | - Свойство `color` имеет значение 'red', если поле `is_online == False`.
129 | - Если `is_online == True` и оба поля `is_trouble1` и `is_trouble2` имеют значение False, то свойство имеет значение 'green'.
130 | - Если `is_online == True` и хотя бы одно из полей `is_trouble1` и `is_trouble2` имеет значение True, то свойство имеет значение 'yellow'.
131 |
132 | ```python
133 | # models.py
134 |
135 | @property
136 | def color(self):
137 | status = 'red'
138 | if self.is_online:
139 | status = 'green'
140 | if self.is_trouble1 or self.is_trouble2:
141 | status = 'yellow'
142 |
143 | return status
144 | ```
145 |
146 | Для фильтрации данных по такому свойству в админке Django можно использовать фильтр MultiChoiceExt.
147 | В атрибуте `options` нужно указать список чекбоксов, который будет отображаться при использовании фильтра.
148 |
149 | Каждый элемент списка состоит из трех значений.
150 |
151 | - уникальная строка, которая будет использоваться в параметре GET-запроса
152 | - текст у чекбокса
153 | - применяемое к таблице модели в БД выражение фильтрации в виде [Q-объектов Django](https://docs.djangoproject.com/en/dev/topics/db/queries/#complex-lookups-with-q-objects)
154 |
155 | В атрибуте `parameter_name` нужно указать имя параметра GET-запроса, в котором будут передаваться данные фильтра.
156 |
157 | Для нашего примера код будет таким.
158 |
159 | ```python
160 | # admin.py
161 |
162 | from django.db.models import Q
163 | from django_admin_filters import MultiChoiceExt
164 |
165 | class ColorFilter(MultiChoiceExt):
166 | FILTER_LABEL = "По цвету"
167 | parameter_name = "color"
168 | options = [
169 | ('red', 'Red', Q(is_online=False)),
170 | ('yellow', 'Yellow', Q(is_online=True) & (Q(is_trouble1=True) | Q(is_trouble2=True))),
171 | ('green', 'Green', Q(is_online=True) & Q(is_trouble1=False) & Q(is_trouble2=False)),
172 | ]
173 |
174 | class Admin(admin.ModelAdmin):
175 | list_display = ['text', 'color']
176 | list_filter = [ColorFilter]
177 |
178 | admin.site.register(Log, Admin)
179 | ```
180 |
181 | В остальном поведение и настройки фильтра `MultiChoiceExt` аналогичны описанному ранее фильтру `MultiChoice`.
182 |
183 | ## Фильтры DateRange и DateRangePicker
184 |
185 | Для использования фильтров с интервалом дат нужно в файле `admin.py` указать их в атрибуте `list_filter` соответствующего класса.
186 |
187 | ```python
188 | # admin.py
189 |
190 | from django.contrib import admin
191 | from django_admin_filters import DateRange, DateRangePicker
192 | from .models import Log
193 |
194 | class Admin(admin.ModelAdmin):
195 | list_display = ['text', 'timestamp1', 'timestamp2']
196 | list_filter = (('timestamp1', DateRange), ('timestamp2', DateRangePicker))
197 |
198 | admin.site.register(Log, Admin)
199 | ```
200 |
201 | ## Настройка фильтра DateRange
202 |
203 | ```python
204 | # admin.py
205 |
206 | from django_admin_filters import DateRange
207 |
208 | class MyDateRange(DateRange):
209 | FILTER_LABEL = "Интервал данных"
210 | BUTTON_LABEL = "Задать интервал"
211 | FROM_LABEL = "От"
212 | TO_LABEL = "До"
213 | ALL_LABEL = 'Все'
214 | CUSTOM_LABEL = "пользовательский"
215 | NULL_LABEL = "без даты"
216 | DATE_FORMAT = "YYYY-MM-DD HH:mm"
217 |
218 | is_null_option = True
219 |
220 | options = (
221 | ('1da', "24 часа вперед", 60 * 60 * 24),
222 | ('1dp', "последние 24 часа", 60 * 60 * -24),
223 | )
224 | ```
225 |
226 | Можно переопределять следующие атрибуты.
227 |
228 | - `FILTER_LABEL`: Заголовок фильтра.
229 | - `BUTTON_LABEL`: Текст кнопки применения фильтра.
230 | - `FROM_LABEL`: Текст у поля начальной даты.
231 | - `TO_LABEL`: Текст у поля конечной даты.
232 | - `ALL_LABEL`: Текст пункта меню фильтра для отображения всех записей.
233 | - `CUSTOM_LABEL`: Текст пункта меню фильтра при использовании интервала дат.
234 | - `NULL_LABEL`: Текст пункта меню фильтра для отображения записей без даты.
235 | - `is_null_option`: Установите этот атрибут в `False`, чтобы убрать из меню фильтра пункт отображения записей без даты.
236 | - `parameter_start_mask`: Маска имени параметра GET-запроса для начальной даты диапазона дат.
237 | - `parameter_end_mask`: Маска имени параметра GET-запроса для конечной даты диапазона дат.
238 | - `DATE_FORMAT`: Текст подсказки о формате полей даты и времени.
239 |
240 | Вы можете изменить формат ввода даты/времени на собственный.
241 | Но при этом вам возможно будет необходимо также переопределить метод `to_dtime`.
242 | Этот метод используется для преобразования введенной пользователем строки в значение `datetime`.
243 | По умолчанию метод определен следующим образом.
244 |
245 | ```python
246 | @staticmethod
247 | def to_dtime(text):
248 | try:
249 | return datetime.fromisoformat(text)
250 | except ValueError:
251 | return None
252 | ```
253 |
254 | Атрибут `options` задает пункты меню фильтра, позволяющие выбирать данные от текущего момента до смещения на заданное количество секунд в прошлом либо будущем.
255 | Каждый элемент списка `options` содержит три значения.
256 |
257 | - Уникальная строка для использования в параметрах GET запроса. Кроме строк 'custom' и 'empty', которые используются фильтром.
258 | - Заголовок пункта в меню фильтра.
259 | - Смещение в секундах относительно текущего момента. Отрицательное значение задает смещение в прошлое.
260 |
261 | ## Настройка фильтра DateRangePicker
262 |
263 | Фильтр `DateRangePicker` с javascript виджетом выбора даты/времени из календаря является производным от фильтра `DateRange` и позволяет переопределять все описанные выше атрибуты.
264 | Кроме того, в `DateRangePicker` можно переопределить дополнительные атрибуты.
265 |
266 | ```python
267 | # admin.py
268 |
269 | from django_admin_filters import DateRangePicker
270 |
271 | class MyDateRangePicker(DateRangePicker):
272 | WIDGET_LOCALE = 'ru'
273 | WIDGET_BUTTON_LABEL = "Выбрать"
274 | WIDGET_WITH_TIME = True
275 |
276 | WIDGET_START_TITLE = 'Начальная дата'
277 | WIDGET_START_TOP = -350
278 | WIDGET_START_LEFT = -400
279 |
280 | WIDGET_END_TITLE = 'Конечная дата'
281 | WIDGET_END_TOP = -350
282 | WIDGET_END_LEFT = -400
283 | ```
284 |
285 | - WIDGET_LOCALE: Код языка, на котором виджет будет отображать названия месяцев и дней недели. По умолчанию используется значение параметра `LANGUAGE_CODE` файла `settings.py` вашего проекта.
286 | - WIDGET_BUTTON_LABEL: Текст кнопки выбора виджета.
287 | - WIDGET_WITH_TIME: Установите значение этого атрибута в `False`, если вам требуется только выбор даты без времени.
288 | - WIDGET_START_TITLE: Заголовок виджета при выборе начальной даты интервала.
289 | - WIDGET_START_TOP: Смещение по вертикали окна календаря виджета при выборе начальной даты интервала.
290 | - WIDGET_START_LEFT: Смещение по горизонтали окна календаря виджета при выборе начальной даты интервала.
291 | - WIDGET_END_TITLE: Заголовок виджета при выборе конечной даты интервала.
292 | - WIDGET_END_TOP: Смещение по вертикали окна календаря виджета при выборе конечной даты интервала.
293 | - WIDGET_END_LEFT: Смещение по горизонтали окна календаря виджета при выборе конечной даты интервала.
294 |
295 | ## Пример использования
296 |
297 | Вы можете запустить работающий на локальном компьютере пример использования библиотеки.
298 |
299 | На платформе Windows для этого нужно предварительно установить следующие программы.
300 |
301 | - [Python3](https://www.python.org/downloads/release/python-3712/)
302 | - GNU [Unix Utils](http://unxutils.sourceforge.net/) для операций через makefile
303 | - [Git for Windows](https://git-scm.com/download/win) для доступа к репозитарию исходных кодов.
304 |
305 | Затем склонировать репозитарий и запустить установку, указав путь на Python 3.
306 |
307 | ```bash
308 | git clone git@github.com:vb64/django.admin.filters.git
309 | cd django.admin.filters
310 | make setup PYTHON_BIN=/usr/bin/python3
311 | ```
312 |
313 | Подключить статические файлы библиотеки и создать базу данных.
314 |
315 | ```bash
316 | make static
317 | make db
318 | ```
319 |
320 | Создать суперюзера базы данных, указав для него логин и пароль.
321 |
322 | ```bash
323 | make superuser
324 | ```
325 |
326 | Запустить пример.
327 |
328 | ```bash
329 | make example
330 | ```
331 |
332 | Открыть в браузере адрес `http://127.0.0.1:8000/admin/` для просмотра сайта примера.
333 | Для входа в админку нужно использовать логин и пароль, заданные при создании суперюзера.
334 |
335 | ## Похожие проекты
336 |
337 | - [django-admin-list-filter-dropdown](https://github.com/mrts/django-admin-list-filter-dropdown) фильтр как "выпадающий список" для боковой панели в админке Django.
338 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # DjangoAdminFilters library
2 |
3 | [](https://github.com/vb64/django.admin.filters/actions?query=workflow%3Apep257)
4 | [](https://github.com/vb64/django.admin.filters/actions?query=workflow%3Adjango3)
5 | [](https://github.com/vb64/django.admin.filters/actions?query=workflow%3Adjango4)
6 | [](https://github.com/vb64/django.admin.filters/actions?query=workflow%3Adjango5)
7 | [](https://app.codacy.com/gh/vb64/django.admin.filters/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)
8 | [](https://app.codacy.com/gh/vb64/django.admin.filters/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_coverage)
9 | [](https://pypistats.org/packages/django-admin-list-filters)
10 |
11 | [In Russian](READMEru.md)
12 |
13 | The free, open-source DjangoAdminFilters library is designed to filter objects in the Django admin site.
14 | The library provide few filters for this purpose.
15 |
16 | - `MultiChoice`: multi choice selection with checkboxes for CharField and IntegerField fields with 'choices' option
17 | - `MultiChoiceExt`: another version of previous filter, that allows filtering by custom defined properties
18 | - `DateRange`: set a custom date range using `input` fields
19 | - `DateRangePicker`: set a custom date range using javascript widget for select datetime from calendar
20 |
21 | MultiChoice and MultiChoiceExt | DateRange | DateRangePicker
22 | :------:|:-----:|:----:
23 |  |  | 
24 |
25 | For javascript widget for DateRangePicker was used code from [date-and-time-picker project](https://github.com/polozin/date-and-time-picker) with merged [pull request](https://github.com/polozin/date-and-time-picker/pull/4/files), that allow to select dates before current.
26 |
27 | ## Installation
28 |
29 | ```bash
30 | pip install django-admin-list-filters
31 | ```
32 |
33 | To connect library to your project, add `django_admin_filters` to the `INSTALLED_APPS` list in your `settings.py` file.
34 |
35 | ```python
36 |
37 | INSTALLED_APPS = (
38 |
39 | ...
40 |
41 | 'django_admin_filters',
42 | )
43 | ```
44 |
45 | Then connect the static files of the library.
46 |
47 | ```bash
48 | manage.py collectstatic
49 | ```
50 |
51 | ## Initial data
52 |
53 | Let's say we have a table in the database. The records contain follows fields.
54 |
55 | ```python
56 | # models.py
57 |
58 | from django.db import models
59 |
60 | STATUS_CHOICES = (
61 | ('P', 'Pending'),
62 | ('A', 'Approved'),
63 | ('R', 'Rejected'),
64 | )
65 |
66 | class Log(models.Model):
67 | text = models.CharField(max_length=100)
68 |
69 | timestamp1 = models.DateTimeField(default=None, null=True)
70 | timestamp2 = models.DateTimeField(default=None, null=True)
71 |
72 | status = models.CharField(max_length=1, default='P', choices=STATUS_CHOICES)
73 |
74 | is_online = models.BooleanField(default=False)
75 | is_trouble1 = models.BooleanField(default=False)
76 | is_trouble2 = models.BooleanField(default=False)
77 | ```
78 |
79 | ## Shared settings for all filters in the library
80 |
81 | You can customize the appearance and behavior of filters to suit your needs by inheriting the filter classes from the library and overriding some of the attributes.
82 | All library filters support the following attributes.
83 |
84 | ```python
85 | from django_admin_filters import MultiChoice
86 |
87 | class MyChoicesFilter(MultiChoice):
88 | FILTER_LABEL = "Select options"
89 | BUTTON_LABEL = "Apply"
90 | ```
91 |
92 | - FILTER_LABEL: Filter title
93 | - BUTTON_LABEL: Title for filter apply button
94 |
95 | ## MultiChoice filter
96 |
97 | For model fields of type `CharField` or `IntegerField` defined using the `choices` parameter (for example, the 'status' field in the `Log` model), you can use the MultiChoice filter.
98 | Values of the parameter `choices` will be displayed as checkboxes.
99 |
100 | To use MultiChoice filter, you need to specify them in the `admin.py` file in the `list_filter` attribute of the corresponding class.
101 |
102 | ```python
103 | # admin.py
104 |
105 | from django.contrib import admin
106 | from django_admin_filters import MultiChoice
107 | from .models import Log
108 |
109 | class StatusFilter(MultiChoice):
110 | FILTER_LABEL = "By status"
111 |
112 | class Admin(admin.ModelAdmin):
113 | list_display = ['text', 'status']
114 | list_filter = [('status', StatusFilter)]
115 |
116 | admin.site.register(Log, Admin)
117 | ```
118 |
119 | In the Django admin panel, check the required checkboxes in the filter and click the "Apply" button.
120 | If all filter checkboxes are unchecked and the apply filter button is pressed, than the filter will not been aplied and all records will be displayed.
121 |
122 | ## MultiChoiceExt filter
123 |
124 | Sometimes you need to filter data by a custom defined property that does not match a single field in the model.
125 |
126 | For example, in the `Log` model of the source data, there are three boolean fields.
127 |
128 | ```python
129 | is_online = models.BooleanField(default=False)
130 | is_trouble1 = models.BooleanField(default=False)
131 | is_trouble2 = models.BooleanField(default=False)
132 | ```
133 |
134 | For this model, we define the `color` property as follows.
135 |
136 | - The `color` property has the value 'red' if the field `is_online == False`.
137 | - If `is_online == True` and both `is_trouble1` and `is_trouble2` fields are False, then the value of the property is 'green'.
138 | - If `is_online == True` and at least one of the fields `is_trouble1` and `is_trouble2` is True, then the property has the value 'yellow'.
139 |
140 | ```python
141 | # models.py
142 |
143 | @property
144 | def color(self):
145 | status = 'red'
146 | if self.is_online:
147 | status = 'green'
148 | if self.is_trouble1 or self.is_trouble2:
149 | status = 'yellow'
150 |
151 | return status
152 | ```
153 |
154 | To filter data by such a property in the Django admin panel, you can use the MultiChoiceExt filter.
155 | In the `options` attribute, you need to specify a list of checkboxes that will be displayed when using the filter.
156 |
157 | Each element of the list consists of three values.
158 |
159 | - a unique string to be used in the GET request parameter
160 | - checkbox label
161 | - filtering expression applied to the DB model in the form of [Django Q-objects](https://docs.djangoproject.com/en/dev/topics/db/queries/#complex-lookups-with-q-objects)
162 |
163 | In the `parameter_name` attribute, you need to specify the name of the GET request parameter for sending filter data.
164 |
165 | For our example, the code will look like this.
166 |
167 | ```python
168 | # admin.py
169 |
170 | from django.db.models import Q
171 | from django_admin_filters import MultiChoiceExt
172 |
173 | class ColorFilter(MultiChoiceExt):
174 | FILTER_LABEL = "By color"
175 | parameter_name = "color"
176 | options = [
177 | ('red', 'Red', Q(is_online=False)),
178 | ('yellow', 'Yellow', Q(is_online=True) & (Q(is_trouble1=True) | Q(is_trouble2=True))),
179 | ('green', 'Green', Q(is_online=True) & Q(is_trouble1=False) & Q(is_trouble2=False)),
180 | ]
181 |
182 | class Admin(admin.ModelAdmin):
183 | list_display = ['text', 'color']
184 | list_filter = [ColorFilter]
185 |
186 | admin.site.register(Log, Admin)
187 | ```
188 |
189 | Otherwise, the behavior and settings of the `MultiChoiceExt` filter are similar to the `MultiChoice` filter described earlier.
190 |
191 | ## DateRange and DateRangePicker filters
192 |
193 | To use filters with a date interval, you need to specify them in the `admin.py` file in the `list_filter` attribute of the corresponding class.
194 |
195 | ```python
196 | # admin.py
197 |
198 | from django.contrib import admin
199 | from django_admin_filters import DateRange, DateRangePicker
200 | from .models import Log
201 |
202 | class Admin(admin.ModelAdmin):
203 | list_display = ['text', 'timestamp1', 'timestamp2']
204 | list_filter = (('timestamp1', DateRange), ('timestamp2', DateRangePicker))
205 |
206 | admin.site.register(Log, Admin)
207 | ```
208 |
209 | ### Customization for DateRange filter
210 |
211 | ```python
212 | # admin.py
213 |
214 | from django_admin_filters import DateRange
215 |
216 | class MyDateRange(DateRange):
217 | FILTER_LABEL = "Data range"
218 | BUTTON_LABEL = "Set range"
219 | FROM_LABEL = "From"
220 | TO_LABEL = "To"
221 | ALL_LABEL = 'All'
222 | CUSTOM_LABEL = "custom range"
223 | NULL_LABEL = "no date"
224 | DATE_FORMAT = "YYYY-MM-DD HH:mm"
225 |
226 | is_null_option = True
227 |
228 | options = (
229 | ('1da', "24 hours ahead", 60 * 60 * 24),
230 | ('1dp', "24 hours in the past", 60 * 60 * -24),
231 | )
232 | ```
233 |
234 | You can override the following attributes.
235 |
236 | - `FILTER_LABEL`: Title of the filter.
237 | - `BUTTON_LABEL`: Text on the apply filter button.
238 | - `FROM_LABEL`: The label of the start date field.
239 | - `TO_LABEL`: The label of the end date field.
240 | - `ALL_LABEL`: The label of the menu item for displaying all records.
241 | - `CUSTOM_LABEL`: The label of the menu item when date range is set.
242 | - `NULL_LABEL`: The label of the menu item for displaying records without date.
243 | - `is_null_option`: Set this attribute to `False` to remove the option to display record without date from the filter menu.
244 | - `parameter_start_mask`: Mask of the GET request parameter name for the start date of the date range.
245 | - `parameter_end_mask`: Mask of the GET request parameter name for the end date of the date range.
246 | - `DATE_FORMAT`: Hint about the format of the date and time fields.
247 |
248 | You can change the date/time input format to your own.
249 | However, you may need to override the `to_dtime` method as well.
250 | This method is used to convert a user-entered string into a `datetime` value.
251 | By default, the method is defined as follows.
252 |
253 | ```python
254 | @staticmethod
255 | def to_dtime(text):
256 | try:
257 | return datetime.fromisoformat(text)
258 | except ValueError:
259 | return None
260 | ```
261 |
262 | The `options` attribute specifies filter menu items that allow you to select data from the current moment to an offset of a specified number of seconds in the past or future.
263 | Each element of the `options` list contains three values.
264 |
265 | - A unique string to use in the GET request parameters. Except for the strings 'custom' and 'empty' which are used by the filter.
266 | - The title of the item in the filter menu.
267 | - Offset in seconds relative to the current moment. A negative value specifies an offset to the past.
268 |
269 | ### Customization for DateRangePicker filter
270 |
271 | The `DateRangePicker` filter with a javascript calendar date/time picker widget is derived from the `DateRange` filter and allows you to override all the attributes described above.
272 | Also, additional attributes can be overridden in `DateRangePicker`.
273 |
274 | ```python
275 | # admin.py
276 |
277 | from django_admin_filters import DateRangePicker
278 |
279 | class MyDateRangePicker(DateRangePicker):
280 | WIDGET_LOCALE = 'en'
281 | WIDGET_BUTTON_LABEL = "Set"
282 | WIDGET_WITH_TIME = True
283 |
284 | WIDGET_START_TITLE = 'Start date'
285 | WIDGET_START_TOP = -350
286 | WIDGET_START_LEFT = -400
287 |
288 | WIDGET_END_TITLE = 'End date'
289 | WIDGET_END_TOP = -350
290 | WIDGET_END_LEFT = -400
291 | ```
292 |
293 | - WIDGET_LOCALE: The language code for display the names of the months and days of the week. By default is the value of the `LANGUAGE_CODE` item in your project's `settings.py` file.
294 | - WIDGET_BUTTON_LABEL: The label of the select button.
295 | - WIDGET_WITH_TIME: Set this attribute to `False` if you only want to select a date without a time.
296 | - WIDGET_START_TITLE: The title of the widget when selecting the start date of the interval.
297 | - WIDGET_START_TOP: The vertical offset of the widget's calendar window when selecting the start date of the interval.
298 | - WIDGET_START_LEFT: The horizontal offset of the widget's calendar window when selecting the start date of the interval.
299 | - WIDGET_END_TITLE: The title of the widget when selecting the end date of the interval.
300 | - WIDGET_END_TOP: The vertical offset of the widget's calendar window when selecting the end date of the interval.
301 | - WIDGET_END_LEFT: The horizontal offset of the widget's calendar window when selecting the end date of the interval.
302 |
303 | ## Usage example
304 |
305 | You can run an example of using the library on your local host.
306 |
307 | On the Windows platform, you must first install the following programs.
308 |
309 | - [Python3](https://www.python.org/downloads/release/python-3712/)
310 | - GNU [Unix Utils](http://unxutils.sourceforge.net/) for operations via makefile
311 | - [Git for Windows](https://git-scm.com/download/win) to access the source code repository.
312 |
313 | Then clone the repository and run the installation, specifying the path to Python 3.
314 |
315 | ```bash
316 | git clone git@github.com:vb64/django.admin.filters.git
317 | cd django.admin.filters
318 | make setup PYTHON_BIN=/usr/bin/python3
319 | ```
320 |
321 | Collect static files and create a database.
322 |
323 | ```bash
324 | make static
325 | make db
326 | ```
327 |
328 | Create a database superuser by specifying a login and password for it.
329 |
330 | ```bash
331 | make superuser
332 | ```
333 |
334 | Run example.
335 |
336 | ```bash
337 | make example
338 | ```
339 |
340 | Open `http://127.0.0.1:8000/admin/` in a browser to view the example site.
341 | To enter the admin panel you need to use the login and password that were set when creating the superuser.
342 |
343 | ## Related projects
344 |
345 | - [django-admin-list-filter-dropdown](https://github.com/mrts/django-admin-list-filter-dropdown) `DropdownFilter` class that renders as a drop-down in the filtering sidebar for Django admin list views.
346 |
--------------------------------------------------------------------------------
/.pylintrc:
--------------------------------------------------------------------------------
1 | [MASTER]
2 |
3 | # A comma-separated list of package or module names from where C extensions may
4 | # be loaded. Extensions are loading into the active Python interpreter and may
5 | # run arbitrary code
6 | extension-pkg-whitelist=
7 |
8 | # Add files or directories to the blacklist. They should be base names, not
9 | # paths.
10 | ignore=CVS
11 |
12 | # Add files or directories matching the regex patterns to the blacklist. The
13 | # regex matches against base names, not paths.
14 | # ignore-patterns=settings_*
15 |
16 | # Python code to execute, usually for sys.path manipulation such as
17 | # pygtk.require().
18 | init-hook=import sys;sys.path.insert(0, './')
19 |
20 | # Use multiple processes to speed up Pylint.
21 | jobs=1
22 |
23 | # List of plugins (as comma separated values of python modules names) to load,
24 | # usually to register additional checkers.
25 | load-plugins=pylint_django,pylint.extensions.mccabe
26 | django-settings-module=example.settings
27 | max-complexity=10
28 |
29 | # Pickle collected data for later comparisons.
30 | persistent=yes
31 |
32 | # Specify a configuration file.
33 | #rcfile=
34 |
35 | # When enabled, pylint would attempt to guess common misconfiguration and emit
36 | # user-friendly hints instead of false-positive error messages
37 | suggestion-mode=yes
38 |
39 | # Allow loading of arbitrary C extensions. Extensions are imported into the
40 | # active Python interpreter and may run arbitrary code.
41 | unsafe-load-any-extension=no
42 |
43 |
44 | [MESSAGES CONTROL]
45 |
46 | # Only show warnings with the listed confidence levels. Leave empty to show
47 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
48 | confidence=
49 |
50 | # Disable the message, report, category or checker with the given id(s). You
51 | # can either give multiple identifiers separated by comma (,) or put this
52 | # option multiple times (only on the command line, not in the configuration
53 | # file where it should appear only once).You can also use "--disable=all" to
54 | # disable everything first and then reenable specific checks. For example, if
55 | # you want to run only the similarities checker, you can use "--disable=all
56 | # --enable=similarities". If you want to run only the classes checker, but have
57 | # no Warning level messages displayed, use"--disable=all --enable=classes
58 | # --disable=W"
59 | disable=raw-checker-failed,
60 | bad-inline-option,
61 | locally-disabled,
62 | file-ignored,
63 | suppressed-message,
64 | useless-suppression,
65 | deprecated-pragma,
66 | superfluous-parens,
67 | import-outside-toplevel,
68 | consider-using-with,
69 | deprecated-module,
70 | consider-using-f-string,
71 | too-few-public-methods
72 |
73 | # Enable the message, report, category or checker with the given id(s). You can
74 | # either give multiple identifier separated by comma (,) or put this option
75 | # multiple time (only on the command line, not in the configuration file where
76 | # it should appear only once). See also the "--disable" option for examples.
77 | enable=c-extension-no-member
78 |
79 |
80 | [REPORTS]
81 |
82 | # Python expression which should return a note less than 10 (10 is the highest
83 | # note). You have access to the variables errors warning, statement which
84 | # respectively contain the number of errors / warnings messages and the total
85 | # number of statements analyzed. This is used by the global evaluation report
86 | # (RP0004).
87 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
88 |
89 | # Template used to display messages. This is a python new-style format string
90 | # used to format the message information. See doc for all details
91 | #msg-template=
92 |
93 | # Set the output format. Available formats are text, parseable, colorized, json
94 | # and msvs (visual studio).You can also give a reporter class, eg
95 | # mypackage.mymodule.MyReporterClass.
96 | output-format=text
97 |
98 | # Tells whether to display a full report or only the messages
99 | reports=no
100 |
101 | # Activate the evaluation score.
102 | score=yes
103 |
104 |
105 | [REFACTORING]
106 |
107 | # Maximum number of nested blocks for function / method body
108 | max-nested-blocks=5
109 |
110 | # Complete name of functions that never returns. When checking for
111 | # inconsistent-return-statements if a never returning function is called then
112 | # it will be considered as an explicit return statement and no message will be
113 | # printed.
114 | never-returning-functions=optparse.Values,sys.exit
115 |
116 |
117 | [BASIC]
118 |
119 | # Naming style matching correct argument names
120 | argument-naming-style=snake_case
121 |
122 | # Regular expression matching correct argument names. Overrides argument-
123 | # naming-style
124 | #argument-rgx=
125 |
126 | # Naming style matching correct attribute names
127 | attr-naming-style=snake_case
128 |
129 | # Regular expression matching correct attribute names. Overrides attr-naming-
130 | # style
131 | #attr-rgx=
132 |
133 | # Bad variable names which should always be refused, separated by a comma
134 | bad-names=foo,
135 | bar,
136 | baz,
137 | toto,
138 | tutu,
139 | tata
140 |
141 | # Naming style matching correct class attribute names
142 | class-attribute-naming-style=any
143 |
144 | # Regular expression matching correct class attribute names. Overrides class-
145 | # attribute-naming-style
146 | #class-attribute-rgx=
147 |
148 | # Naming style matching correct class names
149 | class-naming-style=PascalCase
150 |
151 | # Regular expression matching correct class names. Overrides class-naming-style
152 | #class-rgx=
153 |
154 | # Naming style matching correct constant names
155 | const-naming-style=UPPER_CASE
156 |
157 | # Regular expression matching correct constant names. Overrides const-naming-
158 | # style
159 | #const-rgx=
160 |
161 | # Minimum line length for functions/classes that require docstrings, shorter
162 | # ones are exempt.
163 | docstring-min-length=-1
164 |
165 | # Naming style matching correct function names
166 | function-naming-style=snake_case
167 |
168 | # Regular expression matching correct function names. Overrides function-
169 | # naming-style
170 | #function-rgx=
171 |
172 | # Good variable names which should always be accepted, separated by a comma
173 | good-names=i,
174 | j,
175 | k,
176 | ex,
177 | _
178 |
179 | # Include a hint for the correct naming format with invalid-name
180 | include-naming-hint=no
181 |
182 | # Naming style matching correct inline iteration names
183 | inlinevar-naming-style=any
184 |
185 | # Regular expression matching correct inline iteration names. Overrides
186 | # inlinevar-naming-style
187 | #inlinevar-rgx=
188 |
189 | # Naming style matching correct method names
190 | method-naming-style=snake_case
191 |
192 | # Regular expression matching correct method names. Overrides method-naming-
193 | # style
194 | #method-rgx=
195 |
196 | # Naming style matching correct module names
197 | module-naming-style=snake_case
198 |
199 | # Regular expression matching correct module names. Overrides module-naming-
200 | # style
201 | #module-rgx=
202 |
203 | # Colon-delimited sets of names that determine each other's naming style when
204 | # the name regexes allow several styles.
205 | name-group=
206 |
207 | # Regular expression which should only match function or class names that do
208 | # not require a docstring.
209 | no-docstring-rgx=^_
210 |
211 | # List of decorators that produce properties, such as abc.abstractproperty. Add
212 | # to this list to register other decorators that produce valid properties.
213 | property-classes=abc.abstractproperty
214 |
215 | # Naming style matching correct variable names
216 | variable-naming-style=snake_case
217 |
218 | # Regular expression matching correct variable names. Overrides variable-
219 | # naming-style
220 | #variable-rgx=
221 |
222 |
223 | [FORMAT]
224 |
225 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
226 | expected-line-ending-format=
227 |
228 | # Regexp for a line that is allowed to be longer than the limit.
229 | ignore-long-lines=^\s*(# )??$
230 |
231 | # Number of spaces of indent required inside a hanging or continued line.
232 | indent-after-paren=2
233 |
234 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
235 | # tab).
236 | indent-string=' '
237 |
238 | # Maximum number of characters on a single line.
239 | max-line-length=120
240 |
241 | # Maximum number of lines in a module
242 | max-module-lines=1000
243 |
244 | # List of optional constructs for which whitespace checking is disabled. `dict-
245 | # separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}.
246 | # `trailing-comma` allows a space between comma and closing bracket: (a, ).
247 | # `empty-line` allows space-only lines.
248 | #no-space-check=trailing-comma
249 |
250 | # Allow the body of a class to be on the same line as the declaration if body
251 | # contains single statement.
252 | single-line-class-stmt=no
253 |
254 | # Allow the body of an if to be on the same line as the test if there is no
255 | # else.
256 | single-line-if-stmt=no
257 |
258 |
259 | [LOGGING]
260 |
261 | # Logging modules to check that the string format arguments are in logging
262 | # function parameter format
263 | logging-modules=logging
264 |
265 |
266 | [MISCELLANEOUS]
267 |
268 | # List of note tags to take in consideration, separated by a comma.
269 | notes=FIXME,
270 | XXX,
271 | TODO
272 |
273 |
274 | [SIMILARITIES]
275 |
276 | # Ignore comments when computing similarities.
277 | ignore-comments=yes
278 |
279 | # Ignore docstrings when computing similarities.
280 | ignore-docstrings=no
281 |
282 | # Ignore imports when computing similarities.
283 | ignore-imports=no
284 |
285 | # Minimum lines number of a similarity.
286 | min-similarity-lines=4
287 |
288 |
289 | [SPELLING]
290 |
291 | # Limits count of emitted suggestions for spelling mistakes
292 | max-spelling-suggestions=4
293 |
294 | # Spelling dictionary name. Available dictionaries: none. To make it working
295 | # install python-enchant package.
296 | spelling-dict=
297 |
298 | # List of comma separated words that should not be checked.
299 | spelling-ignore-words=
300 |
301 | # A path to a file that contains private dictionary; one word per line.
302 | spelling-private-dict-file=
303 |
304 | # Tells whether to store unknown words to indicated private dictionary in
305 | # --spelling-private-dict-file option instead of raising a message.
306 | spelling-store-unknown-words=no
307 |
308 |
309 | [TYPECHECK]
310 |
311 | # List of decorators that produce context managers, such as
312 | # contextlib.contextmanager. Add to this list to register other decorators that
313 | # produce valid context managers.
314 | contextmanager-decorators=contextlib.contextmanager
315 |
316 | # List of members which are set dynamically and missed by pylint inference
317 | # system, and so shouldn't trigger E1101 when accessed. Python regular
318 | # expressions are accepted.
319 | generated-members=objects
320 |
321 | # Tells whether missing members accessed in mixin class should be ignored. A
322 | # mixin class is detected if its name ends with "mixin" (case insensitive).
323 | ignore-mixin-members=yes
324 |
325 | # This flag controls whether pylint should warn about no-member and similar
326 | # checks whenever an opaque object is returned when inferring. The inference
327 | # can return multiple potential results while evaluating a Python object, but
328 | # some branches might not be evaluated, which results in partial inference. In
329 | # that case, it might be useful to still emit no-member and other checks for
330 | # the rest of the inferred objects.
331 | ignore-on-opaque-inference=yes
332 |
333 | # List of class names for which member attributes should not be checked (useful
334 | # for classes with dynamically set attributes). This supports the use of
335 | # qualified names.
336 | ignored-classes=optparse.Values,thread._local,_thread._local
337 |
338 | # List of module names for which member attributes should not be checked
339 | # (useful for modules/projects where namespaces are manipulated during runtime
340 | # and thus existing member attributes cannot be deduced by static analysis. It
341 | # supports qualified module names, as well as Unix pattern matching.
342 | ignored-modules=pyodbc
343 |
344 | # Show a hint with possible names when a member name was not found. The aspect
345 | # of finding the hint is based on edit distance.
346 | missing-member-hint=yes
347 |
348 | # The minimum edit distance a name should have in order to be considered a
349 | # similar match for a missing member name.
350 | missing-member-hint-distance=1
351 |
352 | # The total number of similar names that should be taken in consideration when
353 | # showing a hint for a missing member.
354 | missing-member-max-choices=1
355 |
356 |
357 | [VARIABLES]
358 |
359 | # List of additional names supposed to be defined in builtins. Remember that
360 | # you should avoid to define new builtins when possible.
361 | additional-builtins=_
362 |
363 | # Tells whether unused global variables should be treated as a violation.
364 | allow-global-unused-variables=yes
365 |
366 | # List of strings which can identify a callback function by name. A callback
367 | # name must start or end with one of those strings.
368 | callbacks=cb_,
369 | _cb
370 |
371 | # A regular expression matching the name of dummy variables (i.e. expectedly
372 | # not used).
373 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
374 |
375 | # Argument names that match this expression will be ignored. Default to name
376 | # with leading underscore
377 | ignored-argument-names=_.*|^ignored_|^unused_
378 |
379 | # Tells whether we should check for unused import in __init__ files.
380 | init-import=no
381 |
382 | # List of qualified module names which can have objects that can redefine
383 | # builtins.
384 | redefining-builtins-modules=six.moves,past.builtins,future.builtins
385 |
386 |
387 | [CLASSES]
388 |
389 | # List of method names used to declare (i.e. assign) instance attributes.
390 | defining-attr-methods=__init__,
391 | __new__,
392 | setUp
393 |
394 | # List of member names, which should be excluded from the protected access
395 | # warning.
396 | exclude-protected=_asdict,
397 | _fields,
398 | _replace,
399 | _source,
400 | _make
401 |
402 | # List of valid names for the first argument in a class method.
403 | valid-classmethod-first-arg=cls
404 |
405 | # List of valid names for the first argument in a metaclass class method.
406 | valid-metaclass-classmethod-first-arg=mcs
407 |
408 |
409 | [DESIGN]
410 |
411 | # Maximum number of arguments for function / method
412 | max-args=15
413 | max-positional-arguments=10
414 |
415 | # Maximum number of attributes for a class (see R0902).
416 | max-attributes=15
417 |
418 | # Maximum number of boolean expressions in a if statement
419 | max-bool-expr=5
420 |
421 | # Maximum number of branch for function / method body
422 | max-branches=12
423 |
424 | # Maximum number of locals for function / method body
425 | max-locals=15
426 |
427 | # Maximum number of parents for a class (see R0901).
428 | max-parents=15
429 |
430 | # Maximum number of public methods for a class (see R0904).
431 | max-public-methods=20
432 |
433 | # Maximum number of return / yield for function / method body
434 | max-returns=6
435 |
436 | # Maximum number of statements in function / method body
437 | max-statements=50
438 |
439 | # Minimum number of public methods for a class (see R0903).
440 | min-public-methods=2
441 |
442 |
443 | [IMPORTS]
444 |
445 | # Allow wildcard imports from modules that define __all__.
446 | allow-wildcard-with-all=no
447 |
448 | # Analyse import fallback blocks. This can be used to support both Python 2 and
449 | # 3 compatible code, which means that the block might have code that exists
450 | # only in one or another interpreter, leading to false positives when analysed.
451 | analyse-fallback-blocks=no
452 |
453 | # Deprecated modules which should not be used, separated by a comma
454 | deprecated-modules=regsub,
455 | TERMIOS,
456 | Bastion,
457 | rexec
458 |
459 | # Create a graph of external dependencies in the given file (report RP0402 must
460 | # not be disabled)
461 | ext-import-graph=
462 |
463 | # Create a graph of every (i.e. internal and external) dependencies in the
464 | # given file (report RP0402 must not be disabled)
465 | import-graph=
466 |
467 | # Create a graph of internal dependencies in the given file (report RP0402 must
468 | # not be disabled)
469 | int-import-graph=
470 |
471 | # Force import order to recognize a module as part of the standard
472 | # compatibility libraries.
473 | known-standard-library=
474 |
475 | # Force import order to recognize a module as part of a third party library.
476 | known-third-party=enchant
477 |
478 |
479 | [EXCEPTIONS]
480 |
481 | # Exceptions that will emit a warning when being caught. Defaults to
482 | # "Exception"
483 | overgeneral-exceptions=builtins.Exception
484 |
--------------------------------------------------------------------------------
/django_admin_filters/static/js/datetimepicker.js:
--------------------------------------------------------------------------------
1 | (function ($) {
2 | 'use strict';
3 | $.fn.dateTimePicker = function (options) {
4 |
5 | var settings = $.extend({
6 | selectData: "now",
7 | dateFormat: "YYYY-MM-DD HH:mm",
8 | showTime: true,
9 | locale: 'en',
10 | positionShift: { top: 20, left: 0},
11 | title: "Select Date and Time",
12 | buttonTitle: "Select",
13 | allowBackwards: false
14 | }, options);
15 | moment.locale(settings.locale);
16 | var elem = this;
17 | var limitation = {"hour": 23, "minute": 59};
18 | var mousedown = false;
19 | var timeout = 800;
20 | var selectDate = settings.selectData == "now" ? moment() : moment(settings.selectData, settings.dateFormat);
21 | if (selectDate < moment() && (settings.allowBackwards == false)) {
22 | selectDate = moment();
23 | }
24 | var startDate = copyDate(moment());
25 | var lastSelected = copyDate(selectDate);
26 | return this.each(function () {
27 | if (lastSelected != selectDate) {
28 | selectDate = copyDate(lastSelected);
29 | }
30 | elem.addClass("dtp_main");
31 | updateMainElemGlobal();
32 | // elem.text(selectDate.format(settings.dateFormat));
33 | function updateMainElemGlobal() {
34 | var arrF = settings.dateFormat.split(' ');
35 | if (settings.showTime && arrF.length != 2) {
36 | arrF.length = 2;
37 | arrF[0] = 'DD/MM/YY';
38 | arrF[1] = 'HH:mm';
39 | }
40 | var $s = $('');
41 | $s.text(lastSelected.format(arrF[0]));
42 | elem.empty();
43 | elem.append($s);
44 | $s = $('');
45 | $s.addClass('fa fa-calendar ico-size');
46 | elem.append($s);
47 | if (settings.showTime) {
48 | $s = $('');
49 | $s.text(lastSelected.format(arrF[1]));
50 | elem.append($s);
51 | $s = $('');
52 | $s.addClass('fa fa-clock-o ico-size');
53 | elem.append($s);
54 | }
55 | }
56 | elem.on('click', function () {
57 | var $win = $('