Skip to content

Commit

Permalink
⚡ [PERF] Delay loading filter form in List Views
Browse files Browse the repository at this point in the history
  • Loading branch information
Chatewgne committed Jan 7, 2025
1 parent 8bed57f commit 950ac3a
Show file tree
Hide file tree
Showing 20 changed files with 291 additions and 68 deletions.
9 changes: 9 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ CHANGELOG
8.10.0+dev (XXXX-XX-XX)
-----------------------

**Breaking Changes**

- `MapEntityList` view now needs an extra `MapEntityFilter` view to maintain filtering functionalities. If you customize the `FilterSet`, it must be added to: `MapEntityFormatList`, `MapEntityFilter`, and `MapEntityViewSet`. See the "Filters" section in documentation : https://django-mapentity.readthedocs.io/en/stable/customization.html#filters

**Performances**

- Delay loading filter options only when opening form


8.10.0 (2024-10-02)
-----------------------

Expand Down
24 changes: 20 additions & 4 deletions docs/customization.rst
Original file line number Diff line number Diff line change
Expand Up @@ -67,22 +67,38 @@ geographical data. Create a file ``filters.py`` in your app::
from mapentity.filters import MapEntityFilterSet


class MuseumFilter(MapEntityFilterSet):
class MuseumFilterSet(MapEntityFilterSet):
class Meta:
model = Museum
fields = ('name', )


Then update ``views.py`` to use your custom filter in your curstom views::
Then update ``views.py`` to use your custom filter in your custom views::

from .filters import MuseumFilterSet

from .filters import MuseumFilter

class MuseumList(MapEntityList):
model = Museum
filterform = MuseumFilter
columns = ['id', 'name']


class MuseumFilter(MapEntityFilter):
model = Museum
filterset_class = MuseumFilterSet


class MuseumFormatList(MapEntityFormatList):
model = Museum
filterset_class = MuseumFilterSet


class MuseumViewSet(MapEntityViewSet):
model = Museum
filterset_class = MuseumFilterSet



Forms
-----

Expand Down
5 changes: 4 additions & 1 deletion mapentity/locale/en/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-05-22 13:37+0000\n"
"POT-Creation-Date: 2025-01-07 10:05+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <[email protected]>\n"
Expand Down Expand Up @@ -102,6 +102,9 @@ msgstr ""
msgid "Filter"
msgstr ""

msgid "Loading"
msgstr "Chargement"

msgid "Measure distances"
msgstr ""

Expand Down
5 changes: 4 additions & 1 deletion mapentity/locale/fr/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-05-22 13:37+0000\n"
"POT-Creation-Date: 2025-01-07 10:05+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <[email protected]>\n"
Expand Down Expand Up @@ -101,6 +101,9 @@ msgstr "Aucun(e)"
msgid "Filter"
msgstr "Filtrer"

msgid "Loading"
msgstr ""

msgid "Measure distances"
msgstr "Mesurer les distances"

Expand Down
8 changes: 7 additions & 1 deletion mapentity/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
ENTITY_VIEWSET = "drf-viewset"
ENTITY_FORMAT_LIST = "format_list"
ENTITY_DETAIL = "detail"
ENTITY_FILTER = "filter"
ENTITY_MAPIMAGE = "mapimage"
ENTITY_DOCUMENT = "document"
ENTITY_MARKUP = "markup"
Expand All @@ -37,7 +38,7 @@
ENTITY_UPDATE_GEOM = "update_geom"

ENTITY_KINDS = (
ENTITY_LIST, ENTITY_VIEWSET, ENTITY_FORMAT_LIST, ENTITY_DETAIL, ENTITY_MAPIMAGE,
ENTITY_LIST, ENTITY_FILTER, ENTITY_VIEWSET, ENTITY_FORMAT_LIST, ENTITY_DETAIL, ENTITY_MAPIMAGE,
ENTITY_DOCUMENT, ENTITY_MARKUP, ENTITY_CREATE, ENTITY_DUPLICATE, ENTITY_UPDATE, ENTITY_DELETE, ENTITY_UPDATE_GEOM
)

Expand Down Expand Up @@ -161,6 +162,7 @@ def get_entity_kind_permission(cls, entity_kind):
ENTITY_DELETE: ENTITY_PERMISSION_DELETE,
ENTITY_DETAIL: ENTITY_PERMISSION_READ,
ENTITY_LIST: ENTITY_PERMISSION_READ,
ENTITY_FILTER: ENTITY_PERMISSION_READ,
ENTITY_VIEWSET: ENTITY_PERMISSION_READ,
ENTITY_MARKUP: ENTITY_PERMISSION_READ,
ENTITY_FORMAT_LIST: ENTITY_PERMISSION_EXPORT,
Expand Down Expand Up @@ -351,6 +353,10 @@ def map_image_path(self):
class MapEntityMixin(BaseMapEntityMixin):
attachments = GenericRelation(settings.PAPERCLIP_ATTACHMENT_MODEL)

@classmethod
def get_filter_url(cls):
return reverse(cls._entity.url_name(ENTITY_FILTER))

class Meta:
abstract = True

Expand Down
1 change: 1 addition & 0 deletions mapentity/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ def get_queryset(self):
def _url_path(self, view_kind):
kind_to_urlpath = {
mapentity_models.ENTITY_LIST: r'^{modelname}/list/$',
mapentity_models.ENTITY_FILTER: r'^{modelname}/filter/$',
mapentity_models.ENTITY_VIEWSET: r'^api/{modelname}/drf/{modelname}$',
mapentity_models.ENTITY_FORMAT_LIST: r'^{modelname}/list/export/$',
mapentity_models.ENTITY_DETAIL: r'^{modelname}/(?P<pk>\d+)/$',
Expand Down
72 changes: 52 additions & 20 deletions mapentity/static/mapentity/mapentity.filter.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ MapEntity.TogglableFilter = L.Class.extend({

this.fields = {};
this.visible = false;
this.loaded_form = false;
this.popover = $('#filters-popover')
.popover({
placement: 'right',
Expand All @@ -31,26 +32,6 @@ MapEntity.TogglableFilter = L.Class.extend({
self.setfield(this);
});

// Use chosen for multiple values
$("form#mainfilter").bind("reset", function() {
setTimeout(function() {
$('form#mainfilter select[multiple]').trigger('chosen:updated');
}, 1);
});

// Make sure filter-set class is added if a choice is selected.
$('#mainfilter select[multiple]').chosen().on('change', function (e) {
var $target = $(e.target),
name = $target.attr('name'),
$container = $('div#id_' + name + '_chzn > ul');
if ($(e.target).find('option:selected').length > 0) {
$container.addClass('filter-set');
}
else {
$container.removeClass('filter-set');
}
});


//
// Filters open/close
Expand Down Expand Up @@ -87,6 +68,57 @@ MapEntity.TogglableFilter = L.Class.extend({
return $(this.popover.data('bs.popover').tip);
},

load_filter_form: function (mapsync) {
var self = this;
// On first click on Filter button, load HTML content for form
var $mainfilter = $('#mainfilter');
if (!self.loaded_form) {
var filter_url = $mainfilter.attr('filter-url');
$.get(filter_url)
.done(response => {
$('#filters-panel .filter-spinner-container').hide();
// Replace simple form that contains only BBOX with full form, including attributes
$mainfilter.replaceWith(response);
// Update L.MapListSync to refresh datatable on change
mapsync.options.filter.form = $mainfilter;
// Bind new form buttons to keep refreshing list on click
$("#filter").click(function(e) {
mapsync._onFormSubmit(e);
});
$("#reset").click(function(e) {
mapsync._onFormReset(e);
});
// Bind setfields to update list of enabled fields displayed on hover
$mainfilter.find('select,input').change(function () {
self.setfield(this);
});
self.loaded_form = true;

// Use chosen for multiple values
$("form#mainfilter").bind("reset", function() {
setTimeout(function() {
$('form#mainfilter select[multiple]').trigger('chosen:updated');
}, 1);
});

// Make sure filter-set class is added if a choice is selected.
$('#mainfilter select[multiple]').chosen().on('change', function (e) {
var $target = $(e.target),
name = $target.attr('name'),
$container = $('div#id_' + name + '_chzn > ul');
var hasSelectedOption = $target.find('option:selected').length > 0;
$container.toggleClass('filter-set', hasSelectedOption);
});
// Move right-filters to right side
$('#mainfilter .right-filter').parent('p').detach().appendTo('#mainfilter > .right');
// Trigger event allowing to launch further processing
var context = $('body').data();
$(window).trigger('entity:view:filter', {modelname: context.modelname});
})
.fail(xhr => console.error('Error:', xhr.status));
}
},

showinfo: function () {
// If popover is already visible, do not show hover
if (this.visible)
Expand Down
4 changes: 4 additions & 0 deletions mapentity/static/mapentity/mapentity.map.js
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,10 @@ $(window).on('entity:map:list', function (e, data) {
mapsync.on('reloaded', function (data) {
t.setsubmit();
});
// On first click on Filter button, load HTML content for form
t.$button.click(function (e) {
t.load_filter_form(mapsync);
});

// Map screenshot button
var screenshot = new L.Control.Screenshot(window.SETTINGS.urls.screenshot, function () {
Expand Down
22 changes: 22 additions & 0 deletions mapentity/static/mapentity/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -1009,3 +1009,25 @@ TODO: please help;
.dynamic-form label.requiredField {
font-weight: normal;
}

.filter-spinner-container {
display: flex;
justify-content: center;
align-items: center;
height: 80px;
background-color: #f5f5f5;
}

.filter-spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
}

@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
14 changes: 5 additions & 9 deletions mapentity/templates/mapentity/_mapentity_list_filter.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,11 @@
<div id="filters-close">
<a href="#"><i class="bi bi-x-circle-fill"></i></a>
</div>
<form id="mainfilter" action="{{ model.get_datatablelist_url }}">
<div class="left form-group">
{{ filterform.form.as_p }}
<div class="filter-spinner-container text-center">
<div class="spinner-border text-primary" role="status">
<span class="sr-only">{% trans "Loading" %}</span>
</div>
<div class="right form-group"></div>
<div class="actions">
<input id="reset" type="reset" class="btn btn-light"/>
<a id="filter" class="btn btn-primary"><i class="bi bi-search"></i> {% trans "Filter" %}</a>
</div>
</form>
</div>
{% include "mapentity/mapentity_filter.html" %}
</div>
</div>
14 changes: 14 additions & 0 deletions mapentity/templates/mapentity/mapentity_filter.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{% load i18n static %}

{% block mainfilter %}
<form id="mainfilter" action="{{ model.get_datatablelist_url }}" filter-url="{{ filter_url }}">
<div class="left form-group">
{{ filter.form.as_p }}
</div>
<div class="right form-group"></div>
<div class="actions">
<input id="reset" type="reset" class="btn btn-light"/>
<a id="filter" class="btn btn-primary"><i class="bi bi-search"></i> {% trans "Filter" %}</a>
</div>
</form>
{% endblock mainfilter %}
2 changes: 1 addition & 1 deletion mapentity/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ def test_formfilter_in_list_context(self):
return # Abstract test should not run
response = self.client.get(self.model.get_list_url())
self.assertEqual(response.status_code, 200)
self.assertTrue(response.context['filterform'] is not None)
self.assertTrue(response.context['filter'] is not None)

# REST API tests

Expand Down
3 changes: 3 additions & 0 deletions mapentity/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from .generic import (
Convert,
MapEntityList,
MapEntityFilter,
MapEntityFormat,
MapEntityMapImage,
MapEntityDocument,
Expand All @@ -34,6 +35,7 @@

MAPENTITY_GENERIC_VIEWS = [
MapEntityList,
MapEntityFilter,
MapEntityFormat,
MapEntityMapImage,
MapEntityDocument,
Expand All @@ -48,6 +50,7 @@
__all__ = [
'Convert',
'MapEntityList',
'MapEntityFilter',
'MapEntityFormat',
'MapEntityMapImage',
'MapEntityDocument',
Expand Down
Loading

0 comments on commit 950ac3a

Please sign in to comment.