Merge branch 'master' into 138_New_map_client

# Conflicts:
#	konova/models/geometry.py
#	konova/urls.py
#	locale/de/LC_MESSAGES/django.mo
#	locale/de/LC_MESSAGES/django.po
This commit is contained in:
2022-05-25 09:22:15 +02:00
32 changed files with 483 additions and 198 deletions

View File

@@ -118,32 +118,38 @@ class Geometry(BaseResource):
_now = timezone.now()
underlying_parcels = []
for result in fetched_parcels:
fetched_parcel = result[typename]
parcel_properties = result["properties"]
# There could be parcels which include the word 'Flur',
# which needs to be deleted and just keep the numerical values
## THIS CAN BE REMOVED IN THE FUTURE, WHEN 'Flur' WON'T OCCUR ANYMORE!
flr_val = fetched_parcel["ave:flur"].replace("Flur ", "")
flr_val = parcel_properties["flur"].replace("Flur ", "")
district = District.objects.get_or_create(
key=fetched_parcel["ave:kreisschl"],
name=fetched_parcel["ave:kreis"],
key=parcel_properties["kreisschl"],
name=parcel_properties["kreis"],
)[0]
municipal = Municipal.objects.get_or_create(
key=fetched_parcel["ave:gmdschl"],
name=fetched_parcel["ave:gemeinde"],
key=parcel_properties["gmdschl"],
name=parcel_properties["gemeinde"],
district=district,
)[0]
parcel_group = ParcelGroup.objects.get_or_create(
key=fetched_parcel["ave:gemaschl"],
name=fetched_parcel["ave:gemarkung"],
key=parcel_properties["gemaschl"],
name=parcel_properties["gemarkung"],
municipal=municipal,
)[0]
flrstck_nnr = parcel_properties['flstnrnen']
if not flrstck_nnr:
flrstck_nnr = None
flrstck_zhlr = parcel_properties['flstnrzae']
if not flrstck_zhlr:
flrstck_zhlr = None
parcel_obj = Parcel.objects.get_or_create(
district=district,
municipal=municipal,
parcel_group=parcel_group,
flr=flr_val,
flrstck_nnr=fetched_parcel['ave:flstnrnen'],
flrstck_zhlr=fetched_parcel['ave:flstnrzae'],
flrstck_nnr=flrstck_nnr,
flrstck_zhlr=flrstck_zhlr,
)[0]
parcel_obj.district = district
parcel_obj.updated_on = _now
@@ -151,6 +157,7 @@ class Geometry(BaseResource):
underlying_parcels.append(parcel_obj)
# Update the linked parcels
self.parcels.clear()
self.parcels.set(underlying_parcels)
# Set the calculated_on intermediate field, so this related data will be found on lookups
@@ -171,7 +178,6 @@ class Geometry(BaseResource):
Returns:
parcels (QuerySet): The related parcels as queryset
"""
parcels = self.parcels.filter(
parcelintersection__calculated_on__isnull=False,
).prefetch_related(
@@ -183,6 +189,17 @@ class Geometry(BaseResource):
return parcels
def count_underlying_parcels(self):
""" Getter for number of underlying parcels
Returns:
"""
num_parcels = self.parcels.filter(
parcelintersection__calculated_on__isnull=False,
).count()
return num_parcels
def as_feature_collection(self, srid=DEFAULT_SRID_RLP):
""" Returns a FeatureCollection structure holding all polygons of the MultiPolygon as single features
@@ -211,7 +228,6 @@ class Geometry(BaseResource):
return geojson
class GeometryConflict(UuidModel):
"""
Geometry conflicts model

View File

@@ -652,10 +652,21 @@ class GeoReferencedMixin(models.Model):
Returns:
parcels (Iterable): An empty list or a Queryset
"""
result = []
if self.geometry is not None:
return self.geometry.get_underlying_parcels()
else:
return []
result = self.geometry.get_underlying_parcels()
return result
def count_underlying_parcels(self):
""" Getter for number of underlying parcels
Returns:
"""
result = 0
if self.geometry is not None:
result = self.geometry.count_underlying_parcels()
return result
def set_geometry_conflict_message(self, request: HttpRequest):
if self.geometry is None:

View File

@@ -262,4 +262,13 @@ Similar to bootstraps 'shadow-lg'
padding-left: 2em;
}
*/
*/
.collapse-icn > i{
transition: all 0.3s ease;
}
.collapsed .collapse-icn > i{
transform: rotate(-90deg);
}
.tree-label.badge{
font-size: 90%;
}

View File

@@ -0,0 +1,22 @@
{% load l10n i18n %}
{% for parcel in parcels %}
{% if forloop.last and next_page %}
<tr hx-get="{% url 'geometry-parcels-content' geom_id next_page %}"
hx-trigger="intersect once"
hx-swap="afterend">
<td>{{parcel.parcel_group.name|default_if_none:"-"}}</td>
<td>{{parcel.parcel_group.key|default_if_none:"-"}}</td>
<td>{{parcel.flr|default_if_none:"-"|unlocalize}}</td>
<td>{{parcel.flrstck_zhlr|default_if_none:"-"|unlocalize}}</td>
<td>{{parcel.flrstck_nnr|default_if_none:"-"|unlocalize}}</td>
</tr>
{% else %}
<tr>
<td>{{parcel.parcel_group.name|default_if_none:"-"}}</td>
<td>{{parcel.parcel_group.key|default_if_none:"-"}}</td>
<td>{{parcel.flr|default_if_none:"-"|unlocalize}}</td>
<td>{{parcel.flrstck_zhlr|default_if_none:"-"|unlocalize}}</td>
<td>{{parcel.flrstck_nnr|default_if_none:"-"|unlocalize}}</td>
</tr>
{% endif %}
{% endfor %}

View File

@@ -5,6 +5,11 @@
{% trans 'Parcels can not be calculated, since no geometry is given.' %}
</article>
{% else %}
<div>
<h4 class="">
<span class="badge rlp-r">{{num_parcels}}</span>
{% trans 'Parcels found' %}</h4>
</div>
<table id="upper-spatial-table" class="table table-hover">
<thead>
<tr>
@@ -37,16 +42,7 @@
</tr>
</thead>
<tbody>
{% for parcel in parcels %}
<tr>
<td>{{parcel.parcel_group.name|default_if_none:"-"}}</td>
<td>{{parcel.parcel_group.key|default_if_none:"-"}}</td>
<td>{{parcel.flr|default_if_none:"-"|unlocalize}}</td>
<td>{{parcel.flrstck_zhlr|default_if_none:"-"|unlocalize}}</td>
<td>{{parcel.flrstck_nnr|default_if_none:"-"|unlocalize}}</td>
</tr>
{% endfor %}
{% include 'konova/includes/parcels/parcel_table_content.html' %}
</tbody>
</table>
{% endif %}

View File

@@ -2,18 +2,23 @@
{% for code in codes %}
<div class="ml-4 tree-element">
<label class="tree-label" role="{% if not code.is_leaf%}button{% endif %}" for="input_{{code.pk|unlocalize}}" id="{{code.pk|unlocalize}}" data-toggle="collapse" data-target="#children_{{code.pk|unlocalize}}" aria-expanded="true" aria-controls="children_{{code.pk|unlocalize}}">
<label class="tree-label collapsed" role="{% if not code.is_leaf%}button{% endif %}" for="input_{{code.pk|unlocalize}}" id="{{code.pk|unlocalize}}" data-toggle="collapse" data-target="#children_{{code.pk|unlocalize}}" aria-expanded="true" aria-controls="children_{{code.pk|unlocalize}}">
{% if code.is_leaf%}
<input class="tree-input" id="input_{{code.pk|unlocalize}}" name="{{ widget.name }}" type="checkbox" value="{{code.pk|unlocalize}}" {% if code.pk|unlocalize in widget.value %}checked{% endif %}/>
{% else %}
{% fa5_icon 'angle-right' %}
<span class="collapse-icn">
{% fa5_icon 'angle-down' %}
</span>
{% endif %}
{% if code.short_name %}
({{code.short_name}})
{% endif %}
{{code.long_name}}
</label>
{% if not code.is_leaf %}
<div id="children_{{code.pk|unlocalize}}" data-toggle="collapse" class="collapse tree-element-children">
{% with code.children as codes %}
{% include 'konova/widgets/checkbox-tree-select-content.html' %}
{% include 'konova/widgets/tree/checkbox/checkbox-tree-select-content.html' %}
{% endwith %}
</div>
{% endif %}

View File

@@ -5,7 +5,7 @@
</div>
<div id="tree-root">
{% include 'konova/widgets/checkbox-tree-select-content.html' %}
{% include 'konova/widgets/tree/checkbox/checkbox-tree-select-content.html' %}
</div>
<script>
@@ -47,9 +47,12 @@
}
);
if(val.length > 0){
// Hide everything
allTreeElements.hide()
// Now show again everything matching the query
allTreeElementsContain.show()
}else{
// Show everything if no query exists
allTreeElements.show()
}
}

View File

@@ -0,0 +1,25 @@
{% load l10n fontawesome_5 %}
{% for code in codes %}
<div class="ml-4 tree-element">
<label class="tree-label collapsed" role="{% if not code.is_leaf%}button{% endif %}" for="input_{{code.pk|unlocalize}}" id="{{code.pk|unlocalize}}" data-toggle="collapse" data-target="#children_{{code.pk|unlocalize}}" aria-expanded="true" aria-controls="children_{{code.pk|unlocalize}}">
{% if code.is_leaf%}
<input class="tree-input" id="input_{{code.pk|unlocalize}}" name="{{ widget.name }}" type="radio" value="{{code.pk|unlocalize}}" {% if code.pk|unlocalize in widget.value %}checked{% endif %}/>
{% else %}
<span class="collapse-icn">
{% fa5_icon 'angle-down' %}
</span>
{% endif %}
{% if code.short_name %}
({{code.short_name}})
{% endif %}
{{code.long_name}}
</label>
{% if not code.is_leaf %}
<div id="children_{{code.pk|unlocalize}}" data-toggle="collapse" class="collapse tree-element-children">
{% with code.children as codes %}
{% include 'konova/widgets/tree/radio/radio-tree-select-content.html' %}
{% endwith %}
</div>
{% endif %}
</div>
{% endfor %}

View File

@@ -0,0 +1,62 @@
{% load i18n %}
<div class="ml-4 mb-4">
<input id="tree-search-input" class="form-control" type="text" placeholder="{% trans 'Search' %}"/>
</div>
<div id="tree-root">
{% include 'konova/widgets/tree/radio/radio-tree-select-content.html' %}
</div>
<script>
function toggleSelectedCssClass(element){
element = $(element);
var cssClass = "badge rlp-r"
// Find all already tagged input elements and reset them to be untagged
var allTaggedInputs = $("#tree-root").find(".badge.rlp-r")
allTaggedInputs.removeClass(cssClass)
// Find all parents of selected element
var parentElements = element.parents(".tree-element-children")
// Tag parents of element
var parentLabels = parentElements.siblings(".tree-label");
parentLabels.addClass(cssClass);
}
function changeHandler(event){
toggleSelectedCssClass(this);
}
function searchInputHandler(event){
var elem = $(this);
var val = elem.val()
var allTreeElements = $(".tree-element")
var allTreeElementsContain = $(".tree-element").filter(function(){
var reg = new RegExp(val, "i");
return reg.test($(this).text());
}
);
if(val.length > 0){
// Hide everything
allTreeElements.hide()
// Now show again everything matching the query
allTreeElementsContain.show()
}else{
// Show everything if no query exists
allTreeElements.show()
}
}
// Add event listener on search input
$("#tree-search-input").keyup(searchInputHandler)
// Add event listener on changed checkboxes
$(".tree-input").change(changeHandler);
// initialize all pre-checked checkboxes (e.g. on an edit form)
var preCheckedElements = $(".tree-input:checked");
preCheckedElements.each(function (index, element){
toggleSelectedCssClass(element);
})
</script>

View File

@@ -24,7 +24,7 @@ from konova.autocompletes import EcoAccountAutocomplete, \
ShareTeamAutocomplete, HandlerCodeAutocomplete
from konova.settings import SSO_SERVER, SSO_PUBLIC_KEY, SSO_PRIVATE_KEY, DEBUG
from konova.sso.sso import KonovaSSOClient
from konova.views import logout_view, home_view, get_geom_parcels, map_client_proxy_view
from konova.views import logout_view, home_view, get_geom_parcels, get_geom_parcels_content, map_client_proxy_view
sso_client = KonovaSSOClient(SSO_SERVER, SSO_PUBLIC_KEY, SSO_PRIVATE_KEY)
urlpatterns = [
@@ -40,7 +40,8 @@ urlpatterns = [
path('cl/', include("codelist.urls")),
path('analysis/', include("analysis.urls")),
path('api/', include("api.urls")),
path('geom/<id>/parcels', get_geom_parcels, name="geometry-parcels"),
path('geom/<id>/parcels/', get_geom_parcels, name="geometry-parcels"),
path('geom/<id>/parcels/<int:page>', get_geom_parcels_content, name="geometry-parcels-content"),
path('client/proxy', map_client_proxy_view, name="map-client-proxy"),
# Autocomplete paths for all apps

View File

@@ -5,12 +5,13 @@ Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 17.12.21
"""
import json
from abc import abstractmethod
from json import JSONDecodeError
from time import sleep
import requests
import xmltodict
from django.contrib.gis.db.models.functions import AsGML, Transform
from django.contrib.gis.db.models.functions import AsGML, Transform, MakeValid
from requests.auth import HTTPDigestAuth
from konova.settings import DEFAULT_SRID_RLP, PARCEL_WFS_USER, PARCEL_WFS_PW, PROXIES
@@ -90,7 +91,7 @@ class ParcelWFSFetcher(AbstractWFSFetcher):
).annotate(
transformed=Transform(srid=filter_srid, expression="geom")
).annotate(
gml=AsGML('transformed')
gml=AsGML(MakeValid('transformed'))
).first().gml
spatial_filter = f"<Filter><{geometry_operation}><PropertyName>{self.geometry_property_name}</PropertyName>{geom_gml}</{geometry_operation}></Filter>"
return spatial_filter
@@ -115,7 +116,7 @@ class ParcelWFSFetcher(AbstractWFSFetcher):
geometry_operation,
filter_srid
)
_filter = f'<wfs:GetFeature service="WFS" version="{self.version}" xmlns:wfs="http://www.opengis.net/wfs/2.0" xmlns:fes="http://www.opengis.net/fes/2.0" xmlns:myns="http://www.someserver.com/myns" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.opengis.net/wfs/2.0 http://schemas.opengis.net/wfs/2.0.0/wfs.xsd" count="{self.count}" startindex="{start_index}"><wfs:Query typeNames="{typenames}">{spatial_filter}</wfs:Query></wfs:GetFeature>'
_filter = f'<wfs:GetFeature service="WFS" version="{self.version}" xmlns:wfs="http://www.opengis.net/wfs/2.0" xmlns:fes="http://www.opengis.net/fes/2.0" xmlns:myns="http://www.someserver.com/myns" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.opengis.net/wfs/2.0 http://schemas.opengis.net/wfs/2.0.0/wfs.xsd" count="{self.count}" startindex="{start_index}" outputFormat="application/json; subtype=geojson"><wfs:Query typeNames="{typenames}">{spatial_filter}</wfs:Query></wfs:GetFeature>'
return _filter
def get_features(self,
@@ -139,7 +140,7 @@ class ParcelWFSFetcher(AbstractWFSFetcher):
Returns:
features (list): A list of returned features
"""
features = []
found_features = []
while start_index is not None:
post_body = self._create_post_data(
spatial_operator,
@@ -155,19 +156,11 @@ class ParcelWFSFetcher(AbstractWFSFetcher):
)
content = response.content.decode("utf-8")
content = xmltodict.parse(content)
collection = content.get(
"wfs:FeatureCollection",
{},
)
# Check if collection is an exception and does not contain the requested data
if len(collection) == 0:
exception = content.get(
"ows:ExceptionReport",
{}
)
if len(exception) > 0 and rerun_on_exception:
try:
# Check if collection is an exception and does not contain the requested data
content = json.loads(content)
except JSONDecodeError as e:
if rerun_on_exception:
# Wait a second before another try
sleep(1)
self.get_features(
@@ -177,22 +170,21 @@ class ParcelWFSFetcher(AbstractWFSFetcher):
start_index,
rerun_on_exception=False
)
members = collection.get(
"wfs:member",
None,
)
if members is not None:
if len(members) > 1:
# extend feature list with found list of new feature members
features += members
else:
# convert single found feature member into list and extent feature list
features += [members]
e.msg += content
raise e
fetched_features = content.get(
"features",
{},
)
if collection.get("@next", None) is not None:
start_index += self.count
else:
found_features += fetched_features
if len(fetched_features) < self.count:
# The response was not 'full', so we got everything to fetch
start_index = None
else:
# If a 'full' response returned, there might be more to fetch. Increase the start_index!
start_index += self.count
return features
return found_features

View File

@@ -113,12 +113,12 @@ def get_geom_parcels(request: HttpRequest, id: str):
id (str): The geometry's id
Returns:
A rendered piece of HTML
"""
# HTTP code 286 states that the HTMX should stop polling for updates
# https://htmx.org/docs/#polling
status_code = 286
template = "konova/includes/parcel_table.html"
template = "konova/includes/parcels/parcel_table_frame.html"
geom = get_object_or_404(Geometry, id=id)
parcels = geom.get_underlying_parcels()
geos_geom = geom.geom
@@ -136,9 +136,20 @@ def get_geom_parcels(request: HttpRequest, id: str):
parcels = parcels.order_by("-municipal", "flr", "flrstck_zhlr", "flrstck_nnr")
municipals = parcels.order_by("municipal").distinct("municipal").values("municipal__id")
municipals = Municipal.objects.filter(id__in=municipals)
rpp = 100
num_all_parcels = parcels.count()
parcels = parcels[:rpp]
next_page = 1
if len(parcels) < rpp:
next_page = None
context = {
"num_parcels": num_all_parcels,
"parcels": parcels,
"municipals": municipals,
"geom_id": str(id),
"next_page": next_page,
}
html = render_to_string(template, context, request)
return HttpResponse(html, status=status_code)
@@ -146,6 +157,49 @@ def get_geom_parcels(request: HttpRequest, id: str):
return HttpResponse(None, status=404)
@login_required
def get_geom_parcels_content(request: HttpRequest, id: str, page: int):
""" Getter for infinite scroll of HTMX
Returns parcels of a specific page/slice of the found parcel set.
Implementation of infinite scroll htmx example: https://htmx.org/examples/infinite-scroll/
Args:
request (HttpRequest): The incoming request
id (str): The geometry's id
page (int): The requested page number
Returns:
A rendered piece of HTML
"""
if page < 0:
raise AssertionError("Parcel page can not be negative")
# HTTP code 286 states that the HTMX should stop polling for updates
# https://htmx.org/docs/#polling
status_code = 286
template = "konova/includes/parcels/parcel_table_content.html"
geom = get_object_or_404(Geometry, id=id)
parcels = geom.get_underlying_parcels()
parcels = parcels.order_by("-municipal", "flr", "flrstck_zhlr", "flrstck_nnr")
rpp = 100
from_p = rpp * (page-1)
to_p = rpp * (page)
next_page = page + 1
parcels = parcels[from_p:to_p]
if len(parcels) < rpp:
next_page = None
context = {
"parcels": parcels,
"geom_id": str(id),
"next_page": next_page,
}
html = render_to_string(template, context, request)
return HttpResponse(html, status=status_code)
def get_404_view(request: HttpRequest, exception=None):
""" Returns a 404 handling view