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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
17
konova/migrations/0010_auto_20220420_1034.py
Normal file
17
konova/migrations/0010_auto_20220420_1034.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
25
konova/migrations/0011_auto_20220420_1101.py
Normal file
25
konova/migrations/0011_auto_20220420_1101.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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}"
|
||||
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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 %}
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user