Skip to content

Commit

Permalink
adds changelist filters form functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
abidibo committed Dec 23, 2020
1 parent 2d1b8c5 commit 12351e9
Show file tree
Hide file tree
Showing 18 changed files with 187 additions and 52 deletions.
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ install:
- pip install python-dotenv
- pip install django-filer
- pip install django-tinymce
- pip install django-admin-rangefilter
- pip install docutils
- django-admin.py --version
- pip install selenium
Expand Down
13 changes: 5 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ Everything is styled through CSS and when required, JS is used.
- Collapsable stacked inline entries
- Lazy loading of uploaded images
- Optional display of changelist filters in a modal
- Optional use of changelist filters as a form (combine some filters at once and perform the search action)
- Optional index page filled with google analytics widgets
- Customization available for recompiling the js app provided
- IT translations provided
Expand All @@ -70,14 +71,6 @@ The following packages are required to manage the Google Analytics index:
- google-api-python-client
- requests

At the moment __baton__ defines only 5 custom templates:

- `admin/base_site.html`, needed to inject the JS application (which includes css and images, compiled with [webpack](https://webpack.github.io/));
- `admin/change_form.html`, needed to inject the `baton_form_includes` stuff. In any case, the template extends the default one and just adds some stuff at the end of the content block, so it's still full compatible with the django one;
- `admin/change_list.html`, needed to inject the `baton_cl_includes` and `baton_cl_rows_attributes` stuff. In any case, the template extends the default one and just adds some stuff at the end of the content block, so it's still full compatible with the django one;
- `admin/delete_confirmation.html`, needed to wrap contents;
- `admin/delete_selected_confirmation.html`, same as above.

Baton is based on the following frontend technologies:

- Bootstrap 5
Expand Down Expand Up @@ -153,6 +146,7 @@ BATON = {
'ENABLE_IMAGES_PREVIEW': True,
'CHANGELIST_FILTERS_IN_MODAL': True,
'CHANGELIST_FILTERS_ALWAYS_OPEN': False,
'CHANGELIST_FILTERS_FORM': True,
'MENU_ALWAYS_COLLAPSED': False,
'MENU_TITLE': 'Menu',
'GRAVATAR_DEFAULT_IMG': 'retro',
Expand Down Expand Up @@ -199,6 +193,7 @@ Default value is `True`.
- `ENABLE_IMAGES_PREVIEW`: if set to `True` a preview is displayed above all input file fields which contain images. You can control how the preview is displayed by overriding the class `.baton-image-preview`. By default, previews have 100px height and with a box shadow (on "hover").
- `CHANGELIST_FILTERS_IN_MODAL`: if set to `True` the changelist filters are opened in a centered modal above the document, useful when you set many filters. By default, its value is `False` and the changelist filters appears from the right side of the changelist table.
- `CHANGELIST_FILTERS_ALWAYS_OPEN`: if set to `True` the changelist filters are opened by default. By default, its value is `False` and the changelist filters can be expanded clicking a toggler button. This option is considered only if `CHANGELIST_FILTERS_IN_MODAL` is `False`.
- `CHANGELIST_FILTERS_FORM`: if set to `True` the changelist filters are treated as in a form, you can set many of them and then press a filter button. With such option all standard filters are displayed as dropdowns.
- `COLLAPSABLE_USER_AREA`: if set to `True` the sidebar user area is collapsed and can be expanded to show links.
- `MENU_ALWAYS_COLLAPSED`: if set to `True` the menu is hidden at page load, and the navbar toggler is always visible, just click it to show the sidebar menu.
- `MENU_TITLE`: the menu title shown in the sidebar. If an empty string, the menu title is hidden and takes no space on larger screens, the default menu voice will still be visible in the mobile menu.
Expand Down Expand Up @@ -759,5 +754,7 @@ Read [CONTRIBUTING.md](CONTRIBUTING.md)

![Screenshot](docs/screenshots/filters-modal.png)

![Screenshot](docs/screenshots/filters-form.png)

![Screenshot](docs/screenshots/menu-collapsed.png)

1 change: 1 addition & 0 deletions baton/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
'COLLAPSABLE_USER_AREA': False,
'CHANGELIST_FILTERS_IN_MODAL': False,
'CHANGELIST_FILTERS_ALWAYS_OPEN': False,
'CHANGELIST_FILTERS_FORM': False,
'MENU_ALWAYS_COLLAPSED': False,
'GRAVATAR_DEFAULT_IMG': 'retro',
'LOGIN_SPLASH': None,
Expand Down
4 changes: 2 additions & 2 deletions baton/static/baton/app/dist/baton.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion baton/static/baton/app/dist/baton.min.js.map

Large diffs are not rendered by default.

43 changes: 41 additions & 2 deletions baton/static/baton/app/src/core/ChangeList.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ let ChangeList = {
init: function (opts) {
this._filtersDiv = $('#changelist-filter')
this.t = new Translator($('html').attr('lang'))
this.filtersForm = opts.changelistFiltersForm
this.filtersInModal = opts.changelistFiltersInModal
this.filtersAlwaysOpen = opts.changelistFiltersAlwaysOpen
this.initTemplates()
if (this._filtersDiv.length) {
this.activate()
this.fixRangeFilter()
}
},
activate: function () {
Expand Down Expand Up @@ -52,16 +54,24 @@ let ChangeList = {
titleEl.remove()
let content = $('#changelist-filter-modal')
// remove from dom
self.modal = new Modal({
this.modal = new Modal({
title,
content,
size: 'md',
hideFooter: true
hideFooter: !this.filtersForm,
actionBtnLabel: this.t.get('filter'),
actionBtnCb: function () { self.filter(content) }
})
_filtersToggler.click(() => {
self.modal.toggle()
})
} else {
if (this.filtersForm) {
// add filters button
let btn = $('<a />', {'class': 'btn btn-primary'}).html(this.t.get('filter'))
.on('click', () => this.filter($('#changelist-filter')))
$('#changelist-filter').append($('<div />', {'class': 'text-center mb-3'}).append(btn))
}
_filtersToggler.click(() => {
$(document.body).toggleClass('changelist-filter-active')
if (parseInt(this._filtersDiv.css('max-width')) === 100) {
Expand All @@ -78,6 +88,29 @@ let ChangeList = {
$('#changelist-form .results').css('padding-top', '78px')
}
},
getDropdownValue: function (dropdown) {
let items = $(dropdown).find('option').attr('value').substr(1).split('&')
let values = $(dropdown).val().substr(1).split('&').filter(item => items.indexOf(item) === -1)
return values.length ? values.join('&') : null
},
filter: function (wrapper) {
var self = this
let qs = []

let dropdowns = wrapper.find('select')
let textInputs = wrapper.find('input').not('[type=hidden]')

dropdowns
.toArray()
.map(el => self.getDropdownValue(el))
.filter(v => v !== null)
.forEach(v => qs.push(v))

textInputs.each((idx, el) => el.value !== '' ? qs.push(`${el.name}=${el.value}`) : null)

// console.log(location.pathname + (qs.length ? '?' + qs.filter(q => q !== '').join('&') : ''), qs)
location.href = location.pathname + (qs.length ? '?' + qs.filter(q => q !== '').join('&') : '')
},
initTemplates: function () {
const positionMap = {
above: 'before',
Expand Down Expand Up @@ -151,6 +184,12 @@ let ChangeList = {
console.error(e)
}
})
},
fixRangeFilter: function () {
if (this.filtersForm) {
$('.admindatefilter .controls').remove()
$('.admindatefilter form').onSubmit = function () { return false }
}
}
}

Expand Down
31 changes: 31 additions & 0 deletions baton/static/baton/app/src/styles/_changelist.scss
Original file line number Diff line number Diff line change
Expand Up @@ -403,4 +403,35 @@
.baton-cl-include-below {
order: 2;
}

.admindatefilter {
border-bottom: 0 !important;
margin-right: 15px;
padding-bottom: 0;
position: relative;

form > p {
display: flex;
flex-direction: row;
}

.datetimeshortcuts {
right: 2rem;
}

input[type='text'] {
@extend .form-control;
@extend .form-control-sm;
flex-grow: 1;
}

input[type='submit'],
input[type='reset'] {
@extend .btn-sm;
}

input[type='reset'] {
@extend .btn-secondary;
}
}
}
21 changes: 21 additions & 0 deletions baton/templates/admin/filter.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{% load i18n baton_tags %}
<h3>{% blocktranslate with filter_title=title %} By {{ filter_title }} {% endblocktranslate %}</h3>
{% baton_config_value 'CHANGELIST_FILTERS_FORM' as filters_form %}
{% if filters_form %}
<ul style="padding-right: 15px;">
<li>
<select class="form-select form-select-sm" style="width: 100% !important;">
{% for choice in choices %}
<option{% if choice.selected %} selected="selected"{% endif %} value="{{ choice.query_string|iriencode }}">{{ choice.display }}</option>
{% endfor %}
</select>
</li>
</ul>
{% else %}
<ul>
{% for choice in choices %}
<li{% if choice.selected %} class="selected"{% endif %}>
<a href="{{ choice.query_string|iriencode }}" title="{{ choice.display }}">{{ choice.display }}</a></li>
{% endfor %}
</ul>
{% endif %}
52 changes: 33 additions & 19 deletions baton/templates/baton/filters/dropdown_filter.html
Original file line number Diff line number Diff line change
@@ -1,24 +1,38 @@
{% load i18n %}
<script type="text/javascript">var go_from_select = function(opt) { window.location = window.location.pathname + opt };</script>
{% load i18n baton_tags %}
{% baton_config_value 'CHANGELIST_FILTERS_FORM' as filters_form %}
{% if not filters_form %}
<script type="text/javascript">var go_from_select = function(opt) { window.location = window.location.pathname + opt };</script>
{% endif %}
<h3>{% blocktrans with title as filter_title %} By {{ filter_title }} {% endblocktrans %}</h3>
<ul class="admin-filter-{{ title|cut:' ' }}" style="padding-right: 15px;">
{% if choices|slice:"3:" %}
<li>
<select class="form-select form-select-sm" style="width: 100% !important;"
onchange="go_from_select(this.options[this.selectedIndex].value)">
{% for choice in choices %}
<option{% if choice.selected %} selected="selected"{% endif %}
value="{{ choice.query_string|iriencode }}">{{ choice.display }}</option>
{% endfor %}
</select>
</li>
{% else %}

{% for choice in choices %}
<li{% if choice.selected %} class="selected"{% endif %}>
<a href="{{ choice.query_string|iriencode }}">{{ choice.display }}</a>
{% if filters_form %}
<li>
<select class="form-select form-select-sm" style="width: 100% !important;" name="{{ spec.parameter_name }}">
{% for choice in choices %}
<option{% if choice.selected %} selected="selected"{% endif %}
value="{{ choice.query_string|iriencode }}">{{ choice.display }} {{ choice.name }} {{ choice.value }}</option>
{% endfor %}
</select>
</li>
{% endfor %}
{% else %}
{% if choices|slice:"3:" %}
<li>
<select class="form-select form-select-sm" style="width: 100% !important;"
onchange="go_from_select(this.options[this.selectedIndex].value)">
{% for choice in choices %}
<option{% if choice.selected %} selected="selected"{% endif %}
value="{{ choice.query_string|iriencode }}">{{ choice.display }}</option>
{% endfor %}
</select>
</li>
{% else %}

{% endif %}
{% for choice in choices %}
<li{% if choice.selected %} class="selected"{% endif %}>
<a href="{{ choice.query_string|iriencode }}">{{ choice.display }}</a>
</li>
{% endfor %}

{% endif %}
{% endif %}
</ul>
41 changes: 25 additions & 16 deletions baton/templates/baton/filters/input_filter.html
Original file line number Diff line number Diff line change
@@ -1,26 +1,35 @@
{% load i18n %}
{% load i18n baton_tags %}

{% baton_config_value 'CHANGELIST_FILTERS_FORM' as filters_form %}
<h3>{% blocktrans with filter_title=title %} By {{ filter_title }} {% endblocktrans %}</h3>
<ul style="padding-right: 15px;">
<li>
{% with choices.0 as all_choice %}
<form method="GET" action="">
{% if not filters_form %}
{% with choices.0 as all_choice %}
<form method="GET" action="">

{% for k, v in all_choice.query_parts %}
<input type="hidden" name="{{ k }}" value="{{ v }}" />
{% endfor %}
{% for k, v in all_choice.query_parts %}
<input type="hidden" name="{{ k }}" value="{{ v }}" />
{% endfor %}

<input type="text"
class="form-control form-control-sm"
value="{{ spec.value|default_if_none:'' }}"
placeholder="{% trans "type and press enter..."%}"
name="{{ spec.parameter_name }}"/>
<input type="text"
class="form-control form-control-sm"
value="{{ spec.value|default_if_none:'' }}"
placeholder="{% trans "type and press enter..."%}"
name="{{ spec.parameter_name }}"/>

{% if not all_choice.selected %}
<a style="margin-top: .5rem;" href="{{ all_choice.query_string }}" class="btn btn-sm btn-warning">x {% trans 'Remove' %}</a>
{% endif %}
{% if not all_choice.selected %}
<a style="margin-top: .5rem;" href="{{ all_choice.query_string }}" class="btn btn-sm btn-warning">{% trans 'Reset' %}</a>
{% endif %}

</form>
{% endwith %}
</form>
{% endwith %}
{% else %}
<input type="text"
class="form-control form-control-sm"
value="{{ spec.value|default_if_none:'' }}"
placeholder="{% trans "type and press enter..."%}"
name="{{ spec.parameter_name }}"/>
{% endif %}
</li>
</ul>
6 changes: 6 additions & 0 deletions baton/templatetags/baton_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ def baton_config():
"enableImagesPreview": get_config('ENABLE_IMAGES_PREVIEW'),
"changelistFiltersInModal": get_config('CHANGELIST_FILTERS_IN_MODAL'),
"changelistFiltersAlwaysOpen": get_config('CHANGELIST_FILTERS_ALWAYS_OPEN'),
"changelistFiltersForm": get_config('CHANGELIST_FILTERS_FORM'),
"collapsableUserArea": get_config('COLLAPSABLE_USER_AREA'),
"menuAlwaysCollapsed": get_config('MENU_ALWAYS_COLLAPSED'),
"menuTitle": escapejs(get_config('MENU_TITLE')),
Expand All @@ -32,6 +33,11 @@ def baton_config():
return conf


@register.simple_tag
def baton_config_value(key):
return get_config(key)


@register.inclusion_tag('baton/analytics.html', takes_context=True)
def analytics(context, next=None):

Expand Down
12 changes: 11 additions & 1 deletion docs/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ This is an example of configuration::
'ENABLE_IMAGES_PREVIEW': True,
'CHANGELIST_FILTERS_IN_MODAL': True,
'CHANGELIST_FILTERS_ALWAYS_OPEN': False,
'CHANGELIST_FILTERS_FORM': True,
'COLLAPSABLE_USER_AREA': False,
'MENU_ALWAYS_COLLAPSED': False,
'MENU_TITLE': 'Menu',
Expand Down Expand Up @@ -144,7 +145,16 @@ If set to ``True`` the changelist filters are opened in a centered modal above t
Changelist filters always open
-----------------------

if set to ``True`` the changelist filters are opened by default. By default, its value is ``False`` and the changelist filters can be expanded clicking a toggler button. This option is considered only if ``CHANGELIST_FILTERS_IN_MODAL`` is ``False``
If set to ``True`` the changelist filters are opened by default. By default, its value is ``False`` and the changelist filters can be expanded clicking a toggler button. This option is considered only if ``CHANGELIST_FILTERS_IN_MODAL`` is ``False``

**Default**: False

Changelist filters form
-----------------------

.. image:: images/filters-form.png

If set to ``True`` the changelist filters are treated as in a form, you can set many of them at once and then press a filter button in order to actually perform the filtering. With such option all standard filters are displayed as dropdowns.

**Default**: False

Expand Down
Binary file added docs/images/filters-form.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshots/filters-form.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions testapp/app/app/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
'django.contrib.staticfiles',
'django.contrib.admindocs',
'easy_thumbnails',
'rangefilter',
'filer',
'tinymce',
'mptt',
Expand Down Expand Up @@ -120,6 +121,7 @@
'ENABLE_IMAGES_PREVIEW': True,
'CHANGELIST_FILTERS_IN_MODAL': True,
'CHANGELIST_FILTERS_ALWAYS_OPEN': False,
'CHANGELIST_FILTERS_FORM': True,
'COLLAPSABLE_USER_AREA': False,
'MENU_ALWAYS_COLLAPSED': False,
'GRAVATAR_DEFAULT_IMG': 'robohash',
Expand Down
7 changes: 5 additions & 2 deletions testapp/app/app/tests/test_e2e_input_filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,12 @@ def test_filter(self):
'.changelist-filter-toggler')
filter_button.click()
input = self.driver.find_element_by_css_selector(
'#changelist-filter-modal input')
'#changelist-filter-modal li > input')
input.send_keys('super band')
input.send_keys(Keys.RETURN)
btn = self.driver.find_element_by_css_selector(
'.modal .btn-action')
btn.click()
time.sleep(1)
rows = self.driver.find_elements_by_css_selector(
'#result_list tbody tr')
self.assertEqual(len(rows), 1)
Binary file modified testapp/app/db.sqlite3
Binary file not shown.
Loading

0 comments on commit 12351e9

Please sign in to comment.