From 5cfa3fa09b5446d1e5b2885924f81fec2b54ba4d Mon Sep 17 00:00:00 2001 From: Sebastian Gutzeit Date: Tue, 26 Sep 2023 15:37:15 +0200 Subject: [PATCH] App Datenmanagement: Bereinigungsarbeiten Code --- .eslintrc.json | 7 +- bemas/views/base.py | 35 +- datenmanagement/admin.py | 1 - datenmanagement/models/base.py | 4 +- datenmanagement/models/models_complex.py | 2 +- .../static/datenmanagement/js/map.js | 296 ++++++++ .../templates/datenmanagement/list.html | 9 +- .../templates/datenmanagement/map.html | 694 +++++------------- datenmanagement/views/functions.py | 21 +- datenmanagement/views/views_general.py | 6 +- datenmanagement/views/views_list_map.py | 179 +++-- toolbox/utils.py | 42 ++ 12 files changed, 650 insertions(+), 646 deletions(-) delete mode 100644 datenmanagement/admin.py create mode 100644 datenmanagement/static/datenmanagement/js/map.js create mode 100644 toolbox/utils.py diff --git a/.eslintrc.json b/.eslintrc.json index c06f2729..a5bc36c2 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -14,6 +14,7 @@ "configureMap": true, "createFilterObject": true, "currMap": true, + "customMapFilters": true, "enableAddressReferenceButton": true, "enableMapLocate": true, "featureGeometry": true, @@ -29,8 +30,10 @@ "keepDjangoRequiredMessages": true, "L": true, "martinez": true, + "objectsExtent": true, "orangeMarker": true, "proj4": true, + "Promise": true, "redMarker": true, "results": true, "searchField": true, @@ -42,13 +45,13 @@ "setMapExtentByBoundingBox": true, "setMapExtentByLeafletBounds": true, "setMarkerToAddressSearchResult": true, + "showAllMapFeatures": true, "subsetting": true, "toggleModal": true, "Wkt": true }, "ignorePatterns": [ - "datenmanagement/templates/datenmanagement/form.html", - "datenmanagement/templates/datenmanagement/map.html" + "datenmanagement/templates/datenmanagement/form.html" ], "parserOptions": { "ecmaVersion": "latest", diff --git a/bemas/views/base.py b/bemas/views/base.py index 173113b0..672730f5 100644 --- a/bemas/views/base.py +++ b/bemas/views/base.py @@ -1,13 +1,13 @@ from datetime import date, datetime from django.apps import apps -from django.db.models import ForeignKey, Q +from django.db.models import ForeignKey from django.urls import reverse from django.utils.html import escape from django_datatables_view.base_datatable_view import BaseDatatableView from jsonview.views import JsonView -from re import match, search, sub from toolbox.models import Subsets +from toolbox.utils import optimize_datatable_filter from bemas.models import Codelist, Complaint, Contact, LogEntry, Organization, Originator, Person from bemas.utils import LOG_ACTIONS, get_foreign_key_target_model, get_foreign_key_target_object, \ get_icon_from_settings, is_bemas_admin, is_bemas_user, is_geometry_field @@ -254,35 +254,8 @@ def filter_queryset(self, qs): # foreign key target model is object class else: search_column += '__search_content' - case_a = search('^[0-9]{2}\\.[0-9]{2}\\.[0-9]{4}$', search_element) - case_b = search('^[0-9]{2}\\.[0-9]{4}$', search_element) - case_c = search('^[0-9]{2}\\.[0-9]{2}$', search_element) - if case_a or case_b or case_c: - search_element_splitted = search_element.split('.') - kwargs = { - '{0}__{1}'.format(search_column, 'icontains'): (search_element_splitted[ - 2] + '-' if case_a else '') + - search_element_splitted[1] + '-' + - search_element_splitted[0] - } - elif search_element == 'ja': - kwargs = { - '{0}__{1}'.format(search_column, 'icontains'): 'true' - } - elif search_element == 'nein' or search_element == 'nei': - kwargs = { - '{0}__{1}'.format(search_column, 'icontains'): 'false' - } - elif match(r"^[0-9]+,[0-9]+$", search_element): - kwargs = { - '{0}__{1}'.format(search_column, 'icontains'): sub(',', '.', search_element) - } - else: - kwargs = { - '{0}__{1}'.format(search_column, 'icontains'): search_element - } - q = Q(**kwargs) - qs_params_inner = qs_params_inner | q if qs_params_inner else q + qs_params_inner = optimize_datatable_filter( + search_element, search_column, qs_params_inner) qs_params = qs_params & qs_params_inner if qs_params else qs_params_inner qs = qs.filter(qs_params) return qs diff --git a/datenmanagement/admin.py b/datenmanagement/admin.py deleted file mode 100644 index 694323fa..00000000 --- a/datenmanagement/admin.py +++ /dev/null @@ -1 +0,0 @@ -from django.contrib import admin diff --git a/datenmanagement/models/base.py b/datenmanagement/models/base.py index 0ba444dd..c58b7ed5 100644 --- a/datenmanagement/models/base.py +++ b/datenmanagement/models/base.py @@ -162,10 +162,10 @@ class BasemodelMeta: map_deadlinefilter_fields = None # Dictionary: # names of those fields of this model (as keys) - # which shall appear as interval/range map filters in the map view of this model + # which shall appear as interval map filters in the map view of this model # in exactly this order, with their respective titles (as values) # (always processed in pairs!) - map_rangefilter_fields = None + map_intervalfilter_fields = None # Dictionary: # names of those fields of this model (as keys) # which shall appear as map filters in the map view of this model diff --git a/datenmanagement/models/models_complex.py b/datenmanagement/models/models_complex.py index 758c6b77..2b5edbe1 100644 --- a/datenmanagement/models/models_complex.py +++ b/datenmanagement/models/models_complex.py @@ -1020,7 +1020,7 @@ class BasemodelMeta(ComplexModel.BasemodelMeta): 'art_kontrolle': 'art' } map_feature_tooltip_field = 'art_kontrolle' - map_rangefilter_fields = { + map_intervalfilter_fields = { 'startzeitpunkt': 'Startzeitpunkt', 'endzeitpunkt': 'Endzeitpunkt' } diff --git a/datenmanagement/static/datenmanagement/js/map.js b/datenmanagement/static/datenmanagement/js/map.js new file mode 100644 index 00000000..8a6e50b1 --- /dev/null +++ b/datenmanagement/static/datenmanagement/js/map.js @@ -0,0 +1,296 @@ + +/** + * @function + * @name applyFilters + * + * applies filters for passed model based on passed filter objects list + * + * @param {Object[]} filterObjectsList - filter objects list + */ +function applyFilters(filterObjectsList) { + if (filterObjectsList.length > 0) { + showAllMapFeatures(); + window.currMap.eachLayer(function(layer) { + if (layer.feature) { + filterGeoJsonFeatures(filterObjectsList, layer, false); + } else if (layer.id === 'cluster') { + let clusterLayer = layer; + layer.eachLayer(function(subLayer) { + filterGeoJsonFeatures(filterObjectsList, subLayer, true, clusterLayer); + }); + layer.refreshClusters(); + } + }); + } else { + showAllMapFeatures(); + } +} + +/** + * @function + * @name fetchGeoJsonFeatureCollection + * + * fetches GeoJSON feature collection for map + * + * @param {boolean} heavyLoad - large amounts of data expected? + * @param {number} [limit=0] - limit for current loading step + * @param {number} [offset=0] - offset for current loading step (= multiple ``limit``) + * @param {string} url - map data composition URL + */ +async function fetchGeoJsonFeatureCollection(heavyLoad = false, limit = 0, offset = 0, url = window.mapDataUrl) { + try { + // set parameters for the current loading step if large amounts of data are expected + if (heavyLoad) { + url += '?limit=' + limit + '&offset=' + offset; + } + toggleModal( + $('#loading-modal'), + 'Laden der Kartendaten', + 'Die Kartendaten werden (nach-)geladen.' + (heavyLoad ? ' Dies kann einen Moment dauern, da es sich insgesamt um eine sehr große Datenmenge handelt.' : ''), + heavyLoad ? '#loading-modal-map-data' : null + ); + const response = await fetch(url, { + method: 'GET' + }); + const data = await response.json(); + if (heavyLoad) { + window.count += data.features.length; + $('#loading-modal-map-data-count').text(window.count); + if (window.count === window.border) { + toggleModal($('#loading-modal')); + } + } else + toggleModal($('#loading-modal')); + return data; + } catch (error) { + console.error(error); + } +} + +/** + * @function + * @name filterGeoJsonFeatures + * + * filters GeoJSON features based on passed filter objects list + * + * @param {Object[]} filterObjectsList - filter objects list + * @param {Object} layer - GeoJSON map layer + * @param {boolean} isSubLayer - GeoJSON map layer is part of a map cluster? + * @param {Object} [clusterLayer=layer] - map cluster (equals GeoJSON map layer per default) + */ +function filterGeoJsonFeatures(filterObjectsList, layer, isSubLayer, clusterLayer = layer) { + let stillVisible = true; + for (let i = 0; i < filterObjectsList.length; i++) { + // deadline filter + if (filterObjectsList[i].name === 'deadline') { + if (!(new Date(filterObjectsList[i].value) > new Date(layer.feature.properties['deadline_0'])) || !(new Date(layer.feature.properties['deadline_1']) > new Date(filterObjectsList[i].value))) + stillVisible = false; + // "lefty" interval filter + } else if (filterObjectsList[i].intervalside === 'left') { + if (filterObjectsList[i].type === 'date') { + if (new Date(filterObjectsList[i].value) > new Date(layer.feature.properties[filterObjectsList[i].name])) + stillVisible = false; + } else if (filterObjectsList[i].type === 'datetime') { + if (new Date(filterObjectsList[i].value).valueOf() > new Date(layer.feature.properties[filterObjectsList[i].name]).valueOf()) + stillVisible = false; + } else { + if (filterObjectsList[i].value > layer.feature.properties[filterObjectsList[i].name]) + stillVisible = false; + } + // "righty" interval filter + } else if (filterObjectsList[i].intervalside === 'right') { + if (filterObjectsList[i].type === 'date') { + if (new Date(layer.feature.properties[filterObjectsList[i].name]) > new Date(filterObjectsList[i].value)) + stillVisible = false; + } else if (filterObjectsList[i].type === 'datetime') { + if (new Date(layer.feature.properties[filterObjectsList[i].name]).valueOf() > new Date(filterObjectsList[i].value).valueOf()) + stillVisible = false; + } else { + if (layer.feature.properties[filterObjectsList[i].name] > filterObjectsList[i].value) + stillVisible = false; + } + // checkbox sets + } else if (filterObjectsList[i].type === 'checkbox-set' && filterObjectsList[i].value.length > 0) { + if (filterObjectsList[i].filtertype === 'additive') { + for (let j = 0; j < filterObjectsList[i].value.length; j++) { + if (layer.feature.properties[filterObjectsList[i].name].toLowerCase().indexOf(filterObjectsList[i].value[j].toLowerCase()) === -1) + stillVisible = false; + } + } else { + // work with auxiliary variable to do justice to the exclusive character (OR) + let tempStillVisible = false; + for (let k = 0; k < filterObjectsList[i].value.length; k++) { + if (layer.feature.properties[filterObjectsList[i].name].toLowerCase().indexOf(filterObjectsList[i].value[k].toLowerCase()) !== -1) { + tempStillVisible = true; + break; + } + } + if (!tempStillVisible) + stillVisible = false; + } + } else if (filterObjectsList[i].type !== 'checkbox-set') { + // date or datetime filter + if (filterObjectsList[i].type === 'date' || filterObjectsList[i].type === 'datetime') { + // negative or positive impact logic? + if (filterObjectsList[i].logic === 'negative') { + if (new Date(filterObjectsList[i].value).valueOf() === new Date(layer.feature.properties[filterObjectsList[i].name]).valueOf()) + stillVisible = false; + } else { + if (new Date(filterObjectsList[i].value).valueOf() !== new Date(layer.feature.properties[filterObjectsList[i].name]).valueOf()) + stillVisible = false; + } + // text filter based on list values + } else if (filterObjectsList[i].type === 'list') { + // negative or positive impact logic? + if (filterObjectsList[i].logic === 'negative') { + if (layer.feature.properties[filterObjectsList[i].name].toLowerCase() === filterObjectsList[i].value.toLowerCase()) + stillVisible = false; + } else { + if (layer.feature.properties[filterObjectsList[i].name].toLowerCase() !== filterObjectsList[i].value.toLowerCase()) + stillVisible = false; + } + // ordinary text filter + } else { + let value = filterObjectsList[i].value.toLowerCase(); + // handle decimal separator if a decimal number was entered + if (/^([0-9]+,[0-9]+)$/.test(filterObjectsList[i].value) === true) + value = filterObjectsList[i].value.replace(',', '.'); + // negative or positive impact logic? + if (filterObjectsList[i].logic === 'negative') { + if (layer.feature.properties[filterObjectsList[i].name].toLowerCase().indexOf(value) !== -1) + stillVisible = false; + } else { + if (layer.feature.properties[filterObjectsList[i].name].toLowerCase().indexOf(value) === -1) + stillVisible = false; + } + } + } + } + // if feature is not marked for hiding and shall therefore continue to be visible... + if (stillVisible) { + // update variables for primary keys and map extent of currently filtered data + window.currentFilterPrimaryKeys.push(layer.feature.properties['{{ model_pk_field_name }}']); + let north = ((layer.feature.geometry.type === 'Point') ? layer.getLatLng().lat : layer.getBounds().getNorth()); + let east = ((layer.feature.geometry.type === 'Point') ? layer.getLatLng().lng : layer.getBounds().getEast()); + let south = ((layer.feature.geometry.type === 'Point') ? layer.getLatLng().lat : layer.getBounds().getSouth()); + let west = ((layer.feature.geometry.type === 'Point') ? layer.getLatLng().lng : layer.getBounds().getWest()); + if (window.currentFilterExtent.length === 0) { + window.currentFilterExtent[0] = []; + window.currentFilterExtent[0][0] = north; + window.currentFilterExtent[0][1] = east; + window.currentFilterExtent[1] = []; + window.currentFilterExtent[1][0] = south; + window.currentFilterExtent[1][1] = west; + } else { + if (window.currentFilterExtent[0][0] > north) + window.currentFilterExtent[0][0] = north; + if (window.currentFilterExtent[0][1] > east) + window.currentFilterExtent[0][1] = east; + if (window.currentFilterExtent[1][0] < south) + window.currentFilterExtent[1][0] = south; + if (window.currentFilterExtent[1][1] < west) + window.currentFilterExtent[1][1] = west; + } + // hide feature if it is affected by initial feature suppression... + if (window.mapFilterHideInitial) { + if (typeof layer.feature.properties.hide_initial !== 'undefined' && layer.feature.properties.hide_initial) { + layer.setStyle({ + color: '#3388ff', + fill: true, + fillColor: '#3388ff', + stroke: true, + opacity: 1 + }); + } + } + // otherwise... + } else { + // hide feature + if (isSubLayer) { + window.removedLayers.push(layer); + clusterLayer.removeLayer(layer); + } else + layer.getElement().style.display = 'none'; + } +} + +/** + * @function + * @name setGeoJsonFeaturePropertiesAndActions + * + * sets properties and actions for each GeoJSON feature on each GeoJSON map layer + * + * @param {Object} feature - GeoJSON feature + * @param {Object} layer - GeoJSON map layer + */ +function setGeoJsonFeaturePropertiesAndActions(feature, layer) { + // if feature has a link to its viewing/changing page... + if (feature.properties.link) { + // open link when clicking on feature + layer.on('click', function () { + window.open(feature.properties.link, '_blank', 'noopener,noreferrer').focus(); + }); + } + // if feature is inactive... + if (typeof feature.properties.inaktiv !== 'undefined' && feature.properties.inaktiv) { + // display feature differently from standard + layer.setStyle({ + color: '#999', + fillColor: '#999' + }); + } + // if feature shall be highlighted... + if (window.highlightFlag) { + if (typeof feature.properties.highlight !== 'undefined' && feature.properties.highlight) { + // highlight feature + layer.setStyle({ + color: 'red', + fillColor: 'red' + }); + } + } + // if feature is affected by initial feature suppression... + if (window.mapFilterHideInitial) { + if (typeof feature.properties.hide_initial !== 'undefined' && feature.properties.hide_initial) + // hide feature + layer.setStyle({ + fill: false, + stroke: false, + opacity: 0 + }); + } + // if geometry is not point-like... + if (window.geometryType !== 'Point') + // set tooltip + // (otherwise it will be set further down when loading the GeoJSON feature collection) + layer.bindTooltip(feature.properties.tooltip); +} + +/** + * @function + * @name showAllMapFeatures + * + * shows all features on the map (again) + */ +function showAllMapFeatures() { + currMap.eachLayer(function(layer) { + if (layer.feature) { + layer.getElement().style.display = ''; + // if feature is affected by initial feature suppression... + if (window.mapFilterHideInitial) { + if (typeof layer.feature.properties.hide_initial !== 'undefined' && layer.feature.properties.hide_initial) { + // hide feature + layer.setStyle({ + fill: false, + stroke: false, + opacity: 0 + }); + } + } + } else if (layer.id === 'cluster') { + layer.addLayers(window.removedLayers); + layer.refreshClusters(); + } + }); + window.removedLayers = []; +} \ No newline at end of file diff --git a/datenmanagement/templates/datenmanagement/list.html b/datenmanagement/templates/datenmanagement/list.html index 301f2509..8944b2db 100644 --- a/datenmanagement/templates/datenmanagement/list.html +++ b/datenmanagement/templates/datenmanagement/list.html @@ -13,9 +13,9 @@ {% block scripts %} {{ block.super }} - + {% endblock %} {% block content %} @@ -38,8 +38,7 @@

alle Datensätze auf Karte anzeigen {% endif %} - - zurück + zurück {% if objects_count > 0 %}
@@ -117,7 +116,7 @@

let stringWithPrimaryKeys = tableData[i][0]; currentTablePrimaryKeys.push(stringWithPrimaryKeys.replace(/.*value="/, '').replace(/">/, '')); } - // create subset of currently filtered data and transfer it to table + // create subset of currently filtered data and transfer it to map subsetting( currentTablePrimaryKeys, '{% url "toolbox:subset_add" %}', @@ -148,7 +147,7 @@

- + + {% if 'Date' in map_intervalfilter_field|get_type_of_field:model_name %} + {% else %} - + {% endif %}
- +
- {% if 'Date' in map_rangefilter_field|get_type_of_field:model_name %} - + {% if 'Date' in map_intervalfilter_field|get_type_of_field:model_name %} + {% else %} - + {% endif %}
@@ -162,6 +163,7 @@

Ein-Klick-Filter
{% if map_filter_field|get_type_of_field:model_name == 'ChoiceArrayField' %} Listeneinträge + ', { @@ -626,230 +529,221 @@
Adressensuche
} }); - // bei Klick auf einen Ein-Klick-Filter-Button... + // on clicking a one-click-filter button... $('.filter-one-click-button').on('click', function(e) { let $this = $(this); - - // Button zur Übergabe der aktuellen Filtermenge an Tabellenansicht aktivieren + // enable button for passing the current filter set to the table $('#subsetter').prop('disabled', false); - - // alle übrigen Ein-Klick-Filter-Buttons deaktivieren + // disable all other one-click-filter buttons $this.parent().children('button').each(function() { if ($(this).attr('id') !== e.target.id) { $(this).prop('disabled', true); } }); - - // Button zum Anwenden der Filter deaktivieren + // disable button to apply filters $('#filter-apply').prop('disabled', true); - - // Liste für Filterobjekte definieren und befüllen + // declare and fill the filter object list let filterAttributesList = customMapFilters(e.target.id); - - // Filter anwenden + // apply filters applyFilters(filterAttributesList); }); - // bei Klick auf Button zum Anwenden der Filter... + // on clicking the button to apply the filters... $('#filter-apply').on('click', function() { - // Button zur Übergabe der aktuellen Filtermenge an Tabellenansicht aktivieren + // enable button for passing the current filter set to the table $('#subsetter').prop('disabled', false); - // alle Ein-Klick-Filter-Buttons deaktivieren + // disable all one-click-filter buttons $('#filter-one-click').children('button').each(function() { $(this).prop('disabled', true); }); - // Variable für Liste für Filterobjekte definieren + // declare a variable for the filter object list let filterAttributesList = []; - // Primärschlüssel der aktuellen Filtermenge zurücksetzen + // reset primary keys of current filter set window.currentFilterPrimaryKeys = []; - // Bounding-Box der aktuellen Filtermenge zurücksetzen + // reset bounding box of current filter set window.currentFilterExtent = []; - // Hinweise auf additive Wirkung der Filter anzeigen + // show indication of additive effect of the filters $('#filter-alert').show(); - // über alle Filter (außer Boolean-Filter und Checkboxen-Sets) gehen... + // iterate all filters (except Boolean and checkbox filters)... $('.filter-input').each(function() { if ($(this).val()) { - // Filter als Objekt mit Name, Typ, "Intervallseite" und Wert erstellen + // create filter as an object with name, type, "lefty/righty" and value let filterAttribute = {}; filterAttribute.name = $(this).attr('name'); filterAttribute.type = $(this).data('type'); filterAttribute.intervalside = $(this).data('intervalside'); filterAttribute.value = $(this).val(); - // Filterobjekt zur Liste für Filterobjekte hinzufügen + // add filter object to filter object list filterAttributesList.push(filterAttribute); } }); - // über alle Boolean-Filter gehen... + // iterate all Boolean filters... $('.filter-input input[type="checkbox"]').each(function() { - // Filter als Objekt mit Name, Typ, "Intervallseite" und Wert erstellen + // create filter as an object with name, type, "lefty/righty" and value let filterAttribute = {}; filterAttribute.name = $(this).attr('name'); filterAttribute.type = $(this).data('type'); filterAttribute.intervalside = $(this).data('intervalside'); - // Wert des Filterobjekts als Boolean setzen + // set value of filter object as Boolean if ($(this).is(':checked')) { filterAttribute.value = 'True'; } else { filterAttribute.value = 'False'; } - // Filterobjekt zur Liste für Filterobjekte hinzufügen + // add filter object to filter object list filterAttributesList.push(filterAttribute); }); - // über alle Checkboxen-Sets gehen... + // iterate all checkbox filters... $('.filter-checkbox-fieldset').each(function() { let checkedObjectsAtAll = false; - // Filter als Objekt mit Name, Typ, "Intervallseite" und Wert (als Liste) erstellen + // create filter as an object with name, type, "lefty/righty" and values (as a list) let filterAttribute = {}; let name = $(this).attr('name'); filterAttribute.name = name; filterAttribute.type = $(this).data('type'); - // Wirkung der Filter (additiv oder exklusiv) entweder aus entsprechendem Auswahlfeld auslesen - // oder direkt als exklusiv festlegen, wenn kein entsprechendes Auswahlfeld existiert + // either read the effect of the filter (additive or exclusive) + // from the corresponding selection field + // or set it directly as exclusive if no corresponding selection field exists if ($('select[id^=filtertype-' + name + ']').length) filterAttribute.filtertype = $('select[id^=filtertype-' + name + ']').children('option:selected').val(); else filterAttribute.filtertype = 'exclusive'; filterAttribute.intervalside = $(this).data('intervalside'); filterAttribute.value = []; - // Wertliste des Filterobjekts mit dem Wert jeder aktiven Checkbox befüllen + // fill the value list of the filter object with the value of each active checkbox $(this).find('input:checked').each(function() { checkedObjectsAtAll = true; filterAttribute.value.push($(this).val()); }); - // falls mindestens eine Checkbox im Checkboxen-Set aktiv ist... + // if at least one checkbox in the checkbox set is active... if (checkedObjectsAtAll) - // Filterobjekt zur Liste für Filterobjekte hinzufügen + // add filter object to filter object list filterAttributesList.push(filterAttribute); }); - // Filter anwenden + // apply filters applyFilters(filterAttributesList); }); - // bei Klick auf Button zum Zurücksetzen der Filter... + // on clicking the button to reset the filters... $('#filter-reset').on('click', function() { - // alle Ein-Klick-Filter-Buttons (wieder) aktivieren + // (re)enable all one-click filter buttons $('#filter-one-click').children('button').each(function() { $(this).prop('disabled', false); }); - // Button zum Anwenden der Filter (wieder) aktivieren + // (re)enable button to apply the filters $('#filter-apply').prop('disabled', false); - // Button zur Übergabe der aktuellen Filtermenge an Tabellenansicht deaktivieren + // enable button for passing the current filter set to the table $('#subsetter').prop('disabled', true); - // Hinweise auf additive Wirkung der Filter (wieder) verbergen + // hide indication of additive effect of the filters $('#filter-alert').hide(); - // über alle Filterfelder (außer Boolean- und Checkbox-Filterfelder) gehen... + // iterate all filter fields (except Boolean and checkbox filter fields)... $('.filter-input').each(function() { - // falls initialer Wert vorhanden... + // if initial value exists... if ($(this).data('initial')) - // Filterfeld auf initialen Wert setzen + // set filter field to initial value $(this).val($(this).data('initial')); - // ansonsten Filterfeld auf leeren Wert setzen + // otherwise set filter field to empty value else $(this).val(''); }); - // über alle Boolean-Filterfelder gehen... + // iterate all Boolean filter fields... $('.filter-input input[type="checkbox"]').each(function() { - // falls initialer Wert vorhanden... + // if initial value exists... if ($(this).data('initial')) { - // Filterfeld auf initialen Wert setzen + // set filter field to initial value let initialValue = $(this).data('initial'); - // Boolean korrekt behandeln + // treat Boolean correctly if (initialValue.toLowerCase() === 'false') { $(this).prop('checked', false); } else { $(this).prop('checked', true); } } - // ansonsten Filterfeld auf leeren Wert setzen + // otherwise set filter field to empty value else { $(this).prop('checked', false); } }); - // über alle Checkbox-Filterfelder gehen... + // iterate all checkbox filter fields... $('.filter-checkbox-fieldset').each(function() { - // alle aktivierten Checkboxen deaktivieren + // deactivate all activated checkboxes $(this).find('input:checked').each(function() { $(this).prop('checked', false); }); }); - // Wirkmodus aller Checkbox-Filter wieder auf Standard setzen, also additiv (wie „UND“) + // set the effective mode of all checkbox filters back to standard, i.e. additive (= AND) $('.filtertype').each(function() { $(this)[0].selectedIndex = 0; }); - // Primärschlüssel der aktuellen Filtermenge zurücksetzen + // reset primary keys of current filter set window.currentFilterPrimaryKeys = []; - // Bounding-Box der aktuellen Filtermenge zurücksetzen + // reset bounding box of current filter set window.currentFilterExtent = []; - // alle Layers (= Features) auf der Karte wieder anzeigen + // show all features on the map again showAllMapFeatures(); }); - // bei Klick auf Kreuzchen in einem Filterfeld... + // on clicking a button to empty a filter field... $('.input-reset').on('click', function() { - // betreffendes Filterfeld leeren + // empty respective filter field $(this).prev().is('button') ? $(this).prev().prev().val('') : $(this).prev().val(''); }); - // bei Klick auf Button zum Setzen des initialen Kartenausschnitts... + // on clicking the button to set map extent to that of currently filtered objects... + $('#map-extent-filter').on('click', function() { + if (window.currentFilterExtent.length > 0) + // set map extent to that of currently filtered objects, if filter is active + setMapExtentByBoundingBox(window.currentFilterExtent[1][1], window.currentFilterExtent[1][0], window.currentFilterExtent[0][1], window.currentFilterExtent[0][0]); + else + // set map extent to that of all objects, if filter is not active + setMapExtentByLeafletBounds(objectsExtent); + }); + + // on clicking the button to return to initial map extent... $('#map-extent-initial').on('click', function() { - // initiales Zoomlevel aus Leaflet-Konfiguration auslesen + // get initial zoom level from Leaflet configuration let defaultZoom = String('{{ LEAFLET_CONFIG }}'.match(/DEFAULT_ZOOM(?:(?!, ').)*/)); defaultZoom = defaultZoom.match(/ [0-9]+/).toString().trim(); - // initiales Kartenzentrum aus Leaflet-Konfiguration auslesen + // get initial map center from Leaflet configuration let defaultCenter = String('{{ LEAFLET_CONFIG }}'.match(/DEFAULT_CENTER(?:(?!, ').)*/)); let defaultCenterX = String(defaultCenter.match(/ [0-9]+\.[0-9]+/)).trim(); let defaultCenterY = String(defaultCenter.match(/[0-9]+\.[0-9]+/)).trim(); - // Kartenausschnitt mittels Kartenzentrum und Zoom-Level setzen + // set initial map extent by means of initial zoom level initial map center setMapExtentByXYAndZoomLevel(Number(defaultCenterX), Number(defaultCenterY), Number(defaultZoom)); }); - // bei Klick auf Button zum Setzen des Kartenausschnitts auf Bounding-Box der aktuellen Filtermenge... - $('#map-extent-filter').on('click', function() { - // falls Filter aktiv... - if (window.currentFilterExtent.length > 0) - // Kartenausschnitt auf Bounding-Box der aktuellen Filtermenge setzen - setMapExtentByBoundingBox(window.currentFilterExtent[1][1], window.currentFilterExtent[1][0], window.currentFilterExtent[0][1], window.currentFilterExtent[0][0]); - // ansonsten... - else - // Kartenausschnitt auf Bounding-Box aller Objekte setzen - setMapExtentByLeafletBounds(objectsExtent); - }); - - // bei Klick auf Button zur Übergabe der aktuellen Filtermenge an Tabellenansicht... + // on clicking a button to transfer currently filtered data to table... $('#subsetter').on('click', function() { - // Filtermenge behandeln und übergeben - {% with tmp='worschdsupp' %} - let url_mask = "{% url 'datenmanagement:'|add:model_name|add:'_list'|add:'_subset' tmp %}"; - {% endwith %} + // create subset of currently filtered data and transfer it to table subsetting( window.currentFilterPrimaryKeys, '{% url "toolbox:subset_add" %}', '{{ model_name }}', - '{{ model_pk_field }}', - url_mask, + '{{ model_pk_field_name }}', + '{{ url_model_list_subset_placeholder }}', 'Bei der Übernahme der aktuellen Filtermenge in die Tabelle ist ein Serverfehler aufgetreten.' ); }); - // Höhe der Karte dynamisch setzen anhand der Höhen der Seitenbereiche + // dynamically set the height of the map based on the heights of the page areas if ($('#map-control').hasClass('side')) { let height = $('#map-side-container').position().top; $('.side').each(function() { @@ -861,214 +755,6 @@
Adressensuche
currMap.invalidateSize(); } }); - - /** - * @function - * @name filterMapFeatures - * - * filtert Layer (= Features) auf der Karte - * - * @param {Object[]} filterAttributesList - Liste für Filterobjekte - * @param {Object} layer - Layer (= Feature) - * @param {boolean} isSubLayer - Layer (= Feature) ist Bestandteil einer Layergruppe (= eines Clusters)? - * @param {Object} [clusterLayer=layer] - Layergruppe (= Cluster), falls vorhanden; standardmäßig auf den Layer (= das Feature) gesetzt - */ - function filterMapFeatures(filterAttributesList, layer, isSubLayer, clusterLayer = layer) { - let stillVisible = true; - for (let i = 0; i < filterAttributesList.length; i++) { - // bei Stichtagsfilter einen entsprechenden Vergleich durchführen und Layer (= Feature) ggf. zur Ausblendung vormerken - if (filterAttributesList[i].name === 'deadline') { - // auf passende Formatierung achten, das es sich beim Filter um einen Datumsfilter handelt! - if (!(new Date(filterAttributesList[i].value) > new Date(layer.feature.properties['deadline_0'])) || !(new Date(layer.feature.properties['deadline_1']) > new Date(filterAttributesList[i].value))) - stillVisible = false; - // bei "linkem" Intervallfilter (also für niedrigere/kleinere/frühere Werte) einen "kleiner"-Vergleich durchführen und Layer (= Feature) ggf. zur Ausblendung vormerken - } else if (filterAttributesList[i].intervalside === 'left') { - if (filterAttributesList[i].type === 'date') { - // auf passende Formatierung achten, wenn es sich beim Filter um einen Datumsfilter handelt! - if (new Date(filterAttributesList[i].value) > new Date(layer.feature.properties[filterAttributesList[i].name])) - stillVisible = false; - } else if (filterAttributesList[i].type === 'datetime') { - // auf passende Formatierung achten, wenn es sich beim Filter um einen Zeitstempelfilter handelt! - if (new Date(filterAttributesList[i].value).valueOf() > new Date(layer.feature.properties[filterAttributesList[i].name]).valueOf()) - stillVisible = false; - } else { - if (filterAttributesList[i].value > layer.feature.properties[filterAttributesList[i].name]) - stillVisible = false; - } - // bei "rechtem" Intervallfilter (also für höhere/größere/spätere Werte) einen "größer"-Vergleich durchführen und Layer (= Feature) ggf. zur Ausblendung vormerken - } else if (filterAttributesList[i].intervalside === 'right') { - if (filterAttributesList[i].type === 'date') { - // auf passende Formatierung achten, wenn es sich beim Filter um einen Datumsfilter handelt! - if (new Date(layer.feature.properties[filterAttributesList[i].name]) > new Date(filterAttributesList[i].value)) - stillVisible = false; - } else if (filterAttributesList[i].type === 'datetime') { - // auf passende Formatierung achten, wenn es sich beim Filter um einen Zeitstempelfilter handelt! - if (new Date(layer.feature.properties[filterAttributesList[i].name]).valueOf() > new Date(filterAttributesList[i].value).valueOf()) - stillVisible = false; - } else { - if (layer.feature.properties[filterAttributesList[i].name] > filterAttributesList[i].value) - stillVisible = false; - } - // bei Checkboxen-Sets String-Vergleich mit allen aktivierten Checkboxen durchführen und Layer (= Feature) ggf. zur Ausblendung vormerken, falls einzelne Checkboxen additiv wirken sollen - } else if (filterAttributesList[i].type === 'checkbox-set' && filterAttributesList[i].value.length > 0) { - if (filterAttributesList[i].filtertype === 'additive') { - for (let j = 0; j < filterAttributesList[i].value.length; j++) { - if (layer.feature.properties[filterAttributesList[i].name].toLowerCase().indexOf(filterAttributesList[i].value[j].toLowerCase()) === -1) - stillVisible = false; - } - } else { - // mit Hilfsvariable arbeiten, um dem exklusiven Charakter (ODER) gerecht zu werden - let tempStillVisible = false; - for (let k = 0; k < filterAttributesList[i].value.length; k++) { - if (layer.feature.properties[filterAttributesList[i].name].toLowerCase().indexOf(filterAttributesList[i].value[k].toLowerCase()) !== -1) { - tempStillVisible = true; - break; - } - } - if (!tempStillVisible) - stillVisible = false; - } - // ansonsten einen Vergleich durchführen und Layer (= Feature) ggf. zur Ausblendung vormerken - } else if (filterAttributesList[i].type !== 'checkbox-set') { - if (filterAttributesList[i].type === 'date' || filterAttributesList[i].type === 'datetime') { - // negative oder positive Wirkungslogik? - if (filterAttributesList[i].logic === 'negative') { - // auf passende Formatierung achten, wenn es sich beim Filter um einen Datums- oder Zeitstempelfilter handelt! - if (new Date(filterAttributesList[i].value).valueOf() === new Date(layer.feature.properties[filterAttributesList[i].name]).valueOf()) - stillVisible = false; - } else { - // auf passende Formatierung achten, wenn es sich beim Filter um einen Datums- oder Zeitstempelfilter handelt! - if (new Date(filterAttributesList[i].value).valueOf() !== new Date(layer.feature.properties[filterAttributesList[i].name]).valueOf()) - stillVisible = false; - } - } else if (filterAttributesList[i].type === 'list') { - // negative oder positive Wirkungslogik? - if (filterAttributesList[i].logic === 'negative') { - if (layer.feature.properties[filterAttributesList[i].name].toLowerCase() === filterAttributesList[i].value.toLowerCase()) - stillVisible = false; - } else { - if (layer.feature.properties[filterAttributesList[i].name].toLowerCase() !== filterAttributesList[i].value.toLowerCase()) - stillVisible = false; - } - } else { - let value = filterAttributesList[i].value.toLowerCase(); - // Dezimaltrenner behandeln, falls eine Dezimalzahl eingegeben wurde - if (/^([0-9]+,[0-9]+)$/.test(filterAttributesList[i].value) === true) - value = filterAttributesList[i].value.replace(',', '.'); - // negative oder positive Wirkungslogik? - if (filterAttributesList[i].logic === 'negative') { - if (layer.feature.properties[filterAttributesList[i].name].toLowerCase().indexOf(value) !== -1) - stillVisible = false; - } else { - if (layer.feature.properties[filterAttributesList[i].name].toLowerCase().indexOf(value) === -1) - stillVisible = false; - } - } - } - } - // falls Layer (= Feature) nicht zur Ausblendung vorgemerkt ist, also weiterhin sichtbar sein soll: - if (stillVisible) { - // Variable für Primärschlüssel der aktuellen Filtermenge aktualisieren - window.currentFilterPrimaryKeys.push(layer.feature.properties['{{ model_pk_field }}']); - // Variable für Bounding-Box der aktuellen Filtermenge aktualisieren - let north = ((layer.feature.geometry.type === 'Point') ? layer.getLatLng().lat : layer.getBounds().getNorth()); - let east = ((layer.feature.geometry.type === 'Point') ? layer.getLatLng().lng : layer.getBounds().getEast()); - let south = ((layer.feature.geometry.type === 'Point') ? layer.getLatLng().lat : layer.getBounds().getSouth()); - let west = ((layer.feature.geometry.type === 'Point') ? layer.getLatLng().lng : layer.getBounds().getWest()); - if (window.currentFilterExtent.length === 0) { - window.currentFilterExtent[0] = []; - window.currentFilterExtent[0][0] = north; - window.currentFilterExtent[0][1] = east; - window.currentFilterExtent[1] = []; - window.currentFilterExtent[1][0] = south; - window.currentFilterExtent[1][1] = west; - } else { - if (window.currentFilterExtent[0][0] > north) - window.currentFilterExtent[0][0] = north; - if (window.currentFilterExtent[0][1] > east) - window.currentFilterExtent[0][1] = east; - if (window.currentFilterExtent[1][0] < south) - window.currentFilterExtent[1][0] = south; - if (window.currentFilterExtent[1][1] < west) - window.currentFilterExtent[1][1] = west; - } - // Layer (= Feature) einblenden, falls es von initialer Feature-Ausblendung betroffen ist - if (window.mapFilterHideInitial) { - if (typeof layer.feature.properties.hide_initial !== 'undefined' && layer.feature.properties.hide_initial) { - layer.setStyle({ - color: '#3388ff', - fill: true, - fillColor: '#3388ff', - stroke: true, - opacity: 1 - }); - } - } - // ansonsten: - } else { - // Layer (= Feature) tatsächlich ausblenden - if (isSubLayer) { - window.removedLayers.push(layer); - clusterLayer.removeLayer(layer); - } else - layer.getElement().style.display = 'none'; - } - } - - /** - * @function - * @name showAllMapFeatures - * - * zeigt alle Layers (= Features) auf der Karte (wieder) an - */ - function showAllMapFeatures() { - currMap.eachLayer(function(layer) { - if (layer.feature) { - layer.getElement().style.display = ''; - // Layer (= Feature) ausblenden, falls es von initialer Feature-Ausblendung betroffen ist - if (window.mapFilterHideInitial) { - if (typeof layer.feature.properties.hide_initial !== 'undefined' && layer.feature.properties.hide_initial) { - layer.setStyle({ - fill: false, - stroke: false, - opacity: 0 - }); - } - } - } else if (layer.id === 'cluster') { - layer.addLayers(window.removedLayers); - layer.refreshClusters(); - } - }); - window.removedLayers = []; - } - - /** - * @function - * @name applyFilters - * - * wendet Filter an - * - * @param {Object[]} filterAttributesList - Liste für Filterobjekte - */ - function applyFilters(filterAttributesList) { - if (filterAttributesList.length > 0) { - showAllMapFeatures(); - window.currMap.eachLayer(function(layer) { - if (layer.feature) { - filterMapFeatures(filterAttributesList, layer, false); - } else if (layer.id === 'cluster') { - let clusterLayer = layer; - layer.eachLayer(function(subLayer) { - filterMapFeatures(filterAttributesList, subLayer, true, clusterLayer); - }); - layer.refreshClusters(); - } - }); - } else { - showAllMapFeatures(); - } - } {% endif %} {% endblock %} diff --git a/datenmanagement/views/functions.py b/datenmanagement/views/functions.py index 88118902..46ffd665 100644 --- a/datenmanagement/views/functions.py +++ b/datenmanagement/views/functions.py @@ -15,14 +15,31 @@ from .fields import ArrayDateField +def add_basic_model_context_elements(context, model): + """ + adds basic model related elements to the passed context and returns the context + + :param context: context + :param model: model + :return: context with basic model related elements added + """ + context['model_name'] = model.__name__ + context['model_verbose_name_plural'] = model._meta.verbose_name_plural + context['model_description'] = model.BasemodelMeta.description + context['model_is_editable'] = model.BasemodelMeta.editable + context['model_geometry_type'] = model.BasemodelMeta.geometry_type + context['model_pk_field_name'] = model._meta.pk.name + return context + + def add_model_context_elements(context, model, kwargs=None): """ - adds general model related elements to the passed context and returns the context + adds model related elements to the passed context and returns the context :param context: context :param model: model :param kwargs: kwargs of the view calling this function - :return: context with general model related elements added + :return: context with model related elements added """ model_name = model.__name__ context['model_name'] = model_name diff --git a/datenmanagement/views/views_general.py b/datenmanagement/views/views_general.py index 5276eb3a..a1afcf3d 100644 --- a/datenmanagement/views/views_general.py +++ b/datenmanagement/views/views_general.py @@ -13,7 +13,7 @@ from datenmanagement.models.base import Codelist, ComplexModel, Metamodel from datenmanagement.utils import user_has_model_permissions_any, \ user_has_model_permissions_change_delete_view -from .functions import get_uuids_geometries_from_sql +from .functions import add_basic_model_context_elements, get_uuids_geometries_from_sql class GeometryView(JsonView): @@ -207,8 +207,8 @@ def get_context_data(self, **kwargs): model_name = self.model.__name__ model_name_lower = model_name.lower() context = super().get_context_data(**kwargs) - context['model_verbose_name_plural'] = self.model._meta.verbose_name_plural - context['model_description'] = self.model.BasemodelMeta.description + # add basic model related elements to context + context = add_basic_model_context_elements(context, self.model) if ( self.model.BasemodelMeta.editable and self.request.user.has_perm('datenmanagement.add_' + model_name_lower) diff --git a/datenmanagement/views/views_list_map.py b/datenmanagement/views/views_list_map.py index 1dfe04ed..1fdf13ef 100644 --- a/datenmanagement/views/views_list_map.py +++ b/datenmanagement/views/views_list_map.py @@ -2,20 +2,20 @@ from decimal import Decimal from django.conf import settings from django.core.serializers import serialize -from django.db.models import Q from django.urls import reverse from django.utils.html import escape from django.views.generic.base import TemplateView from django_datatables_view.base_datatable_view import BaseDatatableView from jsonview.views import JsonView from json import dumps, loads -from re import IGNORECASE, match, search, sub +from re import IGNORECASE, match, sub from time import time from zoneinfo import ZoneInfo from datenmanagement.utils import get_data, get_thumb_url, localize_number -from .functions import add_user_agent_context_elements, get_model_objects, \ - add_model_context_elements +from toolbox.utils import optimize_datatable_filter +from .functions import add_basic_model_context_elements, add_user_agent_context_elements, \ + get_model_objects class TableDataCompositionView(BaseDatatableView): @@ -192,35 +192,7 @@ def filter_queryset(self, qs): column_with_foreign_key = self.columns_with_foreign_key.get(column) if column_with_foreign_key is not None: column = column + str('__') + column_with_foreign_key - case_a = search('^[0-9]{2}\\.[0-9]{2}\\.[0-9]{4}$', search_element) - case_b = search('^[0-9]{2}\\.[0-9]{4}$', search_element) - case_c = search('^[0-9]{2}\\.[0-9]{2}$', search_element) - if case_a or case_b or case_c: - search_element_splitted = search_element.split('.') - kwargs = { - '{0}__{1}'.format(column, 'icontains'): (search_element_splitted[ - 2] + '-' if case_a else '') + - search_element_splitted[1] + '-' + - search_element_splitted[0] - } - elif search_element == 'ja': - kwargs = { - '{0}__{1}'.format(column, 'icontains'): 'true' - } - elif search_element == 'nein' or search_element == 'nei': - kwargs = { - '{0}__{1}'.format(column, 'icontains'): 'false' - } - elif match(r"^[0-9]+,[0-9]+$", search_element): - kwargs = { - '{0}__{1}'.format(column, 'icontains'): sub(',', '.', search_element) - } - else: - kwargs = { - '{0}__{1}'.format(column, 'icontains'): search_element - } - q = Q(**kwargs) - qs_params_inner = qs_params_inner | q if qs_params_inner else q + qs_params_inner = optimize_datatable_filter(search_element, column, qs_params_inner) qs_params = qs_params & qs_params_inner if qs_params else qs_params_inner qs = qs.filter(qs_params) return qs @@ -265,12 +237,9 @@ def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) # add user agent related elements to context context = add_user_agent_context_elements(context, self.request) + # add basic model related elements to context + context = add_basic_model_context_elements(context, self.model) # add further elements to context - context['model_name'] = model_name - context['model_verbose_name_plural'] = self.model._meta.verbose_name_plural - context['model_description'] = self.model.BasemodelMeta.description - context['model_pk_field_name'] = self.model._meta.pk.name - context['model_is_editable'] = self.model.BasemodelMeta.editable if ( self.model.BasemodelMeta.editable and self.request.user.has_perm('datenmanagement.delete_' + model_name_lower) @@ -408,7 +377,7 @@ def get_context_data(self, **kwargs): ) # optional: mark object as inactive if hasattr(curr_object, 'aktiv') and curr_object.aktiv is False: - feature['properties']['inaktiv'] = True + feature['properties']['inaktiv'] = 'true' # optional: mark object as to hide it initially if self.model.BasemodelMeta.map_filter_hide_initial: if str( @@ -417,7 +386,7 @@ def get_context_data(self, **kwargs): self.model.BasemodelMeta.map_filter_hide_initial.keys())[0])) == str( list( self.model.BasemodelMeta.map_filter_hide_initial.values())[0]): - feature['properties']['hide_initial'] = True + feature['properties']['hide_initial'] = 'true' # optional: mark object as to highlight it if self.model.BasemodelMeta.highlight_flag: data = getattr(curr_object, self.model.BasemodelMeta.highlight_flag) @@ -434,9 +403,9 @@ def get_context_data(self, **kwargs): feature['properties']['deadline_' + str(index)] = str(data) # additionally set field as an ordinary map filter property, too feature['properties'][field] = str(data) - # optional: set deadline interval/range map filter as properties - if self.model.BasemodelMeta.map_rangefilter_fields: - for field in self.model.BasemodelMeta.map_rangefilter_fields.keys(): + # optional: set deadline interval map filter as properties + if self.model.BasemodelMeta.map_intervalfilter_fields: + for field in self.model.BasemodelMeta.map_intervalfilter_fields.keys(): feature['properties'][field] = str(get_data(curr_object, field)) # optional: set all other map filters as properties if self.model.BasemodelMeta.map_filter_fields: @@ -463,68 +432,67 @@ def get_context_data(self, **kwargs): :param kwargs: :return: dictionary with all context elements for this view """ - # Variablen für Filterfelder vorbereiten, die als Intervallfelder fungieren sollen, - # und zwar eine Variable mit dem Minimal- und eine Variable mit dem Maximalwert + # declare variables for filter fields that are to act as interval map filters, + # one variable with the minimum value and one variable with the maximum value interval_filter_min = None interval_filter_max = None - if self.model.BasemodelMeta.map_rangefilter_fields: - # Feld für Minimalwerte definieren - field_name = list(self.model.BasemodelMeta.map_rangefilter_fields.keys())[0] - # NOT-NULL-Filter konstruieren + if self.model.BasemodelMeta.map_intervalfilter_fields: + # define field for minimum values + field_name = list(self.model.BasemodelMeta.map_intervalfilter_fields.keys())[0] + # construct NOT NULL filter field_name_isnull = field_name + '__isnull' - # Minimalwert erhalten und in vorbereitete Variable einfügen + # get minimum value and insert into declared variable interval_filter_min = self.model.objects.exclude(**{field_name_isnull: True}).order_by( field_name).values_list(field_name, flat=True).first() if isinstance(interval_filter_min, date): interval_filter_min = interval_filter_min.strftime('%Y-%m-%d') elif isinstance(interval_filter_min, datetime): interval_filter_min = interval_filter_min.strftime('%Y-%m-%d %H:%M:%S') - # Feld für Maximalwerte definieren - field_name = list(self.model.BasemodelMeta.map_rangefilter_fields.keys())[1] - # NOT-NULL-Filter konstruieren + # define field for maximum values + field_name = list(self.model.BasemodelMeta.map_intervalfilter_fields.keys())[1] + # construct NOT NULL filter field_name_isnull = field_name + '__isnull' - # Maximalwert erhalten und in vorbereitete Variable einfügen + # get maximum value and insert into declared variable interval_filter_max = self.model.objects.exclude(**{field_name_isnull: True}).order_by( field_name).values_list(field_name, flat=True).last() if isinstance(interval_filter_max, date): interval_filter_max = interval_filter_max.strftime('%Y-%m-%d') elif isinstance(interval_filter_max, datetime): interval_filter_max = interval_filter_max.strftime('%Y-%m-%d %H:%M:%S') - # Dictionary für Filterfelder vorbereiten, die als Auswahlfeld fungieren sollen + # declare dictionary for filter fields that are to act as selections list_filter_lists = {} if self.model.BasemodelMeta.map_filter_fields_as_list: - # alle entsprechend definierten Felder durchgehen + # go through all appropriately defined fields... for field_name in self.model.BasemodelMeta.map_filter_fields_as_list: - # passendes Zielmodell identifizieren + # identify suitable target model target_model = self.model._meta.get_field(field_name).remote_field.model - # passendes Feld im Zielmodell für die Sortierung identifizieren + # identify the suitable field in the target model for sorting foreign_field_name_ordering = target_model._meta.ordering[0] - # passendes Feld im Zielmodell für die Anzeige identifizieren + # identify the suitable field in the target model for display if target_model.BasemodelMeta.naming: foreign_field_name_naming = target_model.BasemodelMeta.naming else: foreign_field_name_naming = foreign_field_name_ordering - # NOT-NULL-Filter konstruieren + # construct NOT NULL filter field_name_isnull = field_name + '__isnull' - # sortierte Liste aller eindeutigen Werte des passenden Feldes aus Zielmodell erhalten + # sorted list of all unique values of the matching field obtained from the target model value_list = list( self.model.objects.exclude(**{field_name_isnull: True}).order_by( field_name + '__' + foreign_field_name_ordering).values_list( field_name + '__' + foreign_field_name_naming, flat=True).distinct()) - # Dezimalzahlen in Liste in Strings umwandeln, - # da Dezimalzahlen nicht als JSON serialisiert werden können + # convert decimal numbers in list to strings, + # since decimal numbers cannot be serialized as JSON cleaned_value_list = [] for value in value_list: cleaned_value_list.append(str(value) if isinstance(value, Decimal) else value) - # Liste in vorbereitetes Dictionary einfügen + # insert list into declared dictionary list_filter_lists[field_name] = cleaned_value_list - # Dictionary für Filterfelder vorbereiten, die als Checkboxen-Set fungieren sollen + # declare dictionary for filter fields that are to act as checkbox sets checkbox_filter_lists = {} if self.model.BasemodelMeta.map_filter_fields: - # alle entsprechend definierten Felder durchgehen + # go through all appropriately defined fields... for field_name in self.model.BasemodelMeta.map_filter_fields: - # falls es sich um ein ChoiceArrayField handelt - # oder das Feld explizit als Checkboxen-Set fungieren soll... + # if it is a ChoiceArrayField or the field shall explicitly function as a checkbox set... if ( self.model._meta.get_field(field_name).__class__.__name__ == 'ChoiceArrayField' or ( @@ -533,85 +501,106 @@ def get_context_data(self, **kwargs): ) ): complete_field_name_ordering = complete_field_name_naming = field_name - # falls es sich um ein Fremdschlüsselfeld handelt... + # if it is a foreign key field... if ( hasattr(self.model._meta.get_field(field_name), 'remote_field') and self.model._meta.get_field(field_name).remote_field is not None ): - # passendes Zielmodell identifizieren + # identify suitable target model target_model = self.model._meta.get_field(field_name).remote_field.model - # passendes Feld im Zielmodell für die Sortierung identifizieren + # identify the suitable field in the target model for sorting foreign_field_name_ordering = target_model._meta.ordering[0] - # passendes Feld im Zielmodell für die Anzeige identifizieren + # identify the suitable field in the target model for display if target_model.BasemodelMeta.naming: foreign_field_name_naming = target_model.BasemodelMeta.naming else: foreign_field_name_naming = foreign_field_name_ordering complete_field_name_ordering = field_name + '__' + foreign_field_name_ordering complete_field_name_naming = field_name + '__' + foreign_field_name_naming - # NOT-NULL-Filter konstruieren + # construct NOT NULL filter field_name_isnull = field_name + '__isnull' - # sortierte Liste aller eindeutigen Werte des Feldes erhalten + # sorted list of all unique values of the matching field obtained from the target model values_list = list( self.model.objects.exclude(**{field_name_isnull: True}).order_by( complete_field_name_ordering).values_list( complete_field_name_naming, flat=True).distinct()) - # falls es sich NICHT um ein Fremdschlüsselfeld handelt... + # if it is NOT a foreign key field... if field_name == complete_field_name_ordering: - # Werte vereinzeln und sortierte Liste aller eindeutigen Einzelwerte erhalten + # separate values and get a sorted list of all unique individual values value_list = list([item for sublist in values_list for item in sublist]) distinct_value_list = [] for value_list_item in value_list: if value_list_item not in distinct_value_list: distinct_value_list.append(value_list_item) - # Dezimalzahlen in Liste in Strings umwandeln, - # da Dezimalzahlen nicht als JSON serialisiert werden können + # convert decimal numbers in list to strings, + # since decimal numbers cannot be serialized as JSON cleaned_distinct_value_list = [] for distinct_value in distinct_value_list: cleaned_distinct_value_list.append(str(distinct_value) if isinstance(distinct_value, Decimal) else distinct_value) - # Liste in vorbereitetes Dictionary einfügen + # insert list into declared dictionary checkbox_filter_lists[field_name] = cleaned_distinct_value_list - # ansonsten... + # otherwise... else: - # sortierte Liste aller eindeutigen Einzelwerte - # direkt in vorbereitetes Dictionary einfügen + # insert a sorted list of all unique individual values + # directly into the declared dictionary checkbox_filter_lists[field_name] = values_list + model_name = self.model.__name__ + model_name_lower = model_name.lower() context = super().get_context_data(**kwargs) # add user agent related elements to context context = add_user_agent_context_elements(context, self.request) - context = add_model_context_elements(context, self.model, self.kwargs) + # add basic model related elements to context + context = add_basic_model_context_elements(context, self.model) + # add further elements to context context['LEAFLET_CONFIG'] = settings.LEAFLET_CONFIG - if self.kwargs and self.kwargs['subset_id']: - context['subset_id'] = int(self.kwargs['subset_id']) + if self.kwargs and 'subset_id' in self.kwargs and self.kwargs['subset_id']: + subset_id = int(kwargs['subset_id']) + context['subset_id'] = subset_id + context['objects_count'] = get_model_objects(self.model, subset_id, True) + context['url_model_mapdata_subset'] = reverse( + 'datenmanagement:' + model_name + '_mapdata_subset', args=[subset_id]) + else: + context['objects_count'] = get_model_objects(self.model, None, True) context['highlight_flag'] = self.model.BasemodelMeta.highlight_flag + context['heavy_load_limit'] = self.model.BasemodelMeta.heavy_load_limit + context['additional_wms_layers'] = self.model.BasemodelMeta.additional_wms_layers + if ( + self.model.BasemodelMeta.editable + and self.request.user.has_perm('datenmanagement.add_' + model_name_lower) + ): + context['url_model_add'] = reverse('datenmanagement:' + model_name + '_add') + context['url_model_list'] = reverse('datenmanagement:' + model_name + '_list') + context['url_model_list_subset_placeholder'] = reverse( + 'datenmanagement:' + model_name + '_list_subset', args=['worschdsupp']) + context['url_model_mapdata'] = reverse('datenmanagement:' + model_name + '_mapdata') + context['url_back'] = reverse('datenmanagement:' + model_name + '_start') + # add map filter related elements to context if ( self.model.BasemodelMeta.map_filter_fields - or self.model.BasemodelMeta.map_rangefilter_fields + or self.model.BasemodelMeta.map_intervalfilter_fields ): context['map_filters_enabled'] = True context['map_one_click_filters'] = self.model.BasemodelMeta.map_one_click_filters - if self.model.BasemodelMeta.map_rangefilter_fields: - context['map_rangefilter_fields'] = list( - self.model.BasemodelMeta.map_rangefilter_fields.keys()) - context['map_rangefilter_fields_labels'] = list( - self.model.BasemodelMeta.map_rangefilter_fields.values()) + context['map_deadlinefilter_fields'] = self.model.BasemodelMeta.map_deadlinefilter_fields + if self.model.BasemodelMeta.map_intervalfilter_fields: + context['map_intervalfilter_fields'] = list( + self.model.BasemodelMeta.map_intervalfilter_fields.keys()) + context['map_intervalfilter_fields_labels'] = list( + self.model.BasemodelMeta.map_intervalfilter_fields.values()) context['interval_filter_min'] = interval_filter_min context['interval_filter_max'] = interval_filter_max - context['map_deadlinefilter_fields'] = self.model.BasemodelMeta.map_deadlinefilter_fields if self.model.BasemodelMeta.map_filter_fields: context['map_filter_fields'] = list(self.model.BasemodelMeta.map_filter_fields.keys()) context['map_filter_fields_labels'] = list( self.model.BasemodelMeta.map_filter_fields.values()) context['map_filter_fields_as_checkbox'] = ( self.model.BasemodelMeta.map_filter_fields_as_checkbox) - context['checkbox_filter_lists'] = dumps(checkbox_filter_lists) context['map_filter_fields_as_list'] = self.model.BasemodelMeta.map_filter_fields_as_list context['list_filter_lists'] = dumps(list_filter_lists) + context['checkbox_filter_lists'] = dumps(checkbox_filter_lists) context['map_filter_boolean_fields_as_checkbox'] = ( self.model.BasemodelMeta.map_filter_boolean_fields_as_checkbox) context['map_filter_hide_initial'] = self.model.BasemodelMeta.map_filter_hide_initial - context['additional_wms_layers'] = self.model.BasemodelMeta.additional_wms_layers - context['heavy_load_limit'] = self.model.BasemodelMeta.heavy_load_limit return context diff --git a/toolbox/utils.py b/toolbox/utils.py new file mode 100644 index 00000000..926c08d1 --- /dev/null +++ b/toolbox/utils.py @@ -0,0 +1,42 @@ +from django.db.models import Q +from re import match, search, sub + + +def optimize_datatable_filter(search_element, search_column, qs_params_inner): + """ + optimizes datatables queryset filter based on passed parameters + + :param search_element: search element + :param search_column: search column + :param qs_params_inner: queryset parameters + :return: optimized datatables queryset filter based on passed parameters + """ + case_a = search('^[0-9]{2}\\.[0-9]{2}\\.[0-9]{4}$', search_element) + case_b = search('^[0-9]{2}\\.[0-9]{4}$', search_element) + case_c = search('^[0-9]{2}\\.[0-9]{2}$', search_element) + if case_a or case_b or case_c: + search_element_splitted = search_element.split('.') + kwargs = { + '{0}__{1}'.format(search_column, 'icontains'): (search_element_splitted[ + 2] + '-' if case_a else '') + + search_element_splitted[1] + '-' + + search_element_splitted[0] + } + elif search_element == 'ja': + kwargs = { + '{0}__{1}'.format(search_column, 'icontains'): 'true' + } + elif search_element == 'nein' or search_element == 'nei': + kwargs = { + '{0}__{1}'.format(search_column, 'icontains'): 'false' + } + elif match(r"^[0-9]+,[0-9]+$", search_element): + kwargs = { + '{0}__{1}'.format(search_column, 'icontains'): sub(',', '.', search_element) + } + else: + kwargs = { + '{0}__{1}'.format(search_column, 'icontains'): search_element + } + q = Q(**kwargs) + return qs_params_inner | q if qs_params_inner else q