Merge pull request 'master' (#172) from master into Docker

Reviewed-on: SGD-Nord/konova#172
This commit was merged in pull request #172.
This commit is contained in:
2022-06-02 08:53:32 +02:00
135 changed files with 61001 additions and 424 deletions

View File

@@ -98,6 +98,18 @@ class DeadlineAdmin(admin.ModelAdmin):
]
class DeletableObjectMixinAdmin(admin.ModelAdmin):
class Meta:
abstract = True
def restore_deleted_data(self, request, queryset):
queryset = queryset.filter(
deleted__isnull=False
)
for entry in queryset:
entry.deleted.delete()
class BaseResourceAdmin(admin.ModelAdmin):
fields = [
"created",
@@ -109,7 +121,7 @@ class BaseResourceAdmin(admin.ModelAdmin):
]
class BaseObjectAdmin(BaseResourceAdmin):
class BaseObjectAdmin(BaseResourceAdmin, DeletableObjectMixinAdmin):
search_fields = [
"identifier",
"title",
@@ -126,13 +138,6 @@ class BaseObjectAdmin(BaseResourceAdmin):
"deleted",
]
def restore_deleted_data(self, request, queryset):
queryset = queryset.filter(
deleted__isnull=False
)
for entry in queryset:
entry.deleted.delete()
# Outcommented for a cleaner admin backend on production

View File

@@ -96,7 +96,9 @@ class ShareTeamAutocomplete(Select2QuerySetView):
def get_queryset(self):
if self.request.user.is_anonymous:
return Team.objects.none()
qs = Team.objects.all()
qs = Team.objects.filter(
deleted__isnull=True
)
if self.q:
# Due to privacy concerns only a full username match will return the proper user entry
qs = qs.filter(
@@ -108,6 +110,29 @@ class ShareTeamAutocomplete(Select2QuerySetView):
return qs
class TeamAdminAutocomplete(Select2QuerySetView):
""" Autocomplete for share with teams
"""
def get_queryset(self):
if self.request.user.is_anonymous:
return User.objects.none()
qs = User.objects.filter(
id__in=self.forwarded.get("members", [])
).exclude(
id__in=self.forwarded.get("admins", [])
)
if self.q:
# Due to privacy concerns only a full username match will return the proper user entry
qs = qs.filter(
name__icontains=self.q
)
qs = qs.order_by(
"username"
)
return qs
class KonovaCodeAutocomplete(Select2GroupQuerySetView):
"""
Provides simple autocomplete functionality for codes

View File

@@ -305,7 +305,7 @@ class ShareableTableFilterMixin(django_filters.FilterSet):
if not value:
return queryset.filter(
Q(users__in=[self.user]) | # requesting user has access
Q(teams__users__in=[self.user])
Q(teams__in=self.user.shared_teams)
).distinct()
else:
return queryset

View File

@@ -5,18 +5,20 @@ Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 16.11.20
"""
import json
from abc import abstractmethod
from bootstrap_modal_forms.forms import BSModalForm
from bootstrap_modal_forms.utils import is_ajax
from django import forms
from django.contrib import messages
from django.contrib.gis import gdal
from django.db.models.fields.files import FieldFile
from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP
from user.models import User
from django.contrib.gis.forms import OSMWidget, MultiPolygonField
from django.contrib.gis.geos import MultiPolygon
from django.contrib.gis.forms import MultiPolygonField
from django.contrib.gis.geos import MultiPolygon, Polygon
from django.db import transaction
from django.http import HttpRequest, HttpResponseRedirect
from django.shortcuts import render
@@ -272,41 +274,85 @@ class SimpleGeomForm(BaseForm):
""" A geometry form for rendering geometry read-only using a widget
"""
read_only = True
geom = MultiPolygonField(
srid=DEFAULT_SRID,
srid=DEFAULT_SRID_RLP,
label=_("Geometry"),
help_text=_(""),
label_suffix="",
required=False,
disabled=False,
widget=OSMWidget(
attrs={
"map_width": 600,
"map_height": 400,
# default_zoom defines the nearest possible zoom level from which the JS automatically
# zooms out if geometry requires a larger view port. So define a larger range for smaller geometries
"default_zoom": 25,
}
)
)
def __init__(self, *args, **kwargs):
read_only = kwargs.pop("read_only", True)
self.read_only = kwargs.pop("read_only", True)
super().__init__(*args, **kwargs)
# Initialize geometry
try:
geom = self.instance.geometry.geom
self.empty = geom.empty
if self.empty:
raise AttributeError
geojson = self.instance.geometry.as_feature_collection(srid=DEFAULT_SRID_RLP)
geom = json.dumps(geojson)
except AttributeError:
# If no geometry exists for this form, we simply set the value to None and zoom to the maximum level
geom = None
geom = ""
self.empty = True
self.fields["geom"].widget.attrs["default_zoom"] = 1
self.initialize_form_field("geom", geom)
if read_only:
self.fields["geom"].disabled = True
def is_valid(self):
super().is_valid()
is_valid = True
# Get geojson from form
geom = self.data["geom"]
if geom is None or len(geom) == 0:
# empty geometry is a valid geometry
return is_valid
geom = json.loads(geom)
# Write submitted data back into form field to make sure invalid geometry
# will be rendered again on failed submit
self.initialize_form_field("geom", self.data["geom"])
# Read geojson into gdal geometry
# HINT: This can be simplified if the geojson format holds data in epsg:4326 (GDAL provides direct creation for
# this case)
features = []
features_json = geom.get("features", [])
for feature in features_json:
g = gdal.OGRGeometry(json.dumps(feature.get("geometry", feature)), srs=DEFAULT_SRID_RLP)
if g.geom_type not in ["Polygon", "MultiPolygon"]:
self.add_error("geom", _("Only surfaces allowed. Points or lines must be buffered."))
is_valid = False
return is_valid
polygon = Polygon.from_ewkt(g.ewkt)
is_valid = polygon.valid
if not is_valid:
self.add_error("geom", polygon.valid_reason)
return is_valid
features.append(polygon)
form_geom = MultiPolygon(srid=DEFAULT_SRID_RLP)
for feature in features:
form_geom = form_geom.union(feature)
# Make sure to convert into a MultiPolygon. Relevant if a single Polygon is provided.
if form_geom.geom_type != "MultiPolygon":
form_geom = MultiPolygon(form_geom, srid=DEFAULT_SRID_RLP)
# Write unioned Multipolygon into cleaned data
if self.cleaned_data is None:
self.cleaned_data = {}
self.cleaned_data["geom"] = form_geom.ewkt
return is_valid
def save(self, action: UserActionLogEntry):
""" Saves the form's geometry

View File

@@ -0,0 +1,17 @@
# Generated by Django 3.1.3 on 2022-04-20 08:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('konova', '0009_auto_20220411_1004'),
]
operations = [
migrations.AddConstraint(
model_name='parcel',
constraint=models.UniqueConstraint(fields=('district', 'municipal', 'parcel_group', 'flr', 'flrstck_nnr', 'flrstck_zhlr'), name='Unique parcel constraint'),
),
]

View File

@@ -0,0 +1,25 @@
# Generated by Django 3.1.3 on 2022-04-20 09:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('konova', '0010_auto_20220420_1034'),
]
operations = [
migrations.AddConstraint(
model_name='district',
constraint=models.UniqueConstraint(fields=('key', 'name'), name='Unique district constraint'),
),
migrations.AddConstraint(
model_name='municipal',
constraint=models.UniqueConstraint(fields=('key', 'name', 'district'), name='Unique municipal constraint'),
),
migrations.AddConstraint(
model_name='parcelgroup',
constraint=models.UniqueConstraint(fields=('key', 'name', 'municipal'), name='Unique parcel group constraint'),
),
]

View File

@@ -5,11 +5,15 @@ Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 15.11.21
"""
import json
from django.contrib.gis.db.models import MultiPolygonField
from django.db import models
from django.contrib.gis.geos import Polygon
from django.db import models, transaction
from django.utils import timezone
from konova.models import BaseResource, UuidModel
from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP
from konova.utils.wfs.spatial import ParcelWFSFetcher
@@ -96,6 +100,7 @@ class Geometry(BaseResource):
objs += set_objs
return objs
@transaction.atomic
def update_parcels(self):
""" Updates underlying parcel information
@@ -152,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
@@ -172,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(
@@ -184,6 +189,44 @@ 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
This method is used to convert a single MultiPolygon into multiple Polygons, which can be used as separated
features in the NETGIS map client.
Args:
srid (int): The spatial reference system identifier to be transformed to
Returns:
geojson (dict): The FeatureCollection json (as dict)
"""
geom = self.geom
geom.transform(ct=srid)
polygons = []
for coords in geom.coords:
p = Polygon(coords[0], srid=geom.srid)
polygons.append(p)
geojson = {
"type": "FeatureCollection",
"features": [
json.loads(x.geojson) for x in polygons
]
}
return geojson
class GeometryConflict(UuidModel):
"""

View File

@@ -87,25 +87,15 @@ class BaseResource(UuidModel):
super().delete()
class BaseObject(BaseResource):
"""
A basic object model, which specifies BaseResource.
class DeletableObjectMixin(models.Model):
""" Wraps deleted field and related functionality
Mainly used for intervention, compensation, ecoaccount
"""
identifier = models.CharField(max_length=1000, null=True, blank=True)
title = models.CharField(max_length=1000, null=True, blank=True)
deleted = models.ForeignKey("user.UserActionLogEntry", on_delete=models.SET_NULL, null=True, blank=True, related_name='+')
comment = models.TextField(null=True, blank=True)
log = models.ManyToManyField("user.UserActionLogEntry", blank=True, help_text="Keeps all user actions of an object", editable=False)
class Meta:
abstract = True
@abstractmethod
def set_status_messages(self, request: HttpRequest):
raise NotImplementedError
def mark_as_deleted(self, user, send_mail: bool = True):
""" Mark an entry as deleted
@@ -140,6 +130,25 @@ class BaseObject(BaseResource):
self.save()
class BaseObject(BaseResource, DeletableObjectMixin):
"""
A basic object model, which specifies BaseResource.
Mainly used for intervention, compensation, ecoaccount
"""
identifier = models.CharField(max_length=1000, null=True, blank=True)
title = models.CharField(max_length=1000, null=True, blank=True)
comment = models.TextField(null=True, blank=True)
log = models.ManyToManyField("user.UserActionLogEntry", blank=True, help_text="Keeps all user actions of an object", editable=False)
class Meta:
abstract = True
@abstractmethod
def set_status_messages(self, request: HttpRequest):
raise NotImplementedError
def mark_as_edited(self, performing_user, request: HttpRequest = None, edit_comment: str = None):
""" In case the object or a related object changed the log history needs to be updated
@@ -408,6 +417,20 @@ class CheckableObjectMixin(models.Model):
self.log.add(action)
return action
def get_last_checked_action(self):
""" Getter for the most recent checked action on the log
Returns:
previously_checked (UserActionLogEntry): The most recent checked action
"""
from user.models import UserAction
previously_checked = self.log.filter(
action=UserAction.CHECKED
).order_by(
"-timestamp"
).first()
return previously_checked
class ShareableObjectMixin(models.Model):
# Users having access on this object
@@ -470,8 +493,8 @@ class ShareableObjectMixin(models.Model):
Returns:
"""
directly_shared = self.users.filter(id=user.id).exists()
team_shared = self.teams.filter(
directly_shared = self.shared_users.filter(id=user.id).exists()
team_shared = self.shared_teams.filter(
users__in=[user]
).exists()
is_shared = directly_shared or team_shared
@@ -608,7 +631,9 @@ class ShareableObjectMixin(models.Model):
Returns:
teams (QuerySet)
"""
return self.teams.all()
return self.teams.filter(
deleted__isnull=True
)
@abstractmethod
def get_share_url(self):
@@ -652,10 +677,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

@@ -39,7 +39,17 @@ class District(UuidModel, AdministrativeSpatialReference):
""" The model District refers to "Kreis"
"""
pass
class Meta:
constraints = [
models.UniqueConstraint(
fields=[
"key",
"name",
],
name="Unique district constraint"
)
]
class Municipal(UuidModel, AdministrativeSpatialReference):
@@ -53,6 +63,18 @@ class Municipal(UuidModel, AdministrativeSpatialReference):
blank=True,
)
class Meta:
constraints = [
models.UniqueConstraint(
fields=[
"key",
"name",
"district",
],
name="Unique municipal constraint"
)
]
class ParcelGroup(UuidModel, AdministrativeSpatialReference):
""" The model ParcelGroup refers to "Gemarkung", which is defined as a loose group of parcels
@@ -65,6 +87,18 @@ class ParcelGroup(UuidModel, AdministrativeSpatialReference):
blank=True,
)
class Meta:
constraints = [
models.UniqueConstraint(
fields=[
"key",
"name",
"municipal",
],
name="Unique parcel group constraint"
)
]
class Parcel(UuidModel):
""" The Parcel model holds administrative data on covered properties.
@@ -106,6 +140,21 @@ class Parcel(UuidModel):
)
updated_on = models.DateTimeField(auto_now_add=True)
class Meta:
constraints = [
models.UniqueConstraint(
fields=[
"district",
"municipal",
"parcel_group",
"flr",
"flrstck_nnr",
"flrstck_zhlr",
],
name="Unique parcel constraint"
)
]
def __str__(self):
return f"{self.parcel_group} | {self.flr} | {self.flrstck_zhlr} | {self.flrstck_nnr}"

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

@@ -46,8 +46,8 @@ ALLOWED_HOSTS = [
LOGIN_URL = "/login/"
# Session settings
#SESSION_COOKIE_AGE = 30 * 60 # 30 minutes
#SESSION_SAVE_EVERY_REQUEST = True
SESSION_COOKIE_AGE = 60 * 60 # 60 minutes
SESSION_SAVE_EVERY_REQUEST = True
# Application definition
@@ -192,6 +192,8 @@ STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, "static")
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'konova/static'),
os.path.join(BASE_DIR, 'templates/map/client'), # NETGIS map client files
os.path.join(BASE_DIR, 'templates/map/client/libs'), # NETGIS map client files
]
# DJANGO DEBUG TOOLBAR

View File

@@ -6,7 +6,7 @@
{% if obj.comment %}
<div class="w-100">
<div class="col-sm-12">
<div class="card mt-3">
<div class="card-header rlp-gd">
<div class="row">

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>

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

@@ -72,6 +72,7 @@ class AutocompleteTestCase(BaseTestCase):
"codes-conservation-office-autocomplete",
"share-user-autocomplete",
"share-team-autocomplete",
"team-admin-autocomplete",
]
for test in tests:
self.client.login(username=self.superuser.username, password=self.superuser_pw)

View File

@@ -6,9 +6,11 @@ Created on: 26.10.21
"""
import datetime
import json
from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID
from ema.models import Ema
from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP
from user.models import User, Team
from django.contrib.auth.models import Group
from django.contrib.gis.geos import MultiPolygon, Polygon
@@ -272,7 +274,6 @@ class BaseTestCase(TestCase):
team = Team.objects.get_or_create(
name="Testteam",
description="Testdescription",
admin=self.superuser,
)[0]
team.users.add(self.superuser)
@@ -287,8 +288,28 @@ class BaseTestCase(TestCase):
"""
polygon = Polygon.from_bbox((7.592449, 50.359385, 7.593382, 50.359874))
polygon.srid = 4326
polygon = polygon.transform(3857, clone=True)
return MultiPolygon(polygon, srid=3857) # 3857 is the default srid used for MultiPolygonField in the form
polygon = polygon.transform(DEFAULT_SRID_RLP, clone=True)
return MultiPolygon(polygon, srid=DEFAULT_SRID_RLP)
def create_geojson(self, geometry):
""" Creates a default structure including geojson from a geometry
Args:
geometry ():
Returns:
"""
geom_json = {
"features": [
{
"type": "Feature",
"geometry": json.loads(geometry.geojson),
}
]
}
geom_json = json.dumps(geom_json)
return geom_json
def create_dummy_handler(self) -> Handler:
""" Creates a Handler
@@ -410,11 +431,12 @@ class BaseTestCase(TestCase):
return
if geom1.srid != geom2.srid:
tolerance = 0.001
# Due to prior possible transformation of any of these geometries, we need to make sure there exists a
# transformation from one coordinate system into the other, which is valid
geom1_t = geom1.transform(geom2.srid, clone=True)
geom2_t = geom2.transform(geom1.srid, clone=True)
self.assertTrue(geom1_t.equals(geom2) or geom2_t.equals(geom1))
self.assertTrue(geom1_t.equals_exact(geom2, tolerance) or geom2_t.equals_exact(geom1, tolerance))
else:
self.assertTrue(geom1.equals(geom2))

View File

@@ -21,10 +21,10 @@ from konova.autocompletes import EcoAccountAutocomplete, \
InterventionAutocomplete, CompensationActionCodeAutocomplete, BiotopeCodeAutocomplete, LawCodeAutocomplete, \
RegistrationOfficeCodeAutocomplete, ConservationOfficeCodeAutocomplete, ProcessTypeCodeAutocomplete, \
ShareUserAutocomplete, BiotopeExtraCodeAutocomplete, CompensationActionDetailCodeAutocomplete, \
ShareTeamAutocomplete, HandlerCodeAutocomplete
ShareTeamAutocomplete, HandlerCodeAutocomplete, TeamAdminAutocomplete
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, get_geom_parcels_content
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 = [
@@ -42,6 +42,7 @@ urlpatterns = [
path('api/', include("api.urls")),
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
path("atcmplt/eco-accounts", EcoAccountAutocomplete.as_view(), name="accounts-autocomplete"),
@@ -57,6 +58,7 @@ urlpatterns = [
path("atcmplt/codes/handler", HandlerCodeAutocomplete.as_view(), name="codes-handler-autocomplete"),
path("atcmplt/share/u", ShareUserAutocomplete.as_view(), name="share-user-autocomplete"),
path("atcmplt/share/t", ShareTeamAutocomplete.as_view(), name="share-team-autocomplete"),
path("atcmplt/team/admin", TeamAdminAutocomplete.as_view(), name="team-admin-autocomplete"),
]
if DEBUG:

View File

@@ -81,3 +81,8 @@ GEOMETRY_CONFLICT_WITH_TEMPLATE = _("Geometry conflict detected with {}")
# INTERVENTION
INTERVENTION_HAS_REVOCATIONS_TEMPLATE = _("This intervention has {} revocations")
# CHECKED
DATA_CHECKED_ON_TEMPLATE = _("Checked on {} by {}")
DATA_CHECKED_PREVIOUSLY_TEMPLATE = _("Data has changed since last check on {} by {}")
DATA_IS_UNCHECKED = _("Current data not checked yet")

View File

@@ -112,6 +112,17 @@ class BaseTable(tables.tables.Table):
icon
)
def render_previously_checked_star(self, tooltip: str = None):
"""
Returns a star icon for a check action in the past
"""
icon = "fas fa-star rlp-gd-inv"
return format_html(
"<em title='{}' class='{}'></em>",
tooltip,
icon
)
def render_bookmark(self, tooltip: str = None, icn_filled: bool = False):
"""
Returns a bookmark icon

View File

@@ -11,7 +11,7 @@ from json import JSONDecodeError
from time import sleep
import requests
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
@@ -91,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

View File

@@ -5,9 +5,12 @@ Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 16.11.20
"""
import json
import requests
from django.contrib.auth import logout
from django.contrib.auth.decorators import login_required
from django.http import HttpRequest, HttpResponse
from django.http import HttpRequest, HttpResponse, JsonResponse
from django.shortcuts import redirect, render, get_object_or_404
from django.template.loader import render_to_string
from django.utils import timezone
@@ -135,12 +138,14 @@ def get_geom_parcels(request: HttpRequest, id: str):
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),
@@ -220,3 +225,26 @@ def get_500_view(request: HttpRequest):
"""
context = BaseContext.context
return render(request, "500.html", context, status=500)
@login_required
def map_client_proxy_view(request: HttpRequest):
""" Provides proxy functionality for NETGIS map client.
Used for fetching content of a provided url
Args:
request (HttpRequest): The incoming request
Returns:
"""
url = request.META.get("QUERY_STRING")
response = requests.get(url)
body = json.loads(response.content)
if response.status_code != 200:
return JsonResponse({
"status_code": response.status_code,
"content": body,
})
return JsonResponse(body)