Merge branch 'master' into 132_Old_entries
# Conflicts: # konova/management/commands/update_all_parcels.py # konova/urls.py # user/admin.py
This commit is contained in:
commit
dd5cbbef10
@ -6,6 +6,7 @@
|
||||
"title": "Test_compensation",
|
||||
"is_cef": false,
|
||||
"is_coherence_keeping": false,
|
||||
"is_pik": false,
|
||||
"intervention": "MUST_BE_SET_IN_TEST",
|
||||
"before_states": [
|
||||
],
|
||||
|
@ -5,6 +5,7 @@
|
||||
"properties": {
|
||||
"title": "Test_ecoaccount",
|
||||
"deductable_surface": 10000.0,
|
||||
"is_pik": false,
|
||||
"responsible": {
|
||||
"conservation_office": null,
|
||||
"conservation_file_number": null,
|
||||
|
@ -4,6 +4,7 @@
|
||||
],
|
||||
"properties": {
|
||||
"title": "Test_ema",
|
||||
"is_pik": false,
|
||||
"responsible": {
|
||||
"conservation_office": null,
|
||||
"conservation_file_number": null,
|
||||
|
@ -122,6 +122,7 @@ class APIV1GetTestCase(BaseAPIV1TestCase):
|
||||
props = geojson["properties"]
|
||||
props["is_cef"]
|
||||
props["is_coherence_keeping"]
|
||||
props["is_pik"]
|
||||
props["intervention"]
|
||||
props["intervention"]["id"]
|
||||
props["intervention"]["identifier"]
|
||||
|
@ -46,6 +46,7 @@
|
||||
"title": "TEST_compensation_CHANGED",
|
||||
"is_cef": true,
|
||||
"is_coherence_keeping": true,
|
||||
"is_pik": true,
|
||||
"intervention": "CHANGE_BEFORE_RUN!!!",
|
||||
"before_states": [],
|
||||
"after_states": [],
|
||||
|
@ -45,6 +45,7 @@
|
||||
"properties": {
|
||||
"title": "TEST_account_CHANGED",
|
||||
"deductable_surface": "100000.0",
|
||||
"is_pik": true,
|
||||
"responsible": {
|
||||
"conservation_office": null,
|
||||
"conservation_file_number": "123-TEST",
|
||||
|
@ -52,6 +52,7 @@
|
||||
"detail": "TEST_HANDLER_CHANGED"
|
||||
}
|
||||
},
|
||||
"is_pik": true,
|
||||
"before_states": [],
|
||||
"after_states": [],
|
||||
"actions": [],
|
||||
|
@ -97,6 +97,7 @@ class APIV1UpdateTestCase(BaseAPIV1TestCase):
|
||||
self.assertNotEqual(modified_on, self.compensation.modified)
|
||||
self.assertEqual(put_props["is_cef"], self.compensation.is_cef)
|
||||
self.assertEqual(put_props["is_coherence_keeping"], self.compensation.is_coherence_keeping)
|
||||
self.assertEqual(put_props["is_pik"], self.compensation.is_pik)
|
||||
self.assertEqual(len(put_props["actions"]), self.compensation.actions.count())
|
||||
self.assertEqual(len(put_props["before_states"]), self.compensation.before_states.count())
|
||||
self.assertEqual(len(put_props["after_states"]), self.compensation.after_states.count())
|
||||
|
@ -34,6 +34,7 @@ class CompensationAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensa
|
||||
def _extend_properties_data(self, entry):
|
||||
self.properties_data["is_cef"] = entry.is_cef
|
||||
self.properties_data["is_coherence_keeping"] = entry.is_coherence_keeping
|
||||
self.properties_data["is_pik"] = entry.is_pik
|
||||
self.properties_data["intervention"] = self.intervention_to_json(entry.intervention)
|
||||
self.properties_data["before_states"] = self._compensation_state_to_json(entry.before_states.all())
|
||||
self.properties_data["after_states"] = self._compensation_state_to_json(entry.after_states.all())
|
||||
@ -113,6 +114,7 @@ class CompensationAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensa
|
||||
obj.title = properties["title"]
|
||||
obj.is_cef = properties["is_cef"]
|
||||
obj.is_coherence_keeping = properties["is_coherence_keeping"]
|
||||
obj.is_pik = properties.get("is_pik", False)
|
||||
obj = self.set_intervention(obj, properties["intervention"], user)
|
||||
|
||||
obj.geometry.save()
|
||||
@ -149,6 +151,7 @@ class CompensationAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensa
|
||||
obj.title = properties["title"]
|
||||
obj.is_cef = properties["is_cef"]
|
||||
obj.is_coherence_keeping = properties["is_coherence_keeping"]
|
||||
obj.is_pik = properties.get("is_pik", False)
|
||||
obj.modified = update_action
|
||||
obj.geometry.geom = self._create_geometry_from_json(json_model)
|
||||
obj.geometry.modified = update_action
|
||||
|
@ -25,6 +25,7 @@ class EcoAccountAPISerializerV1(AbstractModelAPISerializerV1,
|
||||
model = EcoAccount
|
||||
|
||||
def _extend_properties_data(self, entry):
|
||||
self.properties_data["is_pik"] = entry.is_pik
|
||||
self.properties_data["deductable_surface"] = entry.deductable_surface
|
||||
self.properties_data["deductable_surface_available"] = entry.deductable_surface - entry.get_deductions_surface()
|
||||
self.properties_data["responsible"] = self._responsible_to_json(entry.responsible)
|
||||
@ -122,6 +123,7 @@ class EcoAccountAPISerializerV1(AbstractModelAPISerializerV1,
|
||||
properties = json_model["properties"]
|
||||
obj.identifier = obj.generate_new_identifier()
|
||||
obj.title = properties["title"]
|
||||
obj.is_pik = properties.get("is_pik", False)
|
||||
|
||||
try:
|
||||
obj.deductable_surface = float(properties["deductable_surface"])
|
||||
@ -169,6 +171,7 @@ class EcoAccountAPISerializerV1(AbstractModelAPISerializerV1,
|
||||
# Fill in data to objects
|
||||
properties = json_model["properties"]
|
||||
obj.title = properties["title"]
|
||||
obj.is_pik = properties.get("is_pik", False)
|
||||
obj.deductable_surface = float(properties["deductable_surface"])
|
||||
obj.modified = update_action
|
||||
obj.geometry.geom = self._create_geometry_from_json(json_model)
|
||||
|
@ -21,6 +21,7 @@ class EmaAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensationAPISe
|
||||
model = Ema
|
||||
|
||||
def _extend_properties_data(self, entry):
|
||||
self.properties_data["is_pik"] = entry.is_pik
|
||||
self.properties_data["responsible"] = self._responsible_to_json(entry.responsible)
|
||||
self.properties_data["before_states"] = self._compensation_state_to_json(entry.before_states.all())
|
||||
self.properties_data["after_states"] = self._compensation_state_to_json(entry.after_states.all())
|
||||
@ -104,6 +105,7 @@ class EmaAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensationAPISe
|
||||
properties = json_model["properties"]
|
||||
obj.identifier = obj.generate_new_identifier()
|
||||
obj.title = properties["title"]
|
||||
obj.is_pik = properties.get("is_pik", False)
|
||||
obj = self._set_responsibility(obj, properties["responsible"])
|
||||
|
||||
obj.geometry.save()
|
||||
@ -141,6 +143,7 @@ class EmaAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensationAPISe
|
||||
# Fill in data to objects
|
||||
properties = json_model["properties"]
|
||||
obj.title = properties["title"]
|
||||
obj.is_pik = properties.get("is_pik", False)
|
||||
obj.modified = update_action
|
||||
obj.geometry.geom = self._create_geometry_from_json(json_model)
|
||||
obj.geometry.modified = update_action
|
||||
|
@ -132,6 +132,7 @@ class InterventionAPISerializerV1(AbstractModelAPISerializerV1,
|
||||
id__in=payments
|
||||
)
|
||||
obj.payments.set(payments)
|
||||
obj.send_data_to_egon()
|
||||
return obj
|
||||
|
||||
def create_model_from_json(self, json_model, user):
|
||||
@ -197,7 +198,7 @@ class InterventionAPISerializerV1(AbstractModelAPISerializerV1,
|
||||
obj.legal.save()
|
||||
obj.save()
|
||||
|
||||
obj.mark_as_edited(user)
|
||||
obj.mark_as_edited(user, edit_comment="API update")
|
||||
|
||||
celery_update_parcels.delay(obj.geometry.id)
|
||||
|
||||
|
@ -75,7 +75,10 @@ class AbstractModelAPISerializerV1(AbstractModelAPISerializer):
|
||||
Returns:
|
||||
|
||||
"""
|
||||
if json_str is None or len(json_str) == 0:
|
||||
if json_str is None:
|
||||
return None
|
||||
json_str = str(json_str)
|
||||
if len(json_str) == 0:
|
||||
return None
|
||||
code = KonovaCode.objects.get(
|
||||
atom_id=json_str,
|
||||
|
@ -33,6 +33,7 @@ class KonovaCodeAdmin(admin.ModelAdmin):
|
||||
"is_selectable",
|
||||
"is_leaf",
|
||||
"parent",
|
||||
"found_in_codelists",
|
||||
]
|
||||
|
||||
search_fields = [
|
||||
@ -42,6 +43,12 @@ class KonovaCodeAdmin(admin.ModelAdmin):
|
||||
"short_name",
|
||||
]
|
||||
|
||||
def found_in_codelists(self, obj):
|
||||
codelists = KonovaCodeList.objects.filter(
|
||||
codes__in=[obj]
|
||||
).values_list("id", flat=True)
|
||||
codelists = "\n".join(str(x) for x in codelists)
|
||||
return codelists
|
||||
|
||||
#admin.site.register(KonovaCodeList, KonovaCodeListAdmin)
|
||||
admin.site.register(KonovaCode, KonovaCodeAdmin)
|
||||
|
@ -65,24 +65,23 @@ class KonovaCode(models.Model):
|
||||
ret_val += ", " + self.parent.long_name
|
||||
return ret_val
|
||||
|
||||
def add_children(self):
|
||||
def add_children(self, order_by: str = "long_name"):
|
||||
""" Adds all children (resurcively until leaf) as .children to the KonovaCode
|
||||
|
||||
Returns:
|
||||
code (KonovaCode): The manipulated KonovaCode instance
|
||||
"""
|
||||
if self.is_leaf:
|
||||
return None
|
||||
return self
|
||||
|
||||
children = KonovaCode.objects.filter(
|
||||
code_lists__in=self.code_lists.all(),
|
||||
parent=self
|
||||
).order_by(
|
||||
"long_name"
|
||||
order_by
|
||||
)
|
||||
self.children = children
|
||||
for child in children:
|
||||
child.add_children()
|
||||
child.add_children(order_by)
|
||||
return self
|
||||
|
||||
|
||||
|
@ -21,16 +21,30 @@ class AbstractCompensationAdmin(BaseObjectAdmin):
|
||||
"identifier",
|
||||
"title",
|
||||
"comment",
|
||||
"after_states",
|
||||
"before_states",
|
||||
"list_after_states",
|
||||
"list_before_states",
|
||||
"geometry",
|
||||
]
|
||||
|
||||
def get_readonly_fields(self, request, obj=None):
|
||||
return super().get_readonly_fields(request, obj) + [
|
||||
"after_states",
|
||||
"before_states",
|
||||
"list_after_states",
|
||||
"list_before_states",
|
||||
"geometry",
|
||||
]
|
||||
|
||||
def list_after_states(self, obj):
|
||||
states = obj.after_states.all()
|
||||
states = [str(state) for state in states]
|
||||
states = "\n".join(states)
|
||||
return states
|
||||
|
||||
def list_before_states(self, obj):
|
||||
states = obj.before_states.all()
|
||||
states = [str(state) for state in states]
|
||||
states = "\n".join(states)
|
||||
return states
|
||||
|
||||
|
||||
class CompensationAdmin(AbstractCompensationAdmin):
|
||||
autocomplete_fields = [
|
||||
@ -41,6 +55,7 @@ class CompensationAdmin(AbstractCompensationAdmin):
|
||||
return super().get_fields(request, obj) + [
|
||||
"is_cef",
|
||||
"is_coherence_keeping",
|
||||
"is_pik",
|
||||
"intervention",
|
||||
]
|
||||
|
||||
|
@ -60,7 +60,7 @@ class CheckboxCompensationTableFilter(CheckboxTableFilter):
|
||||
if not value:
|
||||
return queryset.filter(
|
||||
Q(intervention__users__in=[self.user]) | # requesting user has access
|
||||
Q(intervention__teams__users__in=[self.user])
|
||||
Q(intervention__teams__in=self.user.shared_teams)
|
||||
).distinct()
|
||||
else:
|
||||
return queryset
|
||||
|
@ -160,7 +160,23 @@ class CoherenceCompensationFormMixin(forms.Form):
|
||||
)
|
||||
|
||||
|
||||
class NewCompensationForm(AbstractCompensationForm, CEFCompensationFormMixin, CoherenceCompensationFormMixin):
|
||||
class PikCompensationFormMixin(forms.Form):
|
||||
""" A form mixin, providing PIK compensation field
|
||||
|
||||
"""
|
||||
is_pik = forms.BooleanField(
|
||||
label_suffix="",
|
||||
label=_("Is PIK"),
|
||||
help_text=_("Optionally: Whether this compensation is a compensation integrated in production?"),
|
||||
required=False,
|
||||
widget=forms.CheckboxInput()
|
||||
)
|
||||
|
||||
|
||||
class NewCompensationForm(AbstractCompensationForm,
|
||||
CEFCompensationFormMixin,
|
||||
CoherenceCompensationFormMixin,
|
||||
PikCompensationFormMixin):
|
||||
""" Form for creating new compensations.
|
||||
|
||||
Can be initialized with an intervention id for preselecting the related intervention.
|
||||
@ -191,6 +207,7 @@ class NewCompensationForm(AbstractCompensationForm, CEFCompensationFormMixin, Co
|
||||
"identifier",
|
||||
"title",
|
||||
"intervention",
|
||||
"is_pik",
|
||||
"is_cef",
|
||||
"is_coherence_keeping",
|
||||
"comment",
|
||||
@ -234,6 +251,7 @@ class NewCompensationForm(AbstractCompensationForm, CEFCompensationFormMixin, Co
|
||||
intervention = self.cleaned_data.get("intervention", None)
|
||||
is_cef = self.cleaned_data.get("is_cef", None)
|
||||
is_coherence_keeping = self.cleaned_data.get("is_coherence_keeping", None)
|
||||
is_pik = self.cleaned_data.get("is_pik", None)
|
||||
comment = self.cleaned_data.get("comment", None)
|
||||
|
||||
# Create log entry
|
||||
@ -249,6 +267,7 @@ class NewCompensationForm(AbstractCompensationForm, CEFCompensationFormMixin, Co
|
||||
created=action,
|
||||
is_cef=is_cef,
|
||||
is_coherence_keeping=is_coherence_keeping,
|
||||
is_pik=is_pik,
|
||||
geometry=geometry,
|
||||
comment=comment,
|
||||
)
|
||||
@ -281,6 +300,7 @@ class EditCompensationForm(NewCompensationForm):
|
||||
"intervention": self.instance.intervention,
|
||||
"is_cef": self.instance.is_cef,
|
||||
"is_coherence_keeping": self.instance.is_coherence_keeping,
|
||||
"is_pik": self.instance.is_pik,
|
||||
"comment": self.instance.comment,
|
||||
}
|
||||
disabled_fields = []
|
||||
@ -297,6 +317,7 @@ class EditCompensationForm(NewCompensationForm):
|
||||
intervention = self.cleaned_data.get("intervention", None)
|
||||
is_cef = self.cleaned_data.get("is_cef", None)
|
||||
is_coherence_keeping = self.cleaned_data.get("is_coherence_keeping", None)
|
||||
is_pik = self.cleaned_data.get("is_pik", None)
|
||||
comment = self.cleaned_data.get("comment", None)
|
||||
|
||||
# Create log entry
|
||||
@ -313,6 +334,7 @@ class EditCompensationForm(NewCompensationForm):
|
||||
self.instance.is_cef = is_cef
|
||||
self.instance.is_coherence_keeping = is_coherence_keeping
|
||||
self.instance.comment = comment
|
||||
self.instance.is_pik = is_pik
|
||||
self.instance.modified = action
|
||||
self.instance.save()
|
||||
|
||||
@ -322,7 +344,7 @@ class EditCompensationForm(NewCompensationForm):
|
||||
return self.instance
|
||||
|
||||
|
||||
class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMixin):
|
||||
class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMixin, PikCompensationFormMixin):
|
||||
""" Form for creating eco accounts
|
||||
|
||||
Inherits from basic AbstractCompensationForm and further form fields from CompensationResponsibleFormMixin
|
||||
@ -363,6 +385,7 @@ class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMix
|
||||
"registration_date",
|
||||
"surface",
|
||||
"conservation_file_number",
|
||||
"is_pik",
|
||||
"handler_type",
|
||||
"handler_detail",
|
||||
"comment",
|
||||
@ -392,6 +415,7 @@ class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMix
|
||||
surface = self.cleaned_data.get("surface", None)
|
||||
conservation_office = self.cleaned_data.get("conservation_office", None)
|
||||
conservation_file_number = self.cleaned_data.get("conservation_file_number", None)
|
||||
is_pik = self.cleaned_data.get("is_pik", None)
|
||||
comment = self.cleaned_data.get("comment", None)
|
||||
|
||||
# Create log entry
|
||||
@ -423,6 +447,7 @@ class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMix
|
||||
created=action,
|
||||
geometry=geometry,
|
||||
comment=comment,
|
||||
is_pik=is_pik,
|
||||
legal=legal
|
||||
)
|
||||
acc.share_with_user(user)
|
||||
@ -458,6 +483,7 @@ class EditEcoAccountForm(NewEcoAccountForm):
|
||||
"registration_date": reg_date,
|
||||
"conservation_office": self.instance.responsible.conservation_office,
|
||||
"conservation_file_number": self.instance.responsible.conservation_file_number,
|
||||
"is_pik": self.instance.is_pik,
|
||||
"comment": self.instance.comment,
|
||||
}
|
||||
disabled_fields = []
|
||||
@ -478,6 +504,7 @@ class EditEcoAccountForm(NewEcoAccountForm):
|
||||
conservation_office = self.cleaned_data.get("conservation_office", None)
|
||||
conservation_file_number = self.cleaned_data.get("conservation_file_number", None)
|
||||
comment = self.cleaned_data.get("comment", None)
|
||||
is_pik = self.cleaned_data.get("is_pik", None)
|
||||
|
||||
# Create log entry
|
||||
action = UserActionLogEntry.get_edited_action(user)
|
||||
@ -503,6 +530,7 @@ class EditEcoAccountForm(NewEcoAccountForm):
|
||||
self.instance.deductable_surface = surface
|
||||
self.instance.geometry = geometry
|
||||
self.instance.comment = comment
|
||||
self.instance.is_pik = is_pik
|
||||
self.instance.modified = action
|
||||
self.instance.save()
|
||||
|
||||
|
@ -17,7 +17,8 @@ from codelist.models import KonovaCode
|
||||
from codelist.settings import CODELIST_BIOTOPES_ID, CODELIST_COMPENSATION_ACTION_ID, CODELIST_BIOTOPES_EXTRA_CODES_ID, \
|
||||
CODELIST_COMPENSATION_ACTION_DETAIL_ID
|
||||
from compensation.models import CompensationDocument, EcoAccountDocument
|
||||
from intervention.inputs import CompensationActionTreeCheckboxSelectMultiple
|
||||
from intervention.inputs import CompensationActionTreeCheckboxSelectMultiple, \
|
||||
CompensationStateTreeRadioSelect
|
||||
from konova.contexts import BaseContext
|
||||
from konova.forms import BaseModalForm, NewDocumentModalForm, RemoveModalForm
|
||||
from konova.models import DeadlineType
|
||||
@ -128,6 +129,7 @@ class EditPaymentModalForm(NewPaymentForm):
|
||||
payment.comment = self.cleaned_data.get("comment", None)
|
||||
payment.save()
|
||||
self.instance.mark_as_edited(self.user, self.request, edit_comment=PAYMENT_EDITED)
|
||||
self.instance.send_data_to_egon()
|
||||
return payment
|
||||
|
||||
|
||||
@ -155,22 +157,12 @@ class NewStateModalForm(BaseModalForm):
|
||||
What has been on this area before changes/compensations have been applied and what will be the result ('after')?
|
||||
|
||||
"""
|
||||
biotope_type = forms.ModelChoiceField(
|
||||
biotope_type = forms.ChoiceField(
|
||||
label=_("Biotope Type"),
|
||||
label_suffix="",
|
||||
required=True,
|
||||
help_text=_("Select the biotope type"),
|
||||
queryset=KonovaCode.objects.filter(
|
||||
is_archived=False,
|
||||
is_leaf=True,
|
||||
code_lists__in=[CODELIST_BIOTOPES_ID],
|
||||
),
|
||||
widget=autocomplete.ModelSelect2(
|
||||
url="codes-biotope-autocomplete",
|
||||
attrs={
|
||||
"data-placeholder": _("Biotope Type"),
|
||||
}
|
||||
),
|
||||
widget=CompensationStateTreeRadioSelect(),
|
||||
)
|
||||
biotope_extra = forms.ModelMultipleChoiceField(
|
||||
label=_("Biotope additional type"),
|
||||
@ -208,6 +200,16 @@ class NewStateModalForm(BaseModalForm):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.form_title = _("New state")
|
||||
self.form_caption = _("Insert data for the new state")
|
||||
choices = KonovaCode.objects.filter(
|
||||
code_lists__in=[CODELIST_BIOTOPES_ID],
|
||||
is_archived=False,
|
||||
is_leaf=True,
|
||||
).values_list("id", flat=True)
|
||||
choices = [
|
||||
(choice, choice)
|
||||
for choice in choices
|
||||
]
|
||||
self.fields["biotope_type"].choices = choices
|
||||
|
||||
def save(self, is_before_state: bool = False):
|
||||
state = self.instance.add_state(self, is_before_state)
|
||||
@ -270,8 +272,9 @@ class EditCompensationStateModalForm(NewStateModalForm):
|
||||
self.state = kwargs.pop("state", None)
|
||||
super().__init__(*args, **kwargs)
|
||||
self.form_title = _("Edit state")
|
||||
biotope_type_id = self.state.biotope_type.id if self.state.biotope_type else None
|
||||
form_data = {
|
||||
"biotope_type": self.state.biotope_type,
|
||||
"biotope_type": biotope_type_id,
|
||||
"biotope_extra": self.state.biotope_type_details.all(),
|
||||
"surface": self.state.surface,
|
||||
}
|
||||
@ -279,7 +282,8 @@ class EditCompensationStateModalForm(NewStateModalForm):
|
||||
|
||||
def save(self, is_before_state: bool = False):
|
||||
state = self.state
|
||||
state.biotope_type = self.cleaned_data.get("biotope_type", None)
|
||||
biotope_type_id = self.cleaned_data.get("biotope_type", None)
|
||||
state.biotope_type = KonovaCode.objects.get(id=biotope_type_id)
|
||||
state.biotope_type_details.set(self.cleaned_data.get("biotope_extra", []))
|
||||
state.surface = self.cleaned_data.get("surface", None)
|
||||
state.save()
|
||||
|
23
compensation/migrations/0007_auto_20220531_1245.py
Normal file
23
compensation/migrations/0007_auto_20220531_1245.py
Normal file
@ -0,0 +1,23 @@
|
||||
# Generated by Django 3.1.3 on 2022-05-31 10:45
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('compensation', '0006_ecoaccount_teams'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='compensation',
|
||||
name='is_pik',
|
||||
field=models.BooleanField(blank=True, default=False, help_text="Flag if compensation is a 'Produktonsintegrierte Kompensation'", null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='ecoaccount',
|
||||
name='is_pik',
|
||||
field=models.BooleanField(blank=True, default=False, help_text="Flag if compensation is a 'Produktonsintegrierte Kompensation'", null=True),
|
||||
),
|
||||
]
|
@ -8,6 +8,8 @@ Created on: 16.11.21
|
||||
import shutil
|
||||
|
||||
from django.contrib import messages
|
||||
|
||||
from codelist.models import KonovaCode
|
||||
from user.models import User, Team
|
||||
from django.db import models, transaction
|
||||
from django.db.models import QuerySet, Sum
|
||||
@ -142,8 +144,10 @@ class AbstractCompensation(BaseObject, GeoReferencedMixin):
|
||||
"""
|
||||
form_data = form.cleaned_data
|
||||
with transaction.atomic():
|
||||
biotope_type_id = form_data["biotope_type"]
|
||||
code = KonovaCode.objects.get(id=biotope_type_id)
|
||||
state = CompensationState.objects.create(
|
||||
biotope_type=form_data["biotope_type"],
|
||||
biotope_type=code,
|
||||
surface=form_data["surface"],
|
||||
)
|
||||
state_additional_types = form_data["biotope_extra"]
|
||||
@ -253,7 +257,22 @@ class CoherenceMixin(models.Model):
|
||||
abstract = True
|
||||
|
||||
|
||||
class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin):
|
||||
class PikMixin(models.Model):
|
||||
""" Provides PIK flag as Mixin
|
||||
|
||||
"""
|
||||
is_pik = models.BooleanField(
|
||||
blank=True,
|
||||
null=True,
|
||||
default=False,
|
||||
help_text="Flag if compensation is a 'Produktonsintegrierte Kompensation'"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin, PikMixin):
|
||||
"""
|
||||
Regular compensation, linked to an intervention
|
||||
"""
|
||||
@ -418,6 +437,18 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin):
|
||||
super().set_status_messages(request)
|
||||
return request
|
||||
|
||||
@property
|
||||
def is_recorded(self):
|
||||
""" Getter for record status as property
|
||||
|
||||
Since compensations inherit their record status from their intervention, the intervention's status is being
|
||||
returned
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
return self.intervention.is_recorded
|
||||
|
||||
|
||||
class CompensationDocument(AbstractDocument):
|
||||
"""
|
||||
|
@ -17,14 +17,13 @@ from django.db.models import Sum, QuerySet
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from compensation.managers import EcoAccountManager, EcoAccountDeductionManager
|
||||
from compensation.models.compensation import AbstractCompensation
|
||||
from compensation.models.compensation import AbstractCompensation, PikMixin
|
||||
from compensation.utils.quality import EcoAccountQualityChecker
|
||||
from konova.models import ShareableObjectMixin, RecordableObjectMixin, AbstractDocument, BaseResource, \
|
||||
generate_document_file_upload_path
|
||||
from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE
|
||||
|
||||
|
||||
class EcoAccount(AbstractCompensation, ShareableObjectMixin, RecordableObjectMixin):
|
||||
class EcoAccount(AbstractCompensation, ShareableObjectMixin, RecordableObjectMixin, PikMixin):
|
||||
"""
|
||||
An eco account is a kind of 'prepaid' compensation. It can be compared to an account that already has been filled
|
||||
with some kind of currency. From this account one is able to deduct currency for current projects.
|
||||
|
@ -5,17 +5,15 @@ Contact: michel.peltriaux@sgdnord.rlp.de
|
||||
Created on: 01.12.20
|
||||
|
||||
"""
|
||||
from user.models import User
|
||||
from konova.utils.message_templates import DATA_IS_UNCHECKED, DATA_CHECKED_ON_TEMPLATE, DATA_CHECKED_PREVIOUSLY_TEMPLATE
|
||||
from django.http import HttpRequest
|
||||
from django.template.loader import render_to_string
|
||||
from django.urls import reverse
|
||||
from django.utils.html import format_html
|
||||
from django.utils.timezone import localtime
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from compensation.filters import CompensationTableFilter, EcoAccountTableFilter
|
||||
from compensation.models import Compensation, EcoAccount
|
||||
from konova.sub_settings.django_settings import DEFAULT_DATE_TIME_FORMAT
|
||||
from konova.utils.tables import BaseTable, TableRenderMixin
|
||||
import django_tables2 as tables
|
||||
|
||||
@ -111,16 +109,21 @@ class CompensationTable(BaseTable, TableRenderMixin):
|
||||
"""
|
||||
html = ""
|
||||
checked = value is not None
|
||||
tooltip = _("Not checked yet")
|
||||
tooltip = DATA_IS_UNCHECKED
|
||||
previously_checked = record.intervention.get_last_checked_action()
|
||||
if checked:
|
||||
value = value.timestamp
|
||||
value = localtime(value)
|
||||
checked_on = value.strftime(DEFAULT_DATE_TIME_FORMAT)
|
||||
tooltip = _("Checked on {} by {}").format(checked_on, record.intervention.checked.user)
|
||||
checked_on = value.get_timestamp_str_formatted()
|
||||
tooltip = DATA_CHECKED_ON_TEMPLATE.format(checked_on, record.intervention.checked.user)
|
||||
html += self.render_checked_star(
|
||||
tooltip=tooltip,
|
||||
icn_filled=checked,
|
||||
)
|
||||
if previously_checked and not checked:
|
||||
checked_on = previously_checked.get_timestamp_str_formatted()
|
||||
tooltip = DATA_CHECKED_PREVIOUSLY_TEMPLATE.format(checked_on, previously_checked.user)
|
||||
html += self.render_previously_checked_star(
|
||||
tooltip=tooltip,
|
||||
)
|
||||
return format_html(html)
|
||||
|
||||
def render_d(self, value, record: Compensation):
|
||||
@ -134,7 +137,7 @@ class CompensationTable(BaseTable, TableRenderMixin):
|
||||
|
||||
"""
|
||||
parcels = value.get_underlying_parcels().values_list(
|
||||
"gmrkng",
|
||||
"parcel_group__name",
|
||||
flat=True
|
||||
).distinct()
|
||||
html = render_to_string(
|
||||
@ -159,9 +162,7 @@ class CompensationTable(BaseTable, TableRenderMixin):
|
||||
recorded = value is not None
|
||||
tooltip = _("Not recorded yet")
|
||||
if recorded:
|
||||
value = value.timestamp
|
||||
value = localtime(value)
|
||||
on = value.strftime(DEFAULT_DATE_TIME_FORMAT)
|
||||
on = value.get_timestamp_str_formatted()
|
||||
tooltip = _("Recorded on {} by {}").format(on, record.intervention.recorded.user)
|
||||
html += self.render_bookmark(
|
||||
tooltip=tooltip,
|
||||
@ -179,11 +180,7 @@ class CompensationTable(BaseTable, TableRenderMixin):
|
||||
Returns:
|
||||
|
||||
"""
|
||||
if value is None:
|
||||
value = User.objects.none()
|
||||
has_access = value.filter(
|
||||
id=self.user.id
|
||||
).exists()
|
||||
has_access = record.is_shared_with(self.user)
|
||||
|
||||
html = self.render_icn(
|
||||
tooltip=_("Full access granted") if has_access else _("Access not granted"),
|
||||
@ -295,7 +292,7 @@ class EcoAccountTable(BaseTable, TableRenderMixin):
|
||||
|
||||
"""
|
||||
parcels = value.get_underlying_parcels().values_list(
|
||||
"gmrkng",
|
||||
"parcel_group__name",
|
||||
flat=True
|
||||
).distinct()
|
||||
html = render_to_string(
|
||||
@ -320,9 +317,7 @@ class EcoAccountTable(BaseTable, TableRenderMixin):
|
||||
checked = value is not None
|
||||
tooltip = _("Not recorded yet. Can not be used for deductions, yet.")
|
||||
if checked:
|
||||
value = value.timestamp
|
||||
value = localtime(value)
|
||||
on = value.strftime(DEFAULT_DATE_TIME_FORMAT)
|
||||
on = value.get_timestamp_str_formatted()
|
||||
tooltip = _("Recorded on {} by {}").format(on, record.recorded.user)
|
||||
html += self.render_bookmark(
|
||||
tooltip=tooltip,
|
||||
@ -343,7 +338,7 @@ class EcoAccountTable(BaseTable, TableRenderMixin):
|
||||
html = ""
|
||||
# Do not use value in here, since value does use unprefetched 'users' manager, where record has already
|
||||
# prefetched users data
|
||||
has_access = self.user in record.users.all()
|
||||
has_access = record.is_shared_with(self.user)
|
||||
html += self.render_icn(
|
||||
tooltip=_("Full access granted") if has_access else _("Access not granted"),
|
||||
icn_class="fas fa-edit rlp-r-inv" if has_access else "far fa-edit",
|
||||
|
@ -39,6 +39,16 @@
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans 'Is PIK' %}</th>
|
||||
<td class="align-middle">
|
||||
{% if obj.is_pik %}
|
||||
{% trans 'Yes' %}
|
||||
{% else %}
|
||||
{% trans 'No' %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans 'Is CEF compensation' %}</th>
|
||||
<td class="align-middle">
|
||||
@ -66,6 +76,11 @@
|
||||
<span>
|
||||
{% fa5_icon 'star' 'far' %}
|
||||
</span>
|
||||
{% if last_checked %}
|
||||
<span class="rlp-gd-inv" title="{{last_checked_tooltip}}">
|
||||
{% fa5_icon 'star' 'fas' %}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="check-star" title="{% trans 'Checked on '%} {{obj.intervention.checked.timestamp}} {% trans 'by' %} {{obj.intervention.checked.user}}">
|
||||
{% fa5_icon 'star' %}
|
||||
@ -90,15 +105,21 @@
|
||||
<tr>
|
||||
<th scope="row">{% trans 'Last modified' %}</th>
|
||||
<td class="align-middle">
|
||||
{{obj.modified.timestamp|default_if_none:""|naturalday}}
|
||||
{% if obj.modified %}
|
||||
{{obj.modified.timestamp|default_if_none:""}}
|
||||
<br>
|
||||
{{obj.modified.user.username}}
|
||||
{% else %}
|
||||
{{obj.created.timestamp|default_if_none:""}}
|
||||
<br>
|
||||
{{obj.created.user.username}}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans 'Shared with' %}</th>
|
||||
<td class="align-middle">
|
||||
{% for team in obj.intervention.teams.all %}
|
||||
{% for team in obj.intervention.shared_teams %}
|
||||
{% include 'user/includes/team_data_modal_button.html' %}
|
||||
{% endfor %}
|
||||
<hr>
|
||||
@ -113,10 +134,12 @@
|
||||
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
|
||||
<div class="col">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
{% include 'map/geom_form.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
{% include 'konova/includes/parcels.html' %}
|
||||
{% include 'konova/includes/parcels/parcels.html' %}
|
||||
</div>
|
||||
<div class="row">
|
||||
{% include 'konova/includes/comment_card.html' %}
|
||||
|
@ -70,18 +70,34 @@
|
||||
<th scope="row">{% trans 'Action handler' %}</th>
|
||||
<td class="align-middle">{{obj.responsible.handler|default_if_none:""}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans 'Is PIK' %}</th>
|
||||
<td class="align-middle">
|
||||
{% if obj.is_pik %}
|
||||
{% trans 'Yes' %}
|
||||
{% else %}
|
||||
{% trans 'No' %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans 'Last modified' %}</th>
|
||||
<td class="align-middle">
|
||||
{{obj.modified.timestamp|default_if_none:""|naturalday}}
|
||||
{% if obj.modified %}
|
||||
{{obj.modified.timestamp|default_if_none:""}}
|
||||
<br>
|
||||
{{obj.modified.user.username}}
|
||||
{% else %}
|
||||
{{obj.created.timestamp|default_if_none:""}}
|
||||
<br>
|
||||
{{obj.created.user.username}}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans 'Shared with' %}</th>
|
||||
<td class="align-middle">
|
||||
{% for team in obj.teams.all %}
|
||||
{% for team in obj.shared_teams %}
|
||||
{% include 'user/includes/team_data_modal_button.html' %}
|
||||
{% endfor %}
|
||||
<hr>
|
||||
@ -95,10 +111,12 @@
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
{% include 'map/geom_form.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
{% include 'konova/includes/parcels.html' %}
|
||||
{% include 'konova/includes/parcels/parcels.html' %}
|
||||
</div>
|
||||
<div class="row">
|
||||
{% include 'konova/includes/comment_card.html' %}
|
||||
|
@ -20,6 +20,36 @@
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans 'Is PIK' %}</th>
|
||||
<td class="align-middle">
|
||||
{% if obj.is_pik %}
|
||||
{% trans 'Yes' %}
|
||||
{% else %}
|
||||
{% trans 'No' %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans 'Is CEF' %}</th>
|
||||
<td class="align-middle">
|
||||
{% if obj.is_cef %}
|
||||
{% trans 'Yes' %}
|
||||
{% else %}
|
||||
{% trans 'No' %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans 'Is coherence keeping' %}</th>
|
||||
<td class="align-middle">
|
||||
{% if obj.is_coherence_keeping %}
|
||||
{% trans 'Yes' %}
|
||||
{% else %}
|
||||
{% trans 'No' %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans 'Last modified' %}</th>
|
||||
<td class="align-middle">
|
||||
@ -35,20 +65,15 @@
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
{% include 'map/geom_form.html' %}
|
||||
</div>
|
||||
<div class="row">
|
||||
{% include 'konova/includes/parcels.html' %}
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-6 col-md-6 col-lg-6">
|
||||
<h4>{% trans 'Open in browser' %}</h4>
|
||||
{{ qrcode|safe }}
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-6 col-lg-6">
|
||||
<h4>{% trans 'View in LANIS' %}</h4>
|
||||
{{ qrcode_lanis|safe }}
|
||||
{% include 'konova/includes/parcels/parcels.html' %}
|
||||
</div>
|
||||
<div class="row">
|
||||
{% include 'konova/includes/report/qrcodes.html' %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
@ -20,6 +20,16 @@
|
||||
<th scope="row">{% trans 'Conservation office file number' %}</th>
|
||||
<td class="align-middle">{{obj.responsible.conservation_file_number|default_if_none:""}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans 'Is PIK' %}</th>
|
||||
<td class="align-middle">
|
||||
{% if obj.is_pik %}
|
||||
{% trans 'Yes' %}
|
||||
{% else %}
|
||||
{% trans 'No' %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans 'Deductions for' %}</th>
|
||||
<td class="align-middle">
|
||||
@ -48,20 +58,15 @@
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
{% include 'map/geom_form.html' %}
|
||||
</div>
|
||||
<div class="row">
|
||||
{% include 'konova/includes/parcels.html' %}
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-6 col-md-6 col-lg-6">
|
||||
<h4>{% trans 'Open in browser' %}</h4>
|
||||
{{ qrcode|safe }}
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-6 col-lg-6">
|
||||
<h4>{% trans 'View in LANIS' %}</h4>
|
||||
{{ qrcode_lanis|safe }}
|
||||
{% include 'konova/includes/parcels/parcels.html' %}
|
||||
</div>
|
||||
<div class="row">
|
||||
{% include 'konova/includes/report/qrcodes.html' %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
@ -50,18 +50,20 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
|
||||
test_id = self.create_dummy_string()
|
||||
test_title = self.create_dummy_string()
|
||||
test_geom = self.create_dummy_geometry()
|
||||
geom_json = self.create_geojson(test_geom)
|
||||
post_data = {
|
||||
"identifier": test_id,
|
||||
"title": test_title,
|
||||
"geom": test_geom.geojson,
|
||||
"geom": geom_json,
|
||||
"intervention": self.intervention.id,
|
||||
}
|
||||
pre_creation_intervention_log_count = self.intervention.log.count()
|
||||
|
||||
# Preserve the current number of intervention's compensations
|
||||
num_compensations = self.intervention.compensations.count()
|
||||
self.client_user.post(new_url, post_data)
|
||||
response = self.client_user.post(new_url, post_data)
|
||||
|
||||
self.assertEqual(302, response.status_code)
|
||||
self.intervention.refresh_from_db()
|
||||
self.assertEqual(num_compensations + 1, self.intervention.compensations.count())
|
||||
new_compensation = self.intervention.compensations.get(identifier=test_id)
|
||||
@ -87,10 +89,11 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
|
||||
test_id = self.create_dummy_string()
|
||||
test_title = self.create_dummy_string()
|
||||
test_geom = self.create_dummy_geometry()
|
||||
geom_json = self.create_geojson(test_geom)
|
||||
post_data = {
|
||||
"identifier": test_id,
|
||||
"title": test_title,
|
||||
"geom": test_geom.geojson,
|
||||
"geom": geom_json,
|
||||
}
|
||||
pre_creation_intervention_log_count = self.intervention.log.count()
|
||||
|
||||
@ -125,6 +128,7 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
|
||||
new_identifier = self.create_dummy_string()
|
||||
new_comment = self.create_dummy_string()
|
||||
new_geometry = MultiPolygon(srid=4326) # Create an empty geometry
|
||||
geojson = self.create_geojson(new_geometry)
|
||||
|
||||
check_on_elements = {
|
||||
self.compensation.title: new_title,
|
||||
@ -139,7 +143,7 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
|
||||
"title": new_title,
|
||||
"intervention": self.intervention.id, # just keep the intervention as it is
|
||||
"comment": new_comment,
|
||||
"geom": new_geometry.geojson,
|
||||
"geom": geojson,
|
||||
}
|
||||
self.client_user.post(url, post_data)
|
||||
self.compensation.refresh_from_db()
|
||||
@ -261,3 +265,26 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
|
||||
self.assertIn(recorded, self.compensation.log.all())
|
||||
self.assertEqual(pre_record_log_count + 1, self.compensation.log.count())
|
||||
|
||||
def test_non_editable_after_recording(self):
|
||||
""" Tests that the compensation can not be edited after being recorded
|
||||
|
||||
User must be redirected to another page
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
self.assertIsNotNone(self.compensation)
|
||||
self.assertFalse(self.compensation.is_recorded)
|
||||
edit_url = reverse("compensation:edit", args=(self.compensation.id,))
|
||||
response = self.client_user.get(edit_url)
|
||||
has_redirect = response.status_code == 302
|
||||
self.assertFalse(has_redirect)
|
||||
|
||||
self.compensation.intervention.set_recorded(self.user)
|
||||
self.assertTrue(self.compensation.is_recorded)
|
||||
|
||||
edit_url = reverse("compensation:edit", args=(self.compensation.id,))
|
||||
response = self.client_user.get(edit_url)
|
||||
has_redirect = response.status_code == 302
|
||||
self.assertTrue(has_redirect)
|
||||
self.compensation.intervention.set_unrecorded(self.user)
|
||||
|
@ -40,12 +40,13 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
|
||||
test_id = self.create_dummy_string()
|
||||
test_title = self.create_dummy_string()
|
||||
test_geom = self.create_dummy_geometry()
|
||||
geom_json = self.create_geojson(test_geom)
|
||||
test_deductable_surface = 1000
|
||||
test_conservation_office = self.get_conservation_office_code()
|
||||
post_data = {
|
||||
"identifier": test_id,
|
||||
"title": test_title,
|
||||
"geom": test_geom.geojson,
|
||||
"geom": geom_json,
|
||||
"deductable_surface": test_deductable_surface,
|
||||
"conservation_office": test_conservation_office.id
|
||||
}
|
||||
@ -302,3 +303,27 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
|
||||
self.assertEqual(pre_edit_account_log_count + 1, account.log.count())
|
||||
self.assertEqual(intervention.log.first().action, UserAction.EDITED)
|
||||
self.assertEqual(account.log.first().action, UserAction.EDITED)
|
||||
|
||||
def test_non_editable_after_recording(self):
|
||||
""" Tests that the eco_account can not be edited after being recorded
|
||||
|
||||
User must be redirected to another page
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
self.assertIsNotNone(self.eco_account)
|
||||
self.assertFalse(self.eco_account.is_recorded)
|
||||
edit_url = reverse("compensation:acc:edit", args=(self.eco_account.id,))
|
||||
response = self.client_user.get(edit_url)
|
||||
has_redirect = response.status_code == 302
|
||||
self.assertFalse(has_redirect)
|
||||
|
||||
self.eco_account.set_recorded(self.user)
|
||||
self.assertTrue(self.eco_account.is_recorded)
|
||||
|
||||
edit_url = reverse("compensation:acc:edit", args=(self.eco_account.id,))
|
||||
response = self.client_user.get(edit_url)
|
||||
has_redirect = response.status_code == 302
|
||||
self.assertTrue(has_redirect)
|
||||
self.eco_account.set_unrecorded(self.user)
|
||||
|
@ -1,4 +1,5 @@
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db.models import Sum
|
||||
from django.http import HttpRequest, JsonResponse
|
||||
from django.shortcuts import render
|
||||
@ -22,7 +23,7 @@ from konova.utils.message_templates import FORM_INVALID, IDENTIFIER_REPLACED, DA
|
||||
CHECKED_RECORDED_RESET, COMPENSATION_ADDED_TEMPLATE, COMPENSATION_REMOVED_TEMPLATE, DOCUMENT_ADDED, \
|
||||
COMPENSATION_STATE_REMOVED, COMPENSATION_STATE_ADDED, COMPENSATION_ACTION_REMOVED, COMPENSATION_ACTION_ADDED, \
|
||||
DEADLINE_ADDED, DEADLINE_REMOVED, DOCUMENT_EDITED, COMPENSATION_STATE_EDITED, COMPENSATION_ACTION_EDITED, \
|
||||
DEADLINE_EDITED
|
||||
DEADLINE_EDITED, RECORDED_BLOCKS_EDIT, PARAMS_INVALID, DATA_CHECKED_PREVIOUSLY_TEMPLATE
|
||||
from konova.utils.user_checks import in_group
|
||||
|
||||
|
||||
@ -69,6 +70,19 @@ def new_view(request: HttpRequest, intervention_id: str = None):
|
||||
|
||||
"""
|
||||
template = "compensation/form/view.html"
|
||||
if intervention_id is not None:
|
||||
try:
|
||||
intervention = Intervention.objects.get(id=intervention_id)
|
||||
except ObjectDoesNotExist:
|
||||
messages.error(request, PARAMS_INVALID)
|
||||
return redirect("home")
|
||||
if intervention.is_recorded:
|
||||
messages.info(
|
||||
request,
|
||||
RECORDED_BLOCKS_EDIT
|
||||
)
|
||||
return redirect("intervention:detail", id=intervention_id)
|
||||
|
||||
data_form = NewCompensationForm(request.POST or None, intervention_id=intervention_id)
|
||||
geom_form = SimpleGeomForm(request.POST or None, read_only=False)
|
||||
if request.method == "POST":
|
||||
@ -134,6 +148,13 @@ def edit_view(request: HttpRequest, id: str):
|
||||
template = "compensation/form/view.html"
|
||||
# Get object from db
|
||||
comp = get_object_or_404(Compensation, id=id)
|
||||
if comp.is_recorded:
|
||||
messages.info(
|
||||
request,
|
||||
RECORDED_BLOCKS_EDIT
|
||||
)
|
||||
return redirect("compensation:detail", id=id)
|
||||
|
||||
# Create forms, initialize with values from db/from POST request
|
||||
data_form = EditCompensationForm(request.POST or None, instance=comp)
|
||||
geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=comp)
|
||||
@ -196,8 +217,15 @@ def detail_view(request: HttpRequest, id: str):
|
||||
|
||||
request = comp.set_status_messages(request)
|
||||
|
||||
last_checked = comp.intervention.get_last_checked_action()
|
||||
last_checked_tooltip = ""
|
||||
if last_checked:
|
||||
last_checked_tooltip = DATA_CHECKED_PREVIOUSLY_TEMPLATE.format(last_checked.get_timestamp_str_formatted(), last_checked.user)
|
||||
|
||||
context = {
|
||||
"obj": comp,
|
||||
"last_checked": last_checked,
|
||||
"last_checked_tooltip": last_checked_tooltip,
|
||||
"geom_form": geom_form,
|
||||
"parcels": parcels,
|
||||
"has_access": is_data_shared,
|
||||
@ -596,14 +624,12 @@ def report_view(request: HttpRequest, id: str):
|
||||
instance=comp
|
||||
)
|
||||
parcels = comp.get_underlying_parcels()
|
||||
qrcode_img = generate_qr_code(
|
||||
request.build_absolute_uri(reverse("compensation:report", args=(id,))),
|
||||
10
|
||||
)
|
||||
qrcode_img_lanis = generate_qr_code(
|
||||
comp.get_LANIS_link(),
|
||||
7
|
||||
)
|
||||
|
||||
qrcode_url = request.build_absolute_uri(reverse("compensation:report", args=(id,)))
|
||||
qrcode_img = generate_qr_code(qrcode_url, 10)
|
||||
qrcode_lanis_url = comp.get_LANIS_link()
|
||||
qrcode_img_lanis = generate_qr_code(qrcode_lanis_url, 7)
|
||||
|
||||
# Order states by surface
|
||||
before_states = comp.before_states.all().order_by("-surface").prefetch_related("biotope_type")
|
||||
after_states = comp.after_states.all().order_by("-surface").prefetch_related("biotope_type")
|
||||
@ -611,8 +637,14 @@ def report_view(request: HttpRequest, id: str):
|
||||
|
||||
context = {
|
||||
"obj": comp,
|
||||
"qrcode": qrcode_img,
|
||||
"qrcode_lanis": qrcode_img_lanis,
|
||||
"qrcode": {
|
||||
"img": qrcode_img,
|
||||
"url": qrcode_url,
|
||||
},
|
||||
"qrcode_lanis": {
|
||||
"img": qrcode_img_lanis,
|
||||
"url": qrcode_lanis_url,
|
||||
},
|
||||
"has_access": False, # disables action buttons during rendering
|
||||
"before_states": before_states,
|
||||
"after_states": after_states,
|
||||
|
@ -35,7 +35,8 @@ from konova.utils.generators import generate_qr_code
|
||||
from konova.utils.message_templates import IDENTIFIER_REPLACED, FORM_INVALID, DATA_UNSHARED, DATA_UNSHARED_EXPLANATION, \
|
||||
CANCEL_ACC_RECORDED_OR_DEDUCTED, DEDUCTION_REMOVED, DEDUCTION_ADDED, DOCUMENT_ADDED, COMPENSATION_STATE_REMOVED, \
|
||||
COMPENSATION_STATE_ADDED, COMPENSATION_ACTION_REMOVED, COMPENSATION_ACTION_ADDED, DEADLINE_ADDED, DEADLINE_REMOVED, \
|
||||
DEDUCTION_EDITED, DOCUMENT_EDITED, COMPENSATION_STATE_EDITED, COMPENSATION_ACTION_EDITED, DEADLINE_EDITED
|
||||
DEDUCTION_EDITED, DOCUMENT_EDITED, COMPENSATION_STATE_EDITED, COMPENSATION_ACTION_EDITED, DEADLINE_EDITED, \
|
||||
RECORDED_BLOCKS_EDIT
|
||||
from konova.utils.user_checks import in_group
|
||||
|
||||
|
||||
@ -145,6 +146,13 @@ def edit_view(request: HttpRequest, id: str):
|
||||
template = "compensation/form/view.html"
|
||||
# Get object from db
|
||||
acc = get_object_or_404(EcoAccount, id=id)
|
||||
if acc.is_recorded:
|
||||
messages.info(
|
||||
request,
|
||||
RECORDED_BLOCKS_EDIT
|
||||
)
|
||||
return redirect("compensation:acc:detail", id=id)
|
||||
|
||||
# Create forms, initialize with values from db/from POST request
|
||||
data_form = EditEcoAccountForm(request.POST or None, instance=acc)
|
||||
geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=acc)
|
||||
@ -731,18 +739,16 @@ def report_view(request:HttpRequest, id: str):
|
||||
instance=acc
|
||||
)
|
||||
parcels = acc.get_underlying_parcels()
|
||||
qrcode_img = generate_qr_code(
|
||||
request.build_absolute_uri(reverse("ema:report", args=(id,))),
|
||||
10
|
||||
)
|
||||
qrcode_img_lanis = generate_qr_code(
|
||||
acc.get_LANIS_link(),
|
||||
7
|
||||
)
|
||||
|
||||
qrcode_url = request.build_absolute_uri(reverse("ema:report", args=(id,)))
|
||||
qrcode_img = generate_qr_code(qrcode_url, 10)
|
||||
qrcode_lanis_url = acc.get_LANIS_link()
|
||||
qrcode_img_lanis = generate_qr_code(qrcode_lanis_url, 7)
|
||||
|
||||
# Order states by surface
|
||||
before_states = acc.before_states.all().order_by("-surface").select_related("biotope_type__parent")
|
||||
after_states = acc.after_states.all().order_by("-surface").select_related("biotope_type__parent")
|
||||
actions = acc.actions.all().select_related("action_type__parent")
|
||||
actions = acc.actions.all().prefetch_related("action_type__parent")
|
||||
|
||||
# Reduce amount of db fetched data to the bare minimum we need in the template (deduction's intervention id and identifier)
|
||||
deductions = acc.deductions.all()\
|
||||
@ -752,8 +758,14 @@ def report_view(request:HttpRequest, id: str):
|
||||
|
||||
context = {
|
||||
"obj": acc,
|
||||
"qrcode": qrcode_img,
|
||||
"qrcode_lanis": qrcode_img_lanis,
|
||||
"qrcode": {
|
||||
"img": qrcode_img,
|
||||
"url": qrcode_url,
|
||||
},
|
||||
"qrcode_lanis": {
|
||||
"img": qrcode_img_lanis,
|
||||
"url": qrcode_lanis_url,
|
||||
},
|
||||
"has_access": False, # disables action buttons during rendering
|
||||
"before_states": before_states,
|
||||
"after_states": after_states,
|
||||
|
11
ema/forms.py
11
ema/forms.py
@ -12,14 +12,15 @@ from django.db import transaction
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from compensation.forms.forms import AbstractCompensationForm, CompensationResponsibleFormMixin
|
||||
from compensation.forms.forms import AbstractCompensationForm, CompensationResponsibleFormMixin, \
|
||||
PikCompensationFormMixin
|
||||
from ema.models import Ema, EmaDocument
|
||||
from intervention.models import Responsibility, Handler
|
||||
from konova.forms import SimpleGeomForm, NewDocumentModalForm
|
||||
from user.models import UserActionLogEntry
|
||||
|
||||
|
||||
class NewEmaForm(AbstractCompensationForm, CompensationResponsibleFormMixin):
|
||||
class NewEmaForm(AbstractCompensationForm, CompensationResponsibleFormMixin, PikCompensationFormMixin):
|
||||
""" Form for creating new EMA objects.
|
||||
|
||||
Inherits basic form fields from AbstractCompensationForm and additional from CompensationResponsibleFormMixin.
|
||||
@ -31,6 +32,7 @@ class NewEmaForm(AbstractCompensationForm, CompensationResponsibleFormMixin):
|
||||
"title",
|
||||
"conservation_office",
|
||||
"conservation_file_number",
|
||||
"is_pik",
|
||||
"handler_type",
|
||||
"handler_detail",
|
||||
"comment",
|
||||
@ -58,6 +60,7 @@ class NewEmaForm(AbstractCompensationForm, CompensationResponsibleFormMixin):
|
||||
handler_detail = self.cleaned_data.get("handler_detail", None)
|
||||
conservation_office = self.cleaned_data.get("conservation_office", None)
|
||||
conservation_file_number = self.cleaned_data.get("conservation_file_number", None)
|
||||
is_pik = self.cleaned_data.get("is_pik", None)
|
||||
comment = self.cleaned_data.get("comment", None)
|
||||
|
||||
# Create log entry
|
||||
@ -83,6 +86,7 @@ class NewEmaForm(AbstractCompensationForm, CompensationResponsibleFormMixin):
|
||||
created=action,
|
||||
geometry=geometry,
|
||||
comment=comment,
|
||||
is_pik=is_pik,
|
||||
)
|
||||
|
||||
# Add the creating user to the list of shared users
|
||||
@ -116,6 +120,7 @@ class EditEmaForm(NewEmaForm):
|
||||
"conservation_office": self.instance.responsible.conservation_office,
|
||||
"conservation_file_number": self.instance.responsible.conservation_file_number,
|
||||
"comment": self.instance.comment,
|
||||
"is_pik": self.instance.is_pik,
|
||||
}
|
||||
disabled_fields = []
|
||||
self.load_initial_data(
|
||||
@ -133,6 +138,7 @@ class EditEmaForm(NewEmaForm):
|
||||
conservation_office = self.cleaned_data.get("conservation_office", None)
|
||||
conservation_file_number = self.cleaned_data.get("conservation_file_number", None)
|
||||
comment = self.cleaned_data.get("comment", None)
|
||||
is_pik = self.cleaned_data.get("is_pik", None)
|
||||
|
||||
# Create log entry
|
||||
action = UserActionLogEntry.get_edited_action(user)
|
||||
@ -152,6 +158,7 @@ class EditEmaForm(NewEmaForm):
|
||||
self.instance.title = title
|
||||
self.instance.geometry = geometry
|
||||
self.instance.comment = comment
|
||||
self.instance.is_pik = is_pik
|
||||
self.instance.modified = action
|
||||
self.instance.save()
|
||||
|
||||
|
18
ema/migrations/0004_ema_is_pik.py
Normal file
18
ema/migrations/0004_ema_is_pik.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1.3 on 2022-05-31 10:45
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ema', '0003_ema_teams'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='ema',
|
||||
name='is_pik',
|
||||
field=models.BooleanField(blank=True, default=False, help_text="Flag if compensation is a 'Produktonsintegrierte Kompensation'", null=True),
|
||||
),
|
||||
]
|
@ -13,15 +13,14 @@ from django.db.models import QuerySet
|
||||
from django.http import HttpRequest
|
||||
from django.urls import reverse
|
||||
|
||||
from compensation.models import AbstractCompensation
|
||||
from compensation.models import AbstractCompensation, PikMixin
|
||||
from ema.managers import EmaManager
|
||||
from ema.utils.quality import EmaQualityChecker
|
||||
from konova.models import AbstractDocument, generate_document_file_upload_path, RecordableObjectMixin, ShareableObjectMixin
|
||||
from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE
|
||||
from konova.utils.message_templates import DATA_UNSHARED_EXPLANATION, DOCUMENT_REMOVED_TEMPLATE
|
||||
|
||||
|
||||
class Ema(AbstractCompensation, ShareableObjectMixin, RecordableObjectMixin):
|
||||
class Ema(AbstractCompensation, ShareableObjectMixin, RecordableObjectMixin, PikMixin):
|
||||
"""
|
||||
EMA = Ersatzzahlungsmaßnahme
|
||||
(compensation actions from payments)
|
||||
|
@ -104,7 +104,7 @@ class EmaTable(BaseTable, TableRenderMixin):
|
||||
|
||||
"""
|
||||
parcels = value.get_underlying_parcels().values_list(
|
||||
"gmrkng",
|
||||
"parcel_group__name",
|
||||
flat=True
|
||||
).distinct()
|
||||
html = render_to_string(
|
||||
@ -115,7 +115,6 @@ class EmaTable(BaseTable, TableRenderMixin):
|
||||
)
|
||||
return html
|
||||
|
||||
|
||||
def render_r(self, value, record: Ema):
|
||||
""" Renders the registered column for a EMA
|
||||
|
||||
@ -130,9 +129,7 @@ class EmaTable(BaseTable, TableRenderMixin):
|
||||
recorded = value is not None
|
||||
tooltip = _("Not recorded yet")
|
||||
if recorded:
|
||||
value = value.timestamp
|
||||
value = localtime(value)
|
||||
on = value.strftime(DEFAULT_DATE_TIME_FORMAT)
|
||||
on = value.get_timestamp_str_formatted()
|
||||
tooltip = _("Recorded on {} by {}").format(on, record.recorded.user)
|
||||
html += self.render_bookmark(
|
||||
tooltip=tooltip,
|
||||
@ -151,9 +148,7 @@ class EmaTable(BaseTable, TableRenderMixin):
|
||||
|
||||
"""
|
||||
html = ""
|
||||
has_access = value.filter(
|
||||
id=self.user.id
|
||||
).exists()
|
||||
has_access = record.is_shared_with(self.user)
|
||||
|
||||
html += self.render_icn(
|
||||
tooltip=_("Full access granted") if has_access else _("Access not granted"),
|
||||
|
@ -56,29 +56,38 @@
|
||||
<th scope="row">{% trans 'Action handler' %}</th>
|
||||
<td class="align-middle">{{obj.responsible.handler|default_if_none:""}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans 'Is PIK' %}</th>
|
||||
<td class="align-middle">
|
||||
{% if obj.is_pik %}
|
||||
{% trans 'Yes' %}
|
||||
{% else %}
|
||||
{% trans 'No' %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans 'Last modified' %}</th>
|
||||
<td class="align-middle">
|
||||
{% if obj.modified %}
|
||||
{{obj.modified.timestamp|default_if_none:""|naturalday}}
|
||||
{{obj.modified.timestamp|default_if_none:""}}
|
||||
<br>
|
||||
{{obj.modified.user.username}}
|
||||
{% else %}
|
||||
{{obj.created.timestamp|default_if_none:""|naturalday}}
|
||||
{{obj.created.timestamp|default_if_none:""}}
|
||||
<br>
|
||||
{{obj.created.user.username}}
|
||||
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans 'Shared with' %}</th>
|
||||
<td class="align-middle">
|
||||
{% for team in obj.teams.all %}
|
||||
{% for team in obj.shared_teams %}
|
||||
{% include 'user/includes/team_data_modal_button.html' %}
|
||||
{% endfor %}
|
||||
<hr>
|
||||
{% for user in obj.users.all %}
|
||||
{% for user in obj.user.all %}
|
||||
{% include 'user/includes/contact_modal_button.html' %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
@ -88,10 +97,12 @@
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
{% include 'map/geom_form.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
{% include 'konova/includes/parcels.html' %}
|
||||
{% include 'konova/includes/parcels/parcels.html' %}
|
||||
</div>
|
||||
<div class="row">
|
||||
{% include 'konova/includes/comment_card.html' %}
|
||||
|
@ -20,6 +20,16 @@
|
||||
<th scope="row">{% trans 'Conservation office file number' %}</th>
|
||||
<td class="align-middle">{{obj.responsible.conservation_file_number|default_if_none:""}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans 'Is PIK' %}</th>
|
||||
<td class="align-middle">
|
||||
{% if obj.is_pik %}
|
||||
{% trans 'Yes' %}
|
||||
{% else %}
|
||||
{% trans 'No' %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans 'Last modified' %}</th>
|
||||
<td class="align-middle">
|
||||
@ -35,20 +45,15 @@
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
{% include 'map/geom_form.html' %}
|
||||
</div>
|
||||
<div class="row">
|
||||
{% include 'konova/includes/parcels.html' %}
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-6 col-md-6 col-lg-6">
|
||||
<h4>{% trans 'Open in browser' %}</h4>
|
||||
{{ qrcode|safe }}
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-6 col-lg-6">
|
||||
<h4>{% trans 'View in LANIS' %}</h4>
|
||||
{{ qrcode_lanis|safe }}
|
||||
{% include 'konova/includes/parcels/parcels.html' %}
|
||||
</div>
|
||||
<div class="row">
|
||||
{% include 'konova/includes/report/qrcodes.html' %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
@ -41,11 +41,12 @@ class EmaWorkflowTestCase(BaseWorkflowTestCase):
|
||||
test_id = self.create_dummy_string()
|
||||
test_title = self.create_dummy_string()
|
||||
test_geom = self.create_dummy_geometry()
|
||||
geom_json = self.create_geojson(test_geom)
|
||||
test_conservation_office = self.get_conservation_office_code()
|
||||
post_data = {
|
||||
"identifier": test_id,
|
||||
"title": test_title,
|
||||
"geom": test_geom.geojson,
|
||||
"geom": geom_json,
|
||||
"conservation_office": test_conservation_office.id
|
||||
}
|
||||
self.client_user.post(new_url, post_data)
|
||||
@ -117,6 +118,32 @@ class EmaWorkflowTestCase(BaseWorkflowTestCase):
|
||||
self.assertEqual(pre_edit_log_count + 1, self.ema.log.count())
|
||||
self.assertEqual(self.ema.log.first().action, UserAction.EDITED)
|
||||
|
||||
def test_non_editable_after_recording(self):
|
||||
""" Tests that the EMA can not be edited after being recorded
|
||||
|
||||
User must be redirected to another page
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
self.superuser.groups.add(self.groups.get(name=ETS_GROUP))
|
||||
self.assertIsNotNone(self.ema)
|
||||
self.ema.share_with_user(self.superuser)
|
||||
self.assertFalse(self.ema.is_recorded)
|
||||
edit_url = reverse("ema:edit", args=(self.ema.id,))
|
||||
response = self.client_user.get(edit_url)
|
||||
has_redirect = response.status_code == 302
|
||||
self.assertFalse(has_redirect)
|
||||
|
||||
self.ema.set_recorded(self.superuser)
|
||||
self.assertTrue(self.ema.is_recorded)
|
||||
|
||||
edit_url = reverse("ema:edit", args=(self.ema.id,))
|
||||
response = self.client_user.get(edit_url)
|
||||
has_redirect = response.status_code == 302
|
||||
self.assertTrue(has_redirect)
|
||||
self.ema.set_unrecorded(self.superuser)
|
||||
|
||||
def test_recordability(self):
|
||||
"""
|
||||
This tests if the recordability of the Ema is triggered by the quality of it's data (e.g. not all fields filled)
|
||||
|
33
ema/views.py
33
ema/views.py
@ -26,7 +26,7 @@ from konova.utils.generators import generate_qr_code
|
||||
from konova.utils.message_templates import IDENTIFIER_REPLACED, FORM_INVALID, DATA_UNSHARED, DATA_UNSHARED_EXPLANATION, \
|
||||
DOCUMENT_ADDED, COMPENSATION_STATE_REMOVED, COMPENSATION_STATE_ADDED, COMPENSATION_ACTION_REMOVED, \
|
||||
COMPENSATION_ACTION_ADDED, DEADLINE_ADDED, DEADLINE_REMOVED, DOCUMENT_EDITED, COMPENSATION_STATE_EDITED, \
|
||||
COMPENSATION_ACTION_EDITED, DEADLINE_EDITED
|
||||
COMPENSATION_ACTION_EDITED, DEADLINE_EDITED, RECORDED_BLOCKS_EDIT
|
||||
from konova.utils.user_checks import in_group
|
||||
|
||||
|
||||
@ -213,6 +213,13 @@ def edit_view(request: HttpRequest, id: str):
|
||||
template = "compensation/form/view.html"
|
||||
# Get object from db
|
||||
ema = get_object_or_404(Ema, id=id)
|
||||
if ema.is_recorded:
|
||||
messages.info(
|
||||
request,
|
||||
RECORDED_BLOCKS_EDIT
|
||||
)
|
||||
return redirect("ema:detail", id=id)
|
||||
|
||||
# Create forms, initialize with values from db/from POST request
|
||||
data_form = EditEmaForm(request.POST or None, instance=ema)
|
||||
geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=ema)
|
||||
@ -563,14 +570,12 @@ def report_view(request:HttpRequest, id: str):
|
||||
instance=ema,
|
||||
)
|
||||
parcels = ema.get_underlying_parcels()
|
||||
qrcode_img = generate_qr_code(
|
||||
request.build_absolute_uri(reverse("ema:report", args=(id,))),
|
||||
10
|
||||
)
|
||||
qrcode_img_lanis = generate_qr_code(
|
||||
ema.get_LANIS_link(),
|
||||
7
|
||||
)
|
||||
|
||||
qrcode_url = request.build_absolute_uri(reverse("ema:report", args=(id,)))
|
||||
qrcode_img = generate_qr_code(qrcode_url, 10)
|
||||
qrcode_lanis_url = ema.get_LANIS_link()
|
||||
qrcode_img_lanis = generate_qr_code(qrcode_lanis_url, 7)
|
||||
|
||||
# Order states by surface
|
||||
before_states = ema.before_states.all().order_by("-surface").prefetch_related("biotope_type")
|
||||
after_states = ema.after_states.all().order_by("-surface").prefetch_related("biotope_type")
|
||||
@ -578,8 +583,14 @@ def report_view(request:HttpRequest, id: str):
|
||||
|
||||
context = {
|
||||
"obj": ema,
|
||||
"qrcode": qrcode_img,
|
||||
"qrcode_lanis": qrcode_img_lanis,
|
||||
"qrcode": {
|
||||
"img": qrcode_img,
|
||||
"url": qrcode_url
|
||||
},
|
||||
"qrcode_lanis": {
|
||||
"img": qrcode_img_lanis,
|
||||
"url": qrcode_lanis_url
|
||||
},
|
||||
"has_access": False, # disables action buttons during rendering
|
||||
"before_states": before_states,
|
||||
"after_states": after_states,
|
||||
|
@ -25,12 +25,14 @@ class InterventionAdmin(BaseObjectAdmin):
|
||||
"checked",
|
||||
"recorded",
|
||||
"users",
|
||||
"geometry",
|
||||
]
|
||||
|
||||
def get_readonly_fields(self, request, obj=None):
|
||||
return super().get_readonly_fields(request, obj) + [
|
||||
"checked",
|
||||
"recorded",
|
||||
"geometry",
|
||||
]
|
||||
|
||||
|
||||
|
@ -216,6 +216,10 @@ class NewInterventionForm(BaseForm):
|
||||
identifier = tmp_intervention.generate_new_identifier()
|
||||
self.initialize_form_field("identifier", identifier)
|
||||
|
||||
def is_valid(self):
|
||||
super_valid_result = super().is_valid()
|
||||
return super_valid_result
|
||||
|
||||
def save(self, user: User, geom_form: SimpleGeomForm):
|
||||
with transaction.atomic():
|
||||
# Fetch data from cleaned POST values
|
||||
|
@ -427,13 +427,22 @@ class NewDeductionModalForm(BaseModalForm):
|
||||
"""
|
||||
super_result = super().is_valid()
|
||||
acc = self.cleaned_data["account"]
|
||||
intervention = self.cleaned_data["intervention"]
|
||||
objects_valid = True
|
||||
|
||||
if not acc.recorded:
|
||||
self.add_error(
|
||||
"account",
|
||||
_("Eco-account {} is not recorded yet. You can only deduct from recorded accounts.").format(acc.identifier)
|
||||
)
|
||||
return False
|
||||
objects_valid = False
|
||||
|
||||
if intervention.is_recorded:
|
||||
self.add_error(
|
||||
"intervention",
|
||||
_("Intervention {} is currently recorded. To change any data on it, the entry must be unrecorded.").format(intervention.identifier)
|
||||
)
|
||||
objects_valid = False
|
||||
|
||||
rest_surface = self._get_available_surface(acc)
|
||||
form_surface = float(self.cleaned_data["surface"])
|
||||
@ -447,7 +456,7 @@ class NewDeductionModalForm(BaseModalForm):
|
||||
format_german_float(rest_surface),
|
||||
),
|
||||
)
|
||||
return is_valid_surface and super_result
|
||||
return is_valid_surface and objects_valid and super_result
|
||||
|
||||
def __create_deduction(self):
|
||||
""" Creates the deduction
|
||||
|
@ -1,6 +1,6 @@
|
||||
from django import forms
|
||||
from codelist.models import KonovaCode
|
||||
from codelist.settings import CODELIST_COMPENSATION_ACTION_ID
|
||||
from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_BIOTOPES_ID
|
||||
|
||||
|
||||
class DummyFilterInput(forms.HiddenInput):
|
||||
@ -38,7 +38,17 @@ class TreeCheckboxSelectMultiple(forms.CheckboxSelectMultiple):
|
||||
""" Provides multiple selection of parent-child data
|
||||
|
||||
"""
|
||||
template_name = "konova/widgets/checkbox-tree-select.html"
|
||||
template_name = "konova/widgets/tree/checkbox/checkbox-tree-select.html"
|
||||
|
||||
class meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class TreeRadioSelect(forms.RadioSelect):
|
||||
""" Provides single selection of parent-child data
|
||||
|
||||
"""
|
||||
template_name = "konova/widgets/tree/radio/radio-tree-select.html"
|
||||
|
||||
class meta:
|
||||
abstract = True
|
||||
@ -68,6 +78,30 @@ class KonovaCodeTreeCheckboxSelectMultiple(TreeCheckboxSelectMultiple):
|
||||
return context
|
||||
|
||||
|
||||
class KonovaCodeTreeRadioSelect(TreeRadioSelect):
|
||||
""" Provides single selection of KonovaCode
|
||||
|
||||
"""
|
||||
filter = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.code_list = kwargs.pop("code_list", None)
|
||||
self.filter = kwargs.pop("filter", {})
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def get_context(self, name, value, attrs):
|
||||
context = super().get_context(name, value, attrs)
|
||||
codes = KonovaCode.objects.filter(
|
||||
**self.filter,
|
||||
)
|
||||
codes = [
|
||||
parent_code.add_children()
|
||||
for parent_code in codes
|
||||
]
|
||||
context["codes"] = codes
|
||||
return context
|
||||
|
||||
|
||||
class CompensationActionTreeCheckboxSelectMultiple(KonovaCodeTreeCheckboxSelectMultiple):
|
||||
""" Provides multiple selection of CompensationActions
|
||||
|
||||
@ -80,3 +114,30 @@ class CompensationActionTreeCheckboxSelectMultiple(KonovaCodeTreeCheckboxSelectM
|
||||
"code_lists__in": [CODELIST_COMPENSATION_ACTION_ID],
|
||||
"parent": None,
|
||||
}
|
||||
|
||||
|
||||
class CompensationStateTreeRadioSelect(KonovaCodeTreeRadioSelect):
|
||||
""" Provides single selection of CompensationState
|
||||
|
||||
"""
|
||||
filter = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.filter = {
|
||||
"code_lists__in": [CODELIST_BIOTOPES_ID],
|
||||
"parent": None,
|
||||
"is_archived": False,
|
||||
}
|
||||
|
||||
def get_context(self, name, value, attrs):
|
||||
context = super().get_context(name, value, attrs)
|
||||
codes = KonovaCode.objects.filter(
|
||||
**self.filter,
|
||||
)
|
||||
codes = [
|
||||
parent_code.add_children("short_name")
|
||||
for parent_code in codes
|
||||
]
|
||||
context["codes"] = codes
|
||||
return context
|
@ -13,6 +13,7 @@ from django.db.models.fields.files import FieldFile
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
|
||||
from intervention.tasks import celery_export_to_egon
|
||||
from user.models import User
|
||||
from django.db import models, transaction
|
||||
from django.db.models import QuerySet
|
||||
@ -131,6 +132,16 @@ class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, Chec
|
||||
self.add_log_entry_to_compensations(log_entry)
|
||||
return log_entry
|
||||
|
||||
def send_data_to_egon(self):
|
||||
""" Performs the export to rabbitmq of this intervention's data
|
||||
|
||||
FOLLOWING BACKWARDS COMPATIBILITY LOGIC
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
celery_export_to_egon.delay(self.id)
|
||||
|
||||
def set_recorded(self, user: User) -> UserActionLogEntry:
|
||||
log_entry = super().set_recorded(user)
|
||||
self.add_log_entry_to_compensations(log_entry)
|
||||
@ -171,6 +182,8 @@ class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, Chec
|
||||
intervention=self,
|
||||
)
|
||||
self.mark_as_edited(user, form.request, edit_comment=PAYMENT_ADDED)
|
||||
|
||||
self.send_data_to_egon()
|
||||
return pay
|
||||
|
||||
def add_revocation(self, form):
|
||||
@ -335,6 +348,7 @@ class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, Chec
|
||||
with transaction.atomic():
|
||||
payment.delete()
|
||||
self.mark_as_edited(user, request=form.request, edit_comment=PAYMENT_REMOVED)
|
||||
self.send_data_to_egon()
|
||||
|
||||
|
||||
class InterventionDocument(AbstractDocument):
|
||||
|
@ -7,3 +7,10 @@ Created on: 30.11.20
|
||||
"""
|
||||
INTERVENTION_IDENTIFIER_LENGTH = 6
|
||||
INTERVENTION_IDENTIFIER_TEMPLATE = "EIV-{}"
|
||||
|
||||
# EGON connection settings via rabbitmq
|
||||
# NEEDED FOR BACKWARDS COMPATIBILITY
|
||||
EGON_RABBITMQ_HOST = "CHANGE_ME"
|
||||
EGON_RABBITMQ_PORT = "CHANGE_ME"
|
||||
EGON_RABBITMQ_USER = "CHANGE_ME"
|
||||
EGON_RABBITMQ_PW = "CHANGE_ME"
|
||||
|
@ -9,12 +9,11 @@ from django.http import HttpRequest
|
||||
from django.template.loader import render_to_string
|
||||
from django.urls import reverse
|
||||
from django.utils.html import format_html
|
||||
from django.utils.timezone import localtime
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from intervention.filters import InterventionTableFilter
|
||||
from intervention.models import Intervention
|
||||
from konova.sub_settings.django_settings import DEFAULT_DATE_TIME_FORMAT, DEFAULT_DATE_FORMAT
|
||||
from konova.utils.message_templates import DATA_CHECKED_ON_TEMPLATE, DATA_IS_UNCHECKED, DATA_CHECKED_PREVIOUSLY_TEMPLATE
|
||||
from konova.utils.tables import BaseTable, TableRenderMixin
|
||||
import django_tables2 as tables
|
||||
|
||||
@ -108,16 +107,21 @@ class InterventionTable(BaseTable, TableRenderMixin):
|
||||
"""
|
||||
html = ""
|
||||
checked = value is not None
|
||||
tooltip = _("Not checked yet")
|
||||
previously_checked = record.get_last_checked_action()
|
||||
tooltip = DATA_IS_UNCHECKED
|
||||
if checked:
|
||||
value = value.timestamp
|
||||
value = localtime(value)
|
||||
checked_on = value.strftime(DEFAULT_DATE_TIME_FORMAT)
|
||||
tooltip = _("Checked on {} by {}").format(checked_on, record.checked.user)
|
||||
checked_on = value.get_timestamp_str_formatted()
|
||||
tooltip = DATA_CHECKED_ON_TEMPLATE.format(checked_on, record.checked.user)
|
||||
html += self.render_checked_star(
|
||||
tooltip=tooltip,
|
||||
icn_filled=checked,
|
||||
)
|
||||
if previously_checked and not checked:
|
||||
checked_on = previously_checked.get_timestamp_str_formatted()
|
||||
tooltip = DATA_CHECKED_PREVIOUSLY_TEMPLATE.format(checked_on, previously_checked.user)
|
||||
html += self.render_previously_checked_star(
|
||||
tooltip=tooltip,
|
||||
)
|
||||
return format_html(html)
|
||||
|
||||
def render_d(self, value, record: Intervention):
|
||||
@ -131,7 +135,7 @@ class InterventionTable(BaseTable, TableRenderMixin):
|
||||
|
||||
"""
|
||||
parcels = value.get_underlying_parcels().values_list(
|
||||
"gmrkng",
|
||||
"parcel_group__name",
|
||||
flat=True
|
||||
).distinct()
|
||||
html = render_to_string(
|
||||
@ -156,9 +160,7 @@ class InterventionTable(BaseTable, TableRenderMixin):
|
||||
checked = value is not None
|
||||
tooltip = _("Not recorded yet")
|
||||
if checked:
|
||||
value = value.timestamp
|
||||
value = localtime(value)
|
||||
on = value.strftime(DEFAULT_DATE_TIME_FORMAT)
|
||||
on = value.get_timestamp_str_formatted()
|
||||
tooltip = _("Recorded on {} by {}").format(on, record.recorded.user)
|
||||
html += self.render_bookmark(
|
||||
tooltip=tooltip,
|
||||
@ -177,9 +179,7 @@ class InterventionTable(BaseTable, TableRenderMixin):
|
||||
|
||||
"""
|
||||
html = ""
|
||||
has_access = value.filter(
|
||||
id=self.user.id
|
||||
).exists()
|
||||
has_access = record.is_shared_with(self.user)
|
||||
|
||||
html += self.render_icn(
|
||||
tooltip=_("Full access granted") if has_access else _("Access not granted"),
|
||||
|
18
intervention/tasks.py
Normal file
18
intervention/tasks.py
Normal file
@ -0,0 +1,18 @@
|
||||
"""
|
||||
Author: Michel Peltriaux
|
||||
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
|
||||
Contact: michel.peltriaux@sgdnord.rlp.de
|
||||
Created on: 21.03.22
|
||||
|
||||
"""
|
||||
from celery import shared_task
|
||||
|
||||
from intervention.utils.egon_export import EgonExporter
|
||||
|
||||
|
||||
@shared_task
|
||||
def celery_export_to_egon(intervention_id: str):
|
||||
from intervention.models import Intervention
|
||||
intervention = Intervention.objects.get(id=intervention_id)
|
||||
egon_exporter = EgonExporter(intervention)
|
||||
egon_exporter.export_to_rabbitmq()
|
@ -1,5 +1,5 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n l10n static fontawesome_5 humanize %}
|
||||
{% load i18n l10n static fontawesome_5 %}
|
||||
|
||||
{% block head %}
|
||||
{% comment %}
|
||||
@ -70,6 +70,11 @@
|
||||
<span>
|
||||
{% fa5_icon 'star' 'far' %}
|
||||
</span>
|
||||
{% if last_checked %}
|
||||
<span class="rlp-gd-inv" title="{{last_checked_tooltip}}">
|
||||
{% fa5_icon 'star' 'fas' %}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="check-star" title="{% trans 'Checked on '%} {{obj.checked.timestamp}} {% trans 'by' %} {{obj.checked.user}}">
|
||||
{% fa5_icon 'star' %}
|
||||
@ -106,15 +111,21 @@
|
||||
<tr>
|
||||
<th scope="row">{% trans 'Last modified' %}</th>
|
||||
<td class="align-middle">
|
||||
{{obj.created.timestamp|default_if_none:""|naturalday}}
|
||||
{% if obj.modified %}
|
||||
{{obj.modified.timestamp|default_if_none:""}}
|
||||
<br>
|
||||
{{obj.modified.user.username}}
|
||||
{% else %}
|
||||
{{obj.created.timestamp|default_if_none:""}}
|
||||
<br>
|
||||
{{obj.created.user.username}}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans 'Shared with' %}</th>
|
||||
<td class="align-middle">
|
||||
{% for team in obj.teams.all %}
|
||||
{% for team in obj.shared_teams %}
|
||||
{% include 'user/includes/team_data_modal_button.html' %}
|
||||
{% endfor %}
|
||||
<hr>
|
||||
@ -128,10 +139,12 @@
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
{% include 'map/geom_form.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
{% include 'konova/includes/parcels.html' %}
|
||||
{% include 'konova/includes/parcels/parcels.html' %}
|
||||
</div>
|
||||
<div class="row">
|
||||
{% include 'konova/includes/comment_card.html' %}
|
||||
|
@ -94,20 +94,15 @@
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
{% include 'map/geom_form.html' %}
|
||||
</div>
|
||||
<div class="row">
|
||||
{% include 'konova/includes/parcels.html' %}
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-6 col-md-6 col-lg-6">
|
||||
<h4>{% trans 'Open in browser' %}</h4>
|
||||
{{ qrcode|safe }}
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-6 col-lg-6">
|
||||
<h4>{% trans 'View in LANIS' %}</h4>
|
||||
{{ qrcode_lanis|safe }}
|
||||
{% include 'konova/includes/parcels/parcels.html' %}
|
||||
</div>
|
||||
<div class="row">
|
||||
{% include 'konova/includes/report/qrcodes.html' %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
@ -46,6 +46,7 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase):
|
||||
test_id = self.create_dummy_string()
|
||||
test_title = self.create_dummy_string()
|
||||
test_geom = self.create_dummy_geometry()
|
||||
geom_json = self.create_geojson(test_geom)
|
||||
|
||||
new_url = reverse("intervention:new", args=())
|
||||
|
||||
@ -59,7 +60,7 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase):
|
||||
post_data = {
|
||||
"identifier": test_id,
|
||||
"title": test_title,
|
||||
"geom": test_geom.geojson,
|
||||
"geom": geom_json,
|
||||
}
|
||||
response = self.client_user.post(
|
||||
new_url,
|
||||
@ -89,6 +90,30 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase):
|
||||
self.assertIn(self.superuser, obj.users.all())
|
||||
self.assertEqual(1, obj.users.count())
|
||||
|
||||
def test_non_editable_after_recording(self):
|
||||
""" Tests that the intervention can not be edited after being recorded
|
||||
|
||||
User must be redirected to another page
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
self.assertIsNotNone(self.intervention)
|
||||
self.assertFalse(self.intervention.is_recorded)
|
||||
edit_url = reverse("intervention:edit", args=(self.intervention.id,))
|
||||
response = self.client_user.get(edit_url)
|
||||
has_redirect = response.status_code == 302
|
||||
self.assertFalse(has_redirect)
|
||||
|
||||
self.intervention.set_recorded(self.user)
|
||||
self.assertTrue(self.intervention.is_recorded)
|
||||
|
||||
edit_url = reverse("intervention:edit", args=(self.intervention.id,))
|
||||
response = self.client_user.get(edit_url)
|
||||
has_redirect = response.status_code == 302
|
||||
self.assertTrue(has_redirect)
|
||||
self.intervention.set_unrecorded(self.user)
|
||||
|
||||
def test_checkability(self):
|
||||
""" Tests that the intervention can only be checked if all required data has been added
|
||||
|
||||
|
250
intervention/utils/egon_export.py
Normal file
250
intervention/utils/egon_export.py
Normal file
@ -0,0 +1,250 @@
|
||||
"""
|
||||
Author: Michel Peltriaux
|
||||
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
|
||||
Contact: michel.peltriaux@sgdnord.rlp.de
|
||||
Created on: 07.03.22
|
||||
|
||||
"""
|
||||
import base64
|
||||
import json
|
||||
|
||||
import pika
|
||||
import xmltodict
|
||||
from django.db.models import Sum
|
||||
|
||||
from intervention.settings import EGON_RABBITMQ_HOST, EGON_RABBITMQ_USER, EGON_RABBITMQ_PW, EGON_RABBITMQ_PORT
|
||||
from konova.sub_settings.django_settings import DEFAULT_DATE_FORMAT
|
||||
|
||||
from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP
|
||||
|
||||
|
||||
class EgonExporter:
|
||||
"""
|
||||
EGON is the payment management system of SNU RLP. Due to compatibility reasons we need to provide the old style
|
||||
of data transmission between KSP and EGON:
|
||||
1. Create GML from intervention object
|
||||
2. Send created GML to the appropriate RabbitMQ channel
|
||||
"""
|
||||
intervention = None
|
||||
gml_builder = None
|
||||
|
||||
def __init__(self, intervention):
|
||||
self.intervention = intervention
|
||||
self.gml_builder = EgonGmlBuilder(intervention)
|
||||
|
||||
def export_to_rabbitmq(self):
|
||||
""" Sends the exporter gml to message broker rabbitmq to be fetched by EGON application from there
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
msg = {
|
||||
"nachricht": self.gml_builder.gml,
|
||||
}
|
||||
msg = json.dumps(msg)
|
||||
print(msg)
|
||||
credentials = pika.PlainCredentials(EGON_RABBITMQ_USER, EGON_RABBITMQ_PW)
|
||||
params = pika.ConnectionParameters(
|
||||
EGON_RABBITMQ_HOST,
|
||||
EGON_RABBITMQ_PORT,
|
||||
"/",
|
||||
credentials
|
||||
)
|
||||
conn = pika.BlockingConnection(params)
|
||||
channel = conn.channel()
|
||||
channel.basic_publish(
|
||||
exchange="",
|
||||
routing_key="KSP_EGON",
|
||||
body=msg.encode("utf-8"),
|
||||
)
|
||||
conn.close()
|
||||
|
||||
|
||||
class EgonGmlBuilder:
|
||||
"""
|
||||
Creates the GML for EGON export
|
||||
"""
|
||||
intervention = None
|
||||
gml = None
|
||||
|
||||
def __init__(self, intervention):
|
||||
self.intervention = intervention
|
||||
self.gml = self.build_gml()
|
||||
|
||||
def _gen_flurstuecksKennzeichen(self, parcel):
|
||||
""" Generates oneo:flurstuecksKennzeichen to provide backwards compatibility
|
||||
|
||||
Args:
|
||||
parcel (Parcel): The requested parcel
|
||||
|
||||
Returns:
|
||||
str
|
||||
"""
|
||||
gmrkng_code = "{0:06d}".format(int(parcel.parcel_group.key) or 0)
|
||||
flr_code = "{0:03d}".format(int(parcel.flr or 0))
|
||||
flrstckzhlr_code = "{0:05d}".format(int(parcel.flrstck_zhlr or 0))
|
||||
flrstcknnr_code = "{0:06d}".format(int(parcel.flrstck_nnr or 0))
|
||||
return gmrkng_code + flr_code + flrstckzhlr_code + flrstcknnr_code
|
||||
|
||||
def _sum_all_payments(self):
|
||||
all_payments = self.intervention.payments.aggregate(
|
||||
summed=Sum("amount")
|
||||
)["summed"]
|
||||
return all_payments
|
||||
|
||||
def _gen_kompensationsArt(self) -> (str, int):
|
||||
comp_type = "Ersatzzahlung"
|
||||
comp_type_code = 774898901
|
||||
if self.intervention.compensations.exists():
|
||||
comp_type += " und Kompensation"
|
||||
comp_type_code = 771655351
|
||||
return comp_type, comp_type_code
|
||||
|
||||
def _gen_geometry_list(self):
|
||||
geom = self.intervention.geometry.geom
|
||||
geom.transform(DEFAULT_SRID_RLP)
|
||||
geoms_list = [
|
||||
{
|
||||
"gml:Polygon": {
|
||||
"gml:exterior": {
|
||||
"gml:LinearRing": {
|
||||
"gml:posList": " ".join([f"{str(coord[0])} {str(coord[1])}" for coord in coords[0]])
|
||||
}
|
||||
}
|
||||
}
|
||||
} for coords in geom.coords
|
||||
]
|
||||
return geoms_list
|
||||
|
||||
def _gen_raumreferenz(self):
|
||||
parcels = self.intervention.get_underlying_parcels()
|
||||
spatial_reference_list = [
|
||||
{
|
||||
"oneo:datumAbgleich": None,
|
||||
"oneo:ortsangabe": {
|
||||
"oneo:Ortsangaben": {
|
||||
"oneo:kreisSchluessel": {
|
||||
"xlink:href": f"http://register.naturschutz.rlp.de/repository/services/referenzliste/588/{parcel.district.key}",
|
||||
},
|
||||
"oneo:gemeindeSchluessel": {
|
||||
"xlink:href": f"http://register.naturschutz.rlp.de/repository/services/referenzliste/910/{parcel.municipal.key}",
|
||||
},
|
||||
"oneo:verbandsgemeindeSchluessel": {
|
||||
"xlink:href": f"http://register.naturschutz.rlp.de/repository/services/referenzliste/589/{None}",
|
||||
},
|
||||
"oneo:flurstuecksKennzeichen": self._gen_flurstuecksKennzeichen(parcel),
|
||||
}
|
||||
},
|
||||
} for parcel in parcels
|
||||
]
|
||||
return spatial_reference_list
|
||||
|
||||
def _gen_foto(self):
|
||||
revoc_docs, regular_docs = self.intervention.get_documents()
|
||||
docs_list = [
|
||||
{
|
||||
"oneo:Foto": {
|
||||
"oneo:aufnahmezeitpunkt": doc.date_of_creation.strftime(DEFAULT_DATE_FORMAT),
|
||||
"oneo:bemerkung": doc.comment,
|
||||
"oneo:fotoverweis": base64.b64encode(doc.file.read()).decode("utf-8"),
|
||||
"oneo:dateiname": doc.title,
|
||||
"oneo:hauptfoto": False,
|
||||
}
|
||||
} for doc in regular_docs
|
||||
]
|
||||
return docs_list
|
||||
|
||||
def build_gml(self):
|
||||
comp_type, comp_type_code = self._gen_kompensationsArt()
|
||||
payment = self.intervention.payments.first()
|
||||
payment_date = None
|
||||
if payment is not None:
|
||||
payment_date = payment.due_on
|
||||
payment_date = payment_date.strftime(DEFAULT_DATE_FORMAT)
|
||||
|
||||
cons_office = self.intervention.responsible.conservation_office
|
||||
reg_office = self.intervention.responsible.registration_office
|
||||
law = self.intervention.legal.laws.first()
|
||||
process_type = self.intervention.legal.process_type
|
||||
handler = self.intervention.responsible.handler
|
||||
reg_date = self.intervention.legal.registration_date
|
||||
bind_date = self.intervention.legal.binding_date
|
||||
|
||||
xml_dict = {
|
||||
"wfs:FeatureCollection": {
|
||||
"@xmlns:wfs": "http://www.opengis.net/wfs",
|
||||
"@xmlns:xlink": "http://www.w3.org/1999/xlink",
|
||||
"@xmlns:oneo": "http://www.osiris-projekt.rlp.de/oneo",
|
||||
"@xmlns:gmlexr": "http://www.opengis.net/gml/3.3/exr",
|
||||
"@xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
|
||||
"@xmlns:gml": "http://www.opengis.net/gml/3.2",
|
||||
"oneo:Eingriffsverfahren": {
|
||||
"@gml:id": self.intervention.identifier,
|
||||
"oneo:azEintragungsstelle": self.intervention.responsible.conservation_file_number,
|
||||
"oneo:azZulassungsstelle": self.intervention.responsible.registration_file_number,
|
||||
"oneo:bemerkungZulassungsstelle": None,
|
||||
"oneo:eintragungsstelle": {
|
||||
"@xlink:href": f"http://register.naturschutz.rlp.de/repository/services/referenzliste/907/{cons_office.atom_id if cons_office else None}",
|
||||
"#text": cons_office.long_name if cons_office else None
|
||||
},
|
||||
"oneo:zulassungsstelle": {
|
||||
"@xlink:href": f"http://register.naturschutz.rlp.de/repository/services/referenzliste/1053/{reg_office.atom_id if reg_office else None}",
|
||||
"#text": reg_office.long_name if reg_office else None
|
||||
},
|
||||
"oneo:ersatzzahlung": self._sum_all_payments(),
|
||||
"oneo:kompensationsart": {
|
||||
"@xlink:href": f"http://register.naturschutz.rlp.de/repository/services/referenzliste/88140/{comp_type_code}",
|
||||
"#text": comp_type
|
||||
},
|
||||
"oneo:verfahrensrecht": {
|
||||
"@xlink:href": f"http://register.naturschutz.rlp.de/repository/services/referenzliste/1048/{law.atom_id if law else None}",
|
||||
"#text": law.short_name if law else None
|
||||
},
|
||||
"oneo:verfahrenstyp": {
|
||||
"@xlink:href": f"http://register.naturschutz.rlp.de/repository/services/referenzliste/44382/{process_type.atom_id if process_type else None}",
|
||||
"#text": process_type.long_name if process_type else None,
|
||||
},
|
||||
"oneo:eingreifer": {
|
||||
"oneo:Eingreifer": {
|
||||
"oneo:art": {
|
||||
"@xlink:href": f"http://register.naturschutz.rlp.de/repository/services/referenzliste/1053/{handler.type.atom_id if handler.type else None}",
|
||||
"#text": handler.type.long_name if handler.type else None,
|
||||
},
|
||||
"oneo:bemerkung": handler.detail if handler else None,
|
||||
}
|
||||
},
|
||||
"oneo:erfasser": {
|
||||
"oneo:Erfasser": {
|
||||
"oneo:name": None,
|
||||
"oneo:bemerkung": None,
|
||||
}
|
||||
},
|
||||
"oneo:zulassung": {
|
||||
"oneo:Zulassungstermin": {
|
||||
"oneo:bauBeginn": payment_date,
|
||||
"oneo:erlass": reg_date.strftime(DEFAULT_DATE_FORMAT) if reg_date else None,
|
||||
"oneo:rechtsKraft": bind_date.strftime(DEFAULT_DATE_FORMAT) if bind_date else None,
|
||||
}
|
||||
},
|
||||
"oneo:geometrie": {
|
||||
"gml:multiSurfaceProperty": {
|
||||
"gml:MultiPolygon": {
|
||||
"@srsName": f"http://www.opengis.net/gml/srs/epsg.xml#{DEFAULT_SRID_RLP}",
|
||||
"gml:polygonMember": self._gen_geometry_list(),
|
||||
}
|
||||
},
|
||||
},
|
||||
"oneo:kennung": self.intervention.identifier,
|
||||
"oneo:bezeichnung": self.intervention.title,
|
||||
"oneo:bemerkung": self.intervention.comment,
|
||||
"oneo:verantwortlicheStelle": None,
|
||||
"oneo:veroffentlichtAm": None,
|
||||
"oneo:raumreferenz": {
|
||||
"oneo:Raumreferenz": self._gen_raumreferenz(),
|
||||
},
|
||||
"oneo:foto": self._gen_foto(),
|
||||
}
|
||||
},
|
||||
}
|
||||
gml = xmltodict.unparse(xml_dict)
|
||||
return gml
|
@ -18,7 +18,8 @@ from konova.utils.documents import remove_document, get_document
|
||||
from konova.utils.generators import generate_qr_code
|
||||
from konova.utils.message_templates import INTERVENTION_INVALID, FORM_INVALID, IDENTIFIER_REPLACED, \
|
||||
CHECKED_RECORDED_RESET, DEDUCTION_REMOVED, DEDUCTION_ADDED, REVOCATION_ADDED, REVOCATION_REMOVED, \
|
||||
COMPENSATION_REMOVED_TEMPLATE, DOCUMENT_ADDED, DEDUCTION_EDITED, REVOCATION_EDITED, DOCUMENT_EDITED
|
||||
COMPENSATION_REMOVED_TEMPLATE, DOCUMENT_ADDED, DEDUCTION_EDITED, REVOCATION_EDITED, DOCUMENT_EDITED, \
|
||||
RECORDED_BLOCKS_EDIT, DATA_CHECKED_PREVIOUSLY_TEMPLATE
|
||||
from konova.utils.user_checks import in_group
|
||||
|
||||
|
||||
@ -264,15 +265,18 @@ def detail_view(request: HttpRequest, id: str):
|
||||
geom_form = SimpleGeomForm(
|
||||
instance=intervention,
|
||||
)
|
||||
|
||||
parcels = intervention.get_underlying_parcels()
|
||||
last_checked = intervention.get_last_checked_action()
|
||||
last_checked_tooltip = ""
|
||||
if last_checked:
|
||||
last_checked_tooltip = DATA_CHECKED_PREVIOUSLY_TEMPLATE.format(last_checked.get_timestamp_str_formatted(), last_checked.user)
|
||||
|
||||
context = {
|
||||
"obj": intervention,
|
||||
"last_checked": last_checked,
|
||||
"last_checked_tooltip": last_checked_tooltip,
|
||||
"compensations": compensations,
|
||||
"has_access": is_data_shared,
|
||||
"geom_form": geom_form,
|
||||
"parcels": parcels,
|
||||
"is_default_member": in_group(_user, DEFAULT_GROUP),
|
||||
"is_zb_member": in_group(_user, ZB_GROUP),
|
||||
"is_ets_member": in_group(_user, ETS_GROUP),
|
||||
@ -302,6 +306,13 @@ def edit_view(request: HttpRequest, id: str):
|
||||
template = "intervention/form/view.html"
|
||||
# Get object from db
|
||||
intervention = get_object_or_404(Intervention, id=id)
|
||||
if intervention.is_recorded:
|
||||
messages.info(
|
||||
request,
|
||||
RECORDED_BLOCKS_EDIT
|
||||
)
|
||||
return redirect("intervention:detail", id=id)
|
||||
|
||||
# Create forms, initialize with values from db/from POST request
|
||||
data_form = EditInterventionForm(request.POST or None, instance=intervention)
|
||||
geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=intervention)
|
||||
@ -693,19 +704,22 @@ def report_view(request:HttpRequest, id: str):
|
||||
distinct_deductions = intervention.deductions.all().distinct(
|
||||
"account"
|
||||
)
|
||||
qrcode_img = generate_qr_code(
|
||||
request.build_absolute_uri(reverse("intervention:report", args=(id,))),
|
||||
10
|
||||
)
|
||||
qrcode_img_lanis = generate_qr_code(
|
||||
intervention.get_LANIS_link(),
|
||||
7
|
||||
)
|
||||
qrcode_url = request.build_absolute_uri(reverse("intervention:report", args=(id,)))
|
||||
qrcode_img = generate_qr_code(qrcode_url, 10)
|
||||
qrcode_lanis_url = intervention.get_LANIS_link()
|
||||
qrcode_img_lanis = generate_qr_code(qrcode_lanis_url, 7)
|
||||
|
||||
context = {
|
||||
"obj": intervention,
|
||||
"deductions": distinct_deductions,
|
||||
"qrcode": qrcode_img,
|
||||
"qrcode_lanis": qrcode_img_lanis,
|
||||
"qrcode": {
|
||||
"img": qrcode_img,
|
||||
"url": qrcode_url,
|
||||
},
|
||||
"qrcode_lanis": {
|
||||
"img": qrcode_img_lanis,
|
||||
"url": qrcode_lanis_url,
|
||||
},
|
||||
"geom_form": geom_form,
|
||||
"parcels": parcels,
|
||||
TAB_TITLE_IDENTIFIER: tab_title,
|
||||
|
@ -7,7 +7,8 @@ Created on: 22.07.21
|
||||
"""
|
||||
from django.contrib import admin
|
||||
|
||||
from konova.models import Geometry, Deadline, GeometryConflict, Parcel, District
|
||||
from konova.models import Geometry, Deadline, GeometryConflict, Parcel, District, Municipal, ParcelGroup
|
||||
from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP
|
||||
from konova.utils.message_templates import COMPENSATION_REMOVED_TEMPLATE
|
||||
from user.models import UserAction
|
||||
|
||||
@ -16,13 +17,28 @@ class GeometryAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
"id",
|
||||
"created",
|
||||
"st_area",
|
||||
]
|
||||
readonly_fields = [
|
||||
"st_area",
|
||||
"created",
|
||||
"modified",
|
||||
]
|
||||
|
||||
def st_area(self, obj):
|
||||
val = None
|
||||
geom = obj.geom
|
||||
if geom is not None:
|
||||
geom.transform(ct=DEFAULT_SRID_RLP)
|
||||
val = geom.area
|
||||
return val
|
||||
st_area.short_description = f"Area (srid={DEFAULT_SRID_RLP})"
|
||||
|
||||
|
||||
class ParcelAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
"id",
|
||||
"gmrkng",
|
||||
"parcel_group",
|
||||
"flr",
|
||||
"flrstck_nnr",
|
||||
"flrstck_zhlr",
|
||||
@ -32,9 +48,27 @@ class ParcelAdmin(admin.ModelAdmin):
|
||||
|
||||
class DistrictAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
"name",
|
||||
"key",
|
||||
"id",
|
||||
]
|
||||
|
||||
|
||||
class MunicipalAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
"name",
|
||||
"key",
|
||||
"district",
|
||||
"id",
|
||||
]
|
||||
|
||||
|
||||
class ParcelGroupAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
"name",
|
||||
"key",
|
||||
"municipal",
|
||||
"id",
|
||||
"gmnd",
|
||||
"krs",
|
||||
]
|
||||
|
||||
|
||||
@ -64,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",
|
||||
@ -75,7 +121,7 @@ class BaseResourceAdmin(admin.ModelAdmin):
|
||||
]
|
||||
|
||||
|
||||
class BaseObjectAdmin(BaseResourceAdmin):
|
||||
class BaseObjectAdmin(BaseResourceAdmin, DeletableObjectMixinAdmin):
|
||||
search_fields = [
|
||||
"identifier",
|
||||
"title",
|
||||
@ -92,18 +138,13 @@ 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
|
||||
#admin.site.register(Geometry, GeometryAdmin)
|
||||
#admin.site.register(Parcel, ParcelAdmin)
|
||||
#admin.site.register(District, DistrictAdmin)
|
||||
#admin.site.register(Municipal, MunicipalAdmin)
|
||||
#admin.site.register(ParcelGroup, ParcelGroupAdmin)
|
||||
#admin.site.register(GeometryConflict, GeometryConflictAdmin)
|
||||
#admin.site.register(Deadline, DeadlineAdmin)
|
||||
|
@ -53,14 +53,16 @@ class InterventionAutocomplete(Select2QuerySetView):
|
||||
|
||||
"""
|
||||
def get_queryset(self):
|
||||
if self.request.user.is_anonymous:
|
||||
user = self.request.user
|
||||
if user.is_anonymous:
|
||||
return Intervention.objects.none()
|
||||
qs = Intervention.objects.filter(
|
||||
deleted=None,
|
||||
users__in=[self.request.user],
|
||||
Q(deleted=None) &
|
||||
Q(users__in=[user]) |
|
||||
Q(teams__in=user.teams.all())
|
||||
).order_by(
|
||||
"identifier"
|
||||
)
|
||||
).distinct()
|
||||
if self.q:
|
||||
qs = qs.filter(
|
||||
Q(identifier__icontains=self.q) |
|
||||
@ -95,7 +97,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(
|
||||
@ -107,6 +111,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
|
||||
|
@ -145,26 +145,20 @@ class GeoReferencedTableFilterMixin(django_filters.FilterSet):
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def _filter_parcel_reference(self, queryset, name, value, filter_value) -> QuerySet:
|
||||
""" Filters the parcel entries by a given filter_value.
|
||||
|
||||
filter_value may already include further filter annotations like 'xy__icontains'
|
||||
def _filter_parcel_reference(self, queryset, filter_q) -> QuerySet:
|
||||
""" Filters the parcel entries by a given filter_q
|
||||
|
||||
Args:
|
||||
queryset ():
|
||||
name ():
|
||||
value ():
|
||||
filter_value ():
|
||||
queryset (QuerySet): The queryset
|
||||
filter_q (Q): The Q-style filter expression
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
_filter = {
|
||||
filter_value: value
|
||||
}
|
||||
matching_parcels = Parcel.objects.filter(
|
||||
**_filter
|
||||
filter_q
|
||||
)
|
||||
|
||||
related_geoms = matching_parcels.values(
|
||||
"geometries"
|
||||
).distinct()
|
||||
@ -185,8 +179,9 @@ class GeoReferencedTableFilterMixin(django_filters.FilterSet):
|
||||
|
||||
"""
|
||||
matching_districts = District.objects.filter(
|
||||
krs__icontains=value
|
||||
)
|
||||
Q(name__icontains=value) |
|
||||
Q(key__icontains=value)
|
||||
).distinct()
|
||||
matching_parcels = Parcel.objects.filter(
|
||||
district__in=matching_districts
|
||||
)
|
||||
@ -209,7 +204,10 @@ class GeoReferencedTableFilterMixin(django_filters.FilterSet):
|
||||
Returns:
|
||||
|
||||
"""
|
||||
queryset = self._filter_parcel_reference(queryset, name, value, "gmrkng__icontains")
|
||||
queryset = self._filter_parcel_reference(
|
||||
queryset,
|
||||
Q(parcel_group__name__icontains=value) | Q(parcel_group__key__icontains=value),
|
||||
)
|
||||
return queryset
|
||||
|
||||
def filter_parcel(self, queryset, name, value) -> QuerySet:
|
||||
@ -224,7 +222,10 @@ class GeoReferencedTableFilterMixin(django_filters.FilterSet):
|
||||
|
||||
"""
|
||||
value = value.replace("-", "")
|
||||
queryset = self._filter_parcel_reference(queryset, name, value, "flr")
|
||||
queryset = self._filter_parcel_reference(
|
||||
queryset,
|
||||
Q(flr=value),
|
||||
)
|
||||
return queryset
|
||||
|
||||
def filter_parcel_counter(self, queryset, name, value) -> QuerySet:
|
||||
@ -239,7 +240,10 @@ class GeoReferencedTableFilterMixin(django_filters.FilterSet):
|
||||
|
||||
"""
|
||||
value = value.replace("-", "")
|
||||
queryset = self._filter_parcel_reference(queryset, name, value, "flrstck_zhlr")
|
||||
queryset = self._filter_parcel_reference(
|
||||
queryset,
|
||||
Q(flrstck_zhlr=value)
|
||||
)
|
||||
return queryset
|
||||
|
||||
def filter_parcel_number(self, queryset, name, value) -> QuerySet:
|
||||
@ -254,7 +258,10 @@ class GeoReferencedTableFilterMixin(django_filters.FilterSet):
|
||||
|
||||
"""
|
||||
value = value.replace("-", "")
|
||||
queryset = self._filter_parcel_reference(queryset, name, value, "flrstck_nnr")
|
||||
queryset = self._filter_parcel_reference(
|
||||
queryset,
|
||||
Q(flrstck_nnr=value),
|
||||
)
|
||||
return queryset
|
||||
|
||||
|
||||
@ -298,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
|
||||
|
125
konova/forms.py
125
konova/forms.py
@ -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
|
||||
@ -57,6 +59,8 @@ class BaseForm(forms.Form):
|
||||
self.has_required_fields = True
|
||||
break
|
||||
|
||||
self.check_for_recorded_instance()
|
||||
|
||||
@abstractmethod
|
||||
def save(self):
|
||||
# To be implemented in subclasses!
|
||||
@ -136,6 +140,38 @@ class BaseForm(forms.Form):
|
||||
set_class = set_class.replace(cls, "")
|
||||
self.fields[field].widget.attrs["class"] = set_class
|
||||
|
||||
def check_for_recorded_instance(self):
|
||||
""" Checks if the instance is recorded and runs some special logic if yes
|
||||
|
||||
If the instance is recorded, the form shall not display any possibility to
|
||||
edit any data. Instead, the users should get some information about why they can not edit anything.
|
||||
|
||||
There are situations where the form should be rendered regularly,
|
||||
e.g deduction forms for (recorded) eco accounts.
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
from intervention.forms.modalForms import NewDeductionModalForm, EditEcoAccountDeductionModalForm, \
|
||||
RemoveEcoAccountDeductionModalForm
|
||||
is_none = self.instance is None
|
||||
is_other_data_type = not isinstance(self.instance, BaseObject)
|
||||
is_deduction_form = isinstance(
|
||||
self,
|
||||
(
|
||||
NewDeductionModalForm,
|
||||
EditEcoAccountDeductionModalForm,
|
||||
RemoveEcoAccountDeductionModalForm,
|
||||
)
|
||||
)
|
||||
|
||||
if is_none or is_other_data_type or is_deduction_form:
|
||||
# Do nothing
|
||||
return
|
||||
|
||||
if self.instance.is_recorded:
|
||||
self.template = "form/recorded_no_edit.html"
|
||||
|
||||
|
||||
class RemoveForm(BaseForm):
|
||||
check = forms.BooleanField(
|
||||
@ -238,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
|
||||
@ -410,7 +490,6 @@ class NewDocumentModalForm(BaseModalForm):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.form_title = _("Add new document")
|
||||
self.form_caption = _("")
|
||||
self.template = "modal/modal_form.html"
|
||||
self.form_attrs = {
|
||||
"enctype": "multipart/form-data", # important for file upload
|
||||
}
|
||||
@ -598,3 +677,11 @@ class RecordModalForm(BaseModalForm):
|
||||
else:
|
||||
self.instance.set_recorded(self.user)
|
||||
return self.instance
|
||||
|
||||
def check_for_recorded_instance(self):
|
||||
""" Overwrite the check method for doing nothing on the RecordModalForm
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
pass
|
||||
|
@ -9,7 +9,7 @@ from compensation.models import CompensationState, Compensation, EcoAccount, Com
|
||||
from ema.models import Ema
|
||||
from intervention.models import Intervention
|
||||
from konova.management.commands.setup import BaseKonovaCommand
|
||||
from konova.models import Deadline, Geometry, Parcel, District
|
||||
from konova.models import Deadline, Geometry, Parcel, District, Municipal, ParcelGroup
|
||||
from user.models import UserActionLogEntry, UserAction
|
||||
|
||||
|
||||
@ -271,13 +271,26 @@ class Command(BaseKonovaCommand):
|
||||
self._write_success("No unused states found.")
|
||||
self._break_line()
|
||||
|
||||
def __sanitize_parcel_sub_type(self, cls):
|
||||
unrelated_entries = cls.objects.filter(
|
||||
parcels=None,
|
||||
)
|
||||
num_unrelated_entries = unrelated_entries.count()
|
||||
cls_name = cls.__name__
|
||||
if num_unrelated_entries > 0:
|
||||
self._write_error(f"Found {num_unrelated_entries} unrelated {cls_name} entries. Delete now...")
|
||||
unrelated_entries.delete()
|
||||
self._write_success(f"Unrelated {cls_name} deleted.")
|
||||
else:
|
||||
self._write_success(f"No unrelated {cls_name} found.")
|
||||
|
||||
def sanitize_parcels_and_districts(self):
|
||||
""" Removes unattached parcels and districts
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
self._write_warning("=== Sanitize parcels and districts ===")
|
||||
self._write_warning("=== Sanitize administrative spatial references ===")
|
||||
unrelated_parcels = Parcel.objects.filter(
|
||||
geometries=None,
|
||||
)
|
||||
@ -289,16 +302,12 @@ class Command(BaseKonovaCommand):
|
||||
else:
|
||||
self._write_success("No unrelated parcels found.")
|
||||
|
||||
unrelated_districts = District.objects.filter(
|
||||
parcels=None,
|
||||
)
|
||||
num_unrelated_districts = unrelated_districts.count()
|
||||
if num_unrelated_districts > 0:
|
||||
self._write_error(f"Found {num_unrelated_districts} unrelated district entries. Delete now...")
|
||||
unrelated_districts.delete()
|
||||
self._write_success("Unrelated districts deleted.")
|
||||
else:
|
||||
self._write_success("No unrelated districts found.")
|
||||
sub_types = [
|
||||
District,
|
||||
Municipal,
|
||||
ParcelGroup
|
||||
]
|
||||
for sub_type in sub_types:
|
||||
self.__sanitize_parcel_sub_type(sub_type)
|
||||
|
||||
self._break_line()
|
||||
|
||||
|
@ -5,10 +5,13 @@ Contact: michel.peltriaux@sgdnord.rlp.de
|
||||
Created on: 04.01.22
|
||||
|
||||
"""
|
||||
import datetime
|
||||
from pyexpat import ExpatError
|
||||
|
||||
from requests.exceptions import ProxyError
|
||||
|
||||
from django.contrib.gis.db.models.functions import Area
|
||||
|
||||
from konova.management.commands.setup import BaseKonovaCommand
|
||||
from konova.models import Geometry, Parcel, District
|
||||
|
||||
@ -32,8 +35,11 @@ class Command(BaseKonovaCommand):
|
||||
num_parcels_before = Parcel.objects.count()
|
||||
num_districts_before = District.objects.count()
|
||||
self._write_warning("=== Update parcels and districts ===")
|
||||
# Order geometries by size to process smaller once at first
|
||||
geometries = Geometry.objects.all().exclude(
|
||||
geom=None
|
||||
).annotate(area=Area("geom")).order_by(
|
||||
'area'
|
||||
)
|
||||
|
||||
if ids is not None:
|
||||
|
71
konova/migrations/0006_auto_20220411_0835.py
Normal file
71
konova/migrations/0006_auto_20220411_0835.py
Normal file
@ -0,0 +1,71 @@
|
||||
# Generated by Django 3.1.3 on 2022-04-11 06:35
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('konova', '0005_auto_20220216_0856'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Municipal',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('key', models.IntegerField(blank=True, help_text='Represents Gemeindeschlüssel', null=True)),
|
||||
('name', models.CharField(blank=True, help_text='Gemeinde', max_length=1000, null=True)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='district',
|
||||
old_name='krs',
|
||||
new_name='name',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='district',
|
||||
name='gmnd',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='parcel',
|
||||
name='gmrkng',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='district',
|
||||
name='key',
|
||||
field=models.IntegerField(blank=True, help_text='Represents Kreisschlüssel', null=True),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ParcelGroup',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('key', models.IntegerField(blank=True, help_text='Represents Gemarkungsschlüssel', null=True)),
|
||||
('name', models.CharField(blank=True, help_text='Gemarkung', max_length=1000, null=True)),
|
||||
('municipal', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='konova.municipal')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='municipal',
|
||||
name='district',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='konova.district'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='parcel',
|
||||
name='municipal',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='parcels', to='konova.municipal'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='parcel',
|
||||
name='parcel_group',
|
||||
field=models.ForeignKey(blank=True, help_text='Gemarkung', null=True, on_delete=django.db.models.deletion.SET_NULL, to='konova.parcelgroup'),
|
||||
),
|
||||
]
|
28
konova/migrations/0007_auto_20220411_0848.py
Normal file
28
konova/migrations/0007_auto_20220411_0848.py
Normal file
@ -0,0 +1,28 @@
|
||||
# Generated by Django 3.1.3 on 2022-04-11 06:48
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('konova', '0006_auto_20220411_0835'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='district',
|
||||
name='key',
|
||||
field=models.CharField(blank=True, help_text='Represents Kreisschlüssel', max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='municipal',
|
||||
name='key',
|
||||
field=models.CharField(blank=True, help_text='Represents Gemeindeschlüssel', max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='parcelgroup',
|
||||
name='key',
|
||||
field=models.CharField(blank=True, help_text='Represents Gemarkungsschlüssel', max_length=255, null=True),
|
||||
),
|
||||
]
|
48
konova/migrations/0008_auto_20220411_0914.py
Normal file
48
konova/migrations/0008_auto_20220411_0914.py
Normal file
@ -0,0 +1,48 @@
|
||||
# Generated by Django 3.1.3 on 2022-04-11 07:14
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('konova', '0007_auto_20220411_0848'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='municipal',
|
||||
name='key',
|
||||
field=models.CharField(blank=True, help_text='Represents Kreisschlüssel', max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='municipal',
|
||||
name='name',
|
||||
field=models.CharField(blank=True, help_text='Kreis', max_length=1000, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='parcel',
|
||||
name='flr',
|
||||
field=models.IntegerField(blank=True, help_text='Flur', null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='parcel',
|
||||
name='flrstck_nnr',
|
||||
field=models.IntegerField(blank=True, help_text='Flurstücksnenner', null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='parcel',
|
||||
name='flrstck_zhlr',
|
||||
field=models.IntegerField(blank=True, help_text='Flurstückszähler', null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='parcelgroup',
|
||||
name='key',
|
||||
field=models.CharField(blank=True, help_text='Represents Kreisschlüssel', max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='parcelgroup',
|
||||
name='name',
|
||||
field=models.CharField(blank=True, help_text='Kreis', max_length=1000, null=True),
|
||||
),
|
||||
]
|
19
konova/migrations/0009_auto_20220411_1004.py
Normal file
19
konova/migrations/0009_auto_20220411_1004.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Generated by Django 3.1.3 on 2022-04-11 08:04
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('konova', '0008_auto_20220411_0914'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='parcel',
|
||||
name='parcel_group',
|
||||
field=models.ForeignKey(blank=True, help_text='Gemarkung', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='parcels', to='konova.parcelgroup'),
|
||||
),
|
||||
]
|
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
|
||||
|
||||
|
||||
@ -20,6 +24,9 @@ class Geometry(BaseResource):
|
||||
from konova.settings import DEFAULT_SRID
|
||||
geom = MultiPolygonField(null=True, blank=True, srid=DEFAULT_SRID)
|
||||
|
||||
def __str__(self):
|
||||
return str(self.id)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super().save(*args, **kwargs)
|
||||
self.check_for_conflicts()
|
||||
@ -93,13 +100,14 @@ class Geometry(BaseResource):
|
||||
objs += set_objs
|
||||
return objs
|
||||
|
||||
@transaction.atomic
|
||||
def update_parcels(self):
|
||||
""" Updates underlying parcel information
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
from konova.models import Parcel, District, ParcelIntersection
|
||||
from konova.models import Parcel, District, ParcelIntersection, Municipal, ParcelGroup
|
||||
parcel_fetcher = ParcelWFSFetcher(
|
||||
geometry_id=self.id,
|
||||
)
|
||||
@ -110,20 +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 ", "")
|
||||
parcel_obj = Parcel.objects.get_or_create(
|
||||
gmrkng=fetched_parcel["ave:gemarkung"],
|
||||
flr=flr_val,
|
||||
flrstck_nnr=fetched_parcel['ave:flstnrnen'],
|
||||
flrstck_zhlr=fetched_parcel['ave:flstnrzae'],
|
||||
)[0]
|
||||
flr_val = parcel_properties["flur"].replace("Flur ", "")
|
||||
district = District.objects.get_or_create(
|
||||
gmnd=fetched_parcel["ave:gemeinde"],
|
||||
krs=fetched_parcel["ave:kreis"],
|
||||
key=parcel_properties["kreisschl"],
|
||||
name=parcel_properties["kreis"],
|
||||
)[0]
|
||||
municipal = Municipal.objects.get_or_create(
|
||||
key=parcel_properties["gmdschl"],
|
||||
name=parcel_properties["gemeinde"],
|
||||
district=district,
|
||||
)[0]
|
||||
parcel_group = ParcelGroup.objects.get_or_create(
|
||||
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=flrstck_nnr,
|
||||
flrstck_zhlr=flrstck_zhlr,
|
||||
)[0]
|
||||
parcel_obj.district = district
|
||||
parcel_obj.updated_on = _now
|
||||
@ -131,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
|
||||
@ -151,17 +178,55 @@ class Geometry(BaseResource):
|
||||
Returns:
|
||||
parcels (QuerySet): The related parcels as queryset
|
||||
"""
|
||||
|
||||
parcels = self.parcels.filter(
|
||||
parcelintersection__calculated_on__isnull=False,
|
||||
).prefetch_related(
|
||||
"district"
|
||||
"district",
|
||||
"municipal",
|
||||
).order_by(
|
||||
"gmrkng",
|
||||
"municipal__name",
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
@ -289,6 +298,8 @@ class RecordableObjectMixin(models.Model):
|
||||
from user.models import UserActionLogEntry
|
||||
if self.recorded:
|
||||
return None
|
||||
|
||||
self.unshare_with_default_users()
|
||||
action = UserActionLogEntry.get_recorded_action(user)
|
||||
self.recorded = action
|
||||
self.save()
|
||||
@ -335,6 +346,15 @@ class RecordableObjectMixin(models.Model):
|
||||
"""
|
||||
raise NotImplementedError("Implement this in the subclass!")
|
||||
|
||||
@property
|
||||
def is_recorded(self):
|
||||
""" Getter for record status as property
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
return self.recorded is not None
|
||||
|
||||
|
||||
class CheckableObjectMixin(models.Model):
|
||||
# Checks - Refers to "Genehmigen" but optional
|
||||
@ -397,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
|
||||
@ -459,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
|
||||
@ -597,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):
|
||||
@ -608,6 +644,26 @@ class ShareableObjectMixin(models.Model):
|
||||
"""
|
||||
raise NotImplementedError("Must be implemented in subclasses!")
|
||||
|
||||
def unshare_with_default_users(self):
|
||||
""" Removes all shared users from direct shared access which are only default group users
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
from konova.utils.user_checks import is_default_group_only
|
||||
users = self.shared_users
|
||||
cleaned_users = []
|
||||
default_users = []
|
||||
for user in users:
|
||||
if not is_default_group_only(user):
|
||||
cleaned_users.append(user)
|
||||
else:
|
||||
default_users.append(user)
|
||||
self.share_with_user_list(cleaned_users)
|
||||
|
||||
for user in default_users:
|
||||
celery_send_mail_shared_access_removed.delay(self.identifier, self.title, user.id)
|
||||
|
||||
|
||||
class GeoReferencedMixin(models.Model):
|
||||
geometry = models.ForeignKey("konova.Geometry", null=True, blank=True, on_delete=models.SET_NULL)
|
||||
@ -621,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:
|
||||
|
@ -10,8 +10,98 @@ from django.db import models
|
||||
from konova.models import UuidModel
|
||||
|
||||
|
||||
class AdministrativeSpatialReference(models.Model):
|
||||
key = models.CharField(
|
||||
max_length=255,
|
||||
help_text="Represents Kreisschlüssel",
|
||||
null=True,
|
||||
blank=True
|
||||
)
|
||||
name = models.CharField(
|
||||
max_length=1000,
|
||||
help_text="Kreis",
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.key})"
|
||||
|
||||
@property
|
||||
def table_str(self):
|
||||
return f"{self.name} ({self.key})"
|
||||
|
||||
|
||||
class District(UuidModel, AdministrativeSpatialReference):
|
||||
""" The model District refers to "Kreis"
|
||||
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=[
|
||||
"key",
|
||||
"name",
|
||||
],
|
||||
name="Unique district constraint"
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
class Municipal(UuidModel, AdministrativeSpatialReference):
|
||||
""" The model Municipal refers to "Gemeinde"
|
||||
|
||||
"""
|
||||
district = models.ForeignKey(
|
||||
District,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
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
|
||||
|
||||
"""
|
||||
municipal = models.ForeignKey(
|
||||
Municipal,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
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 the covered properties.
|
||||
""" The Parcel model holds administrative data on covered properties.
|
||||
|
||||
Due to the unique but relevant naming of the administrative data, we have to use these namings as field
|
||||
names in german. Any try to translate them to English result in strange or insufficient translations.
|
||||
@ -24,59 +114,49 @@ class Parcel(UuidModel):
|
||||
"""
|
||||
geometries = models.ManyToManyField("konova.Geometry", blank=True, related_name="parcels", through='ParcelIntersection')
|
||||
district = models.ForeignKey("konova.District", on_delete=models.SET_NULL, null=True, blank=True, related_name="parcels")
|
||||
gmrkng = models.CharField(
|
||||
max_length=1000,
|
||||
municipal = models.ForeignKey("konova.Municipal", on_delete=models.SET_NULL, null=True, blank=True, related_name="parcels")
|
||||
parcel_group = models.ForeignKey(
|
||||
"konova.ParcelGroup",
|
||||
on_delete=models.SET_NULL,
|
||||
help_text="Gemarkung",
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="parcels"
|
||||
)
|
||||
flrstck_nnr = models.CharField(
|
||||
max_length=1000,
|
||||
flr = models.IntegerField(
|
||||
help_text="Flur",
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
flrstck_nnr = models.IntegerField(
|
||||
help_text="Flurstücksnenner",
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
flrstck_zhlr = models.CharField(
|
||||
max_length=1000,
|
||||
flrstck_zhlr = models.IntegerField(
|
||||
help_text="Flurstückszähler",
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
flr = models.CharField(
|
||||
max_length=1000,
|
||||
help_text="Flur",
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
updated_on = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.gmrkng} | {self.flr} | {self.flrstck_zhlr} | {self.flrstck_nnr}"
|
||||
|
||||
|
||||
class District(UuidModel):
|
||||
""" The model District holds more coarse information, such as Kreis, Verbandsgemeinde and Gemeinde.
|
||||
|
||||
There might be the case that a geometry lies on a hundred Parcel entries but only on one District entry.
|
||||
Therefore a geometry can have a lot of relations to Parcel entries but only a few or only a single one to one
|
||||
District.
|
||||
|
||||
"""
|
||||
gmnd = models.CharField(
|
||||
max_length=1000,
|
||||
help_text="Gemeinde",
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
krs = models.CharField(
|
||||
max_length=1000,
|
||||
help_text="Kreis",
|
||||
null=True,
|
||||
blank=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.gmnd} | {self.krs}"
|
||||
return f"{self.parcel_group} | {self.flr} | {self.flrstck_zhlr} | {self.flrstck_nnr}"
|
||||
|
||||
|
||||
class ParcelIntersection(UuidModel):
|
||||
|
@ -263,3 +263,12 @@ Similar to bootstraps 'shadow-lg'
|
||||
}
|
||||
|
||||
*/
|
||||
.collapse-icn > i{
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.collapsed .collapse-icn > i{
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
.tree-label.badge{
|
||||
font-size: 90%;
|
||||
}
|
@ -11,6 +11,7 @@ https://docs.djangoproject.com/en/3.1/ref/settings/
|
||||
"""
|
||||
import os
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.conf.locale.de import formats as de_formats
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = os.path.dirname(
|
||||
@ -45,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
|
||||
|
||||
@ -162,9 +163,15 @@ LANGUAGES = [
|
||||
|
||||
USE_THOUSAND_SEPARATOR = True
|
||||
|
||||
# Regular python relevant date/datetime formatting
|
||||
DEFAULT_DATE_TIME_FORMAT = '%d.%m.%Y %H:%M:%S'
|
||||
DEFAULT_DATE_FORMAT = '%d.%m.%Y'
|
||||
|
||||
# Template relevant date/datetime formatting
|
||||
# See the Note on here: https://docs.djangoproject.com/en/3.2/ref/templates/builtins/#date
|
||||
de_formats.DATETIME_FORMAT = "d.m.Y, H:i"
|
||||
de_formats.DATE_FORMAT = "d.m.Y"
|
||||
|
||||
TIME_ZONE = 'Europe/Berlin'
|
||||
|
||||
USE_I18N = True
|
||||
@ -184,6 +191,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
|
||||
|
@ -19,6 +19,6 @@ PAGE_SIZE_OPTIONS_TUPLES = [
|
||||
(50, 50),
|
||||
(100, 100),
|
||||
]
|
||||
PAGE_SIZE_DEFAULT = 5
|
||||
PAGE_SIZE_DEFAULT = 10
|
||||
PAGE_SIZE_MAX = 100
|
||||
PAGE_DEFAULT = 1
|
||||
|
@ -1,32 +0,0 @@
|
||||
{% load i18n %}
|
||||
<div class="table-container w-100 scroll-300">
|
||||
{% if parcels|length == 0 %}
|
||||
<article class="alert alert-info">
|
||||
{% trans 'Parcels can not be calculated, since no geometry is given.' %}
|
||||
</article>
|
||||
{% else %}
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{% trans 'Kreis' %}</th>
|
||||
<th scope="col">{% trans 'Gemarkung' %}</th>
|
||||
<th scope="col">{% trans 'Parcel' %}</th>
|
||||
<th scope="col">{% trans 'Parcel counter' %}</th>
|
||||
<th scope="col">{% trans 'Parcel number' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for parcel in parcels %}
|
||||
<tr>
|
||||
<td>{{parcel.district.krs|default_if_none:"-"}}</td>
|
||||
<td>{{parcel.gmrkng|default_if_none:"-"}}</td>
|
||||
<td>{{parcel.flr|default_if_none:"-"}}</td>
|
||||
<td>{{parcel.flrstck_zhlr|default_if_none:"-"}}</td>
|
||||
<td>{{parcel.flrstck_nnr|default_if_none:"-"}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
</div>
|
@ -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 %}
|
@ -0,0 +1,49 @@
|
||||
{% load i18n l10n %}
|
||||
<div class="table-container w-100 scroll-300">
|
||||
{% if parcels|length == 0 %}
|
||||
<article class="alert alert-info">
|
||||
{% 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>
|
||||
<th scope="col">{% trans 'Municipal' %}</th>
|
||||
<th scope="col">{% trans 'Municipal key' %}</th>
|
||||
<th scope="col">{% trans 'District' %}</th>
|
||||
<th scope="col">{% trans 'District key' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for municipal in municipals %}
|
||||
<tr>
|
||||
<td>{{municipal.name}}</td>
|
||||
<td>{{municipal.key|unlocalize}}</td>
|
||||
<td>{{municipal.district.name}}</td>
|
||||
<td>{{municipal.district.key|unlocalize}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
<table id="lower-spatial-table" class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{% trans 'Parcel group' %}</th>
|
||||
<th scope="col">{% trans 'Parcel group key' %}</th>
|
||||
<th scope="col">{% trans 'Parcel' %}</th>
|
||||
<th scope="col">{% trans 'Parcel counter' %}</th>
|
||||
<th scope="col">{% trans 'Parcel number' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% include 'konova/includes/parcels/parcel_table_content.html' %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
</div>
|
@ -8,7 +8,7 @@
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div hx-trigger="every 2s" hx-get="{% url 'geometry-parcels' geom_form.instance.geometry.id %}" title="{% trans 'Loading...' %}">
|
||||
<div hx-trigger="load, every 5s" hx-get="{% url 'geometry-parcels' geom_form.instance.geometry.id %}">
|
||||
<div class="row justify-content-center">
|
||||
<span class="spinner-border rlp-r-inv" role="status"></span>
|
||||
</div>
|
19
konova/templates/konova/includes/report/qrcodes.html
Normal file
19
konova/templates/konova/includes/report/qrcodes.html
Normal file
@ -0,0 +1,19 @@
|
||||
{% load i18n %}
|
||||
|
||||
|
||||
<div class="col-sm-6 col-md-6 col-lg-6">
|
||||
<button class="btn btn-outline-default col-sm-12">
|
||||
<a href="{{qrcode.url}}" target="_blank">
|
||||
<h4>{% trans 'Open in browser' %}</h4>
|
||||
{{ qrcode.img|safe }}
|
||||
</a>
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-6 col-lg-6">
|
||||
<button class="btn btn-outline-default col-sm-12">
|
||||
<a href="{{qrcode_lanis.url}}" target="_blank">
|
||||
<h4>{% trans 'View in LANIS' %}</h4>
|
||||
{{ qrcode_lanis.img|safe }}
|
||||
</a>
|
||||
</button>
|
||||
</div>
|
@ -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, CompensationHandlerCodeAutocomplete
|
||||
ShareTeamAutocomplete, HandlerCodeAutocomplete, TeamAdminAutocomplete, CompensationHandlerCodeAutocomplete
|
||||
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
|
||||
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,9 @@ 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
|
||||
path("atcmplt/eco-accounts", EcoAccountAutocomplete.as_view(), name="accounts-autocomplete"),
|
||||
@ -57,6 +59,7 @@ urlpatterns = [
|
||||
path("atcmplt/codes/comp/handler", CompensationHandlerCodeAutocomplete.as_view(), name="codes-compensation-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:
|
||||
|
@ -17,6 +17,7 @@ IDENTIFIER_REPLACED = _("The identifier '{}' had to be changed to '{}' since ano
|
||||
ENTRY_REMOVE_MISSING_PERMISSION = _("Only conservation or registration office users are allowed to remove entries.")
|
||||
MISSING_GROUP_PERMISSION = _("You need to be part of another user group.")
|
||||
CHECKED_RECORDED_RESET = _("Status of Checked and Recorded reseted")
|
||||
RECORDED_BLOCKS_EDIT = _("Entry is recorded. To edit data, the entry first needs to be unrecorded.")
|
||||
|
||||
# SHARE
|
||||
DATA_UNSHARED = _("This data is not shared with you")
|
||||
@ -80,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
|
||||
|
@ -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",
|
||||
{},
|
||||
)
|
||||
|
||||
try:
|
||||
# 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:
|
||||
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,
|
||||
else:
|
||||
e.msg += content
|
||||
raise e
|
||||
fetched_features = content.get(
|
||||
"features",
|
||||
{},
|
||||
)
|
||||
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]
|
||||
|
||||
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
|
||||
|
@ -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
|
||||
@ -17,7 +20,7 @@ from compensation.models import Compensation, EcoAccount
|
||||
from intervention.models import Intervention
|
||||
from konova.contexts import BaseContext
|
||||
from konova.decorators import any_group_check
|
||||
from konova.models import Deadline, Geometry
|
||||
from konova.models import Deadline, Geometry, Municipal
|
||||
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
|
||||
from news.models import ServerMessage
|
||||
from konova.settings import SSO_SERVER_BASE
|
||||
@ -110,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
|
||||
@ -130,8 +133,23 @@ def get_geom_parcels(request: HttpRequest, id: str):
|
||||
status_code = 200
|
||||
|
||||
if parcels_available or no_geometry_given:
|
||||
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)
|
||||
@ -139,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
|
||||
|
||||
@ -164,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)
|
||||
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -28,6 +28,7 @@ kombu==5.2.3
|
||||
openpyxl==3.0.9
|
||||
OWSLib==0.25.0
|
||||
packaging==21.3
|
||||
pika==1.2.0
|
||||
prompt-toolkit==3.0.24
|
||||
psycopg2-binary==2.9.1
|
||||
pyparsing==3.0.6
|
||||
|
19
templates/form/recorded_no_edit.html
Normal file
19
templates/form/recorded_no_edit.html
Normal file
@ -0,0 +1,19 @@
|
||||
{% load i18n fontawesome_5 %}
|
||||
|
||||
<div class="p-5 col-sm-12">
|
||||
<h4>
|
||||
<span class="registered-bookmark">
|
||||
{% fa5_icon 'bookmark' %}
|
||||
</span>
|
||||
<span>
|
||||
{% trans 'This data is recorded' %}
|
||||
</span>
|
||||
</h4>
|
||||
<hr>
|
||||
<article>
|
||||
{% blocktrans %}
|
||||
Whilst recorded the data is published publicly. If you wish to edit any information on this data, the data needs
|
||||
to be unrecorded first. Do not forget to record it afterwards, again.
|
||||
{% endblocktrans %}
|
||||
</article>
|
||||
</div>
|
@ -11,7 +11,7 @@
|
||||
</h4>
|
||||
{% if form.form_caption is not None %}
|
||||
<small>
|
||||
{{ form.form_caption }}
|
||||
{{ form.form_caption|linebreaks }}
|
||||
</small>
|
||||
{% endif %}
|
||||
<form method="post" action="{{ form.action_url }}" {% for attr_key, attr_val in form.form_attrs.items %} {{attr_key}}="{{attr_val}}"{% endfor %}>
|
||||
|
BIN
templates/map/client/assets/logo.png
Normal file
BIN
templates/map/client/assets/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.0 KiB |
139
templates/map/client/config.json
Normal file
139
templates/map/client/config.json
Normal file
@ -0,0 +1,139 @@
|
||||
{
|
||||
"layers":
|
||||
[
|
||||
{ "folder": 5, "type": "WMS", "title": "KOM Flächen", "url": "https://geodaten.naturschutz.rlp.de/kartendienste_naturschutz/mod_ogc/wms_getmap.php?mapfile=kom_f&", "name": "kom_f" },
|
||||
{ "folder": 5, "type": "WMS", "title": "KOM Linien", "url": "https://geodaten.naturschutz.rlp.de/kartendienste_naturschutz/mod_ogc/wms_getmap.php?mapfile=kom_l&", "name": "kom_l" },
|
||||
{ "folder": 5, "type": "WMS", "title": "KOM Punkte", "url": "https://geodaten.naturschutz.rlp.de/kartendienste_naturschutz/mod_ogc/wms_getmap.php?mapfile=komon&", "name": "kom_p" },
|
||||
|
||||
{ "folder": 6, "type": "WMS", "title": "EIV Flächen", "url": "https://geodaten.naturschutz.rlp.de/kartendienste_naturschutz/mod_ogc/wms_getmap.php?mapfile=eiv_f&", "name": "eiv_f" },
|
||||
{ "folder": 6, "type": "WMS", "title": "EIV Linien", "url": "https://geodaten.naturschutz.rlp.de/kartendienste_naturschutz/mod_ogc/wms_getmap.php?mapfile=eiv_l&", "name": "eiv_l" },
|
||||
{ "folder": 6, "type": "WMS", "title": "EIV Punkte", "url": "https://geodaten.naturschutz.rlp.de/kartendienste_naturschutz/mod_ogc/wms_getmap.php?mapfile=eiv_p&", "name": "eiv_p" },
|
||||
|
||||
{ "folder": 7, "type": "WMS", "title": "OEK Flächen", "url": "https://geodaten.naturschutz.rlp.de/kartendienste_naturschutz/mod_ogc/wms_getmap.php?mapfile=oek_f&", "name": "oek_f" },
|
||||
|
||||
{ "folder": 8, "type": "WMS", "title": "EMA Flächen", "url": "https://geodaten.naturschutz.rlp.de/kartendienste_naturschutz/mod_ogc/wms_getmap.php?mapfile=ema_f&", "name": "ema_f" },
|
||||
|
||||
{ "folder": 9, "type": "WMS", "title": "MAE Flächen", "url": "https://geodaten.naturschutz.rlp.de/kartendienste_naturschutz/mod_ogc/wms_getmap.php?mapfile=mae&", "name": "mae" },
|
||||
|
||||
{ "folder": 10, "type": "WMS", "title": "Naturschutzgebiete", "url": "https://geodaten.naturschutz.rlp.de/kartendienste_naturschutz/mod_ogc/wms_getmap.php?mapfile=naturschutzgebiet&", "name": "naturschutzgebiet" },
|
||||
{ "folder": 10, "type": "WMS", "title": "Naturparkzonen", "url": "https://geodaten.naturschutz.rlp.de/kartendienste_naturschutz/mod_ogc/wms_getmap.php?mapfile=naturparkzonen&", "name": "naturparkzonen" },
|
||||
{ "folder": 10, "type": "WMS", "title": "Landschaftsschutzgebiete", "url": "https://geodaten.naturschutz.rlp.de/kartendienste_naturschutz/mod_ogc/wms_getmap.php?mapfile=landschaftsschutzgebiet&", "name": "landschaftsschutzgebiet" },
|
||||
{ "folder": 10, "type": "WMS", "title": "Vogelschutzgebiete", "url": "https://geodaten.naturschutz.rlp.de/kartendienste_naturschutz/mod_ogc/wms_getmap.php?mapfile=vogelschutzgebiet&", "name": "vogelschutzgebiet" },
|
||||
{ "folder": 10, "type": "WMS", "title": "FFH Gebiete", "url": "https://geodaten.naturschutz.rlp.de/kartendienste_naturschutz/mod_ogc/wms_getmap.php?mapfile=ffh&", "name": "ffh" },
|
||||
|
||||
{ "folder": 11, "type": "WMS", "title": "Nationalpark Zonen", "url": "https://geodaten.naturschutz.rlp.de/kartendienste_naturschutz/mod_ogc/wms_getmap.php?mapfile=nationalpark_zonen&", "name": "nationalpark_zonen" },
|
||||
{ "folder": 11, "type": "WMS", "title": "Nationalpark Grenzen", "url": "https://geodaten.naturschutz.rlp.de/kartendienste_naturschutz/mod_ogc/wms_getmap.php?mapfile=nationalpark_grenze&", "name": "nationalpark_grenze" },
|
||||
|
||||
{ "folder": 12, "type": "WMS", "title": "Naturräume Grenzen", "url": "https://geodaten.naturschutz.rlp.de/kartendienste_naturschutz/mod_ogc/wms_getmap.php?mapfile=natraum_lkompvo_grenzen&", "name": "natraum_lkompvo_grenzen" },
|
||||
{ "folder": 12, "type": "WMS", "title": "Naturräume Flächen", "url": "https://geodaten.naturschutz.rlp.de/kartendienste_naturschutz/mod_ogc/wms_getmap.php?mapfile=natraum_lkompvo&", "name": "natraum_lkompvo" },
|
||||
|
||||
{ "folder": 1, "type": "WMS", "title": "Lagebezeichnungen", "url": "https://geo5.service24.rlp.de/wms/liegenschaften_rp.fcgi?", "name": "Lagebezeichnungen" },
|
||||
{ "folder": 1, "type": "WMS", "title": "Flurstücke", "url": "https://geo5.service24.rlp.de/wms/liegenschaften_rp.fcgi?", "name": "Flurstueck", "active": true},
|
||||
{ "folder": 1, "type": "WMS", "title": "Gebäude / Bauwerke", "url": "https://geo5.service24.rlp.de/wms/liegenschaften_rp.fcgi?", "name": "GebaeudeBauwerke" },
|
||||
{ "folder": 1, "type": "WMS", "title": "Nutzung", "url": "https://geo5.service24.rlp.de/wms/liegenschaften_rp.fcgi?", "name": "Nutzung" },
|
||||
|
||||
{ "folder": 2, "type": "WMS", "title": "Landkreise", "url": "http://geo5.service24.rlp.de/wms/verwaltungsgrenzen_rp.fcgi?", "name": "Landkreise" },
|
||||
{ "folder": 2, "type": "WMS", "title": "Verbandsgemeinden", "url": "http://geo5.service24.rlp.de/wms/verwaltungsgrenzen_rp.fcgi?", "name": "Verbandsgemeinden" },
|
||||
{ "folder": 2, "type": "WMS", "title": "Gemeinden", "url": "http://geo5.service24.rlp.de/wms/verwaltungsgrenzen_rp.fcgi?", "name": "Gemeinden" },
|
||||
|
||||
{ "folder": 0, "type": "WMS", "title": "Webatlas farbig", "attribution": "LVermGeo", "url": "https://maps.service24.rlp.de/gisserver/services/RP/RP_WebAtlasRP/MapServer/WmsServer?", "name": "RP_WebAtlasRP", "active": true },
|
||||
{ "folder": 0, "type": "WMS", "title": "Webatlas grau", "attribution": "LVermGeo", "url": "https://maps.service24.rlp.de/gisserver/services/RP/RP_ETRS_Gt/MapServer/WmsServer?", "name": "0", "active": false },
|
||||
{ "folder": 0, "type": "WMS", "title": "Luftbilder", "attribution": "LVermGeo", "url": "http://geo4.service24.rlp.de/wms/dop_basis.fcgi?", "name": "rp_dop", "active": false },
|
||||
{ "folder": 0, "type": "WMS", "title": "TopPlusOpen", "attribution": "BKG", "url": "https://sgx.geodatenzentrum.de/wms_topplus_open?", "name": "web", "active": false },
|
||||
{ "folder": 0, "type": "OSM", "title": "Open Street Map", "attribution": "OSM", "active": false }
|
||||
],
|
||||
|
||||
"folders":
|
||||
[
|
||||
{ "title": "Hintergrund", "parent": -1 },
|
||||
{ "title": "ALKIS Liegenschaften", "parent": -1 },
|
||||
{ "title": "Verwaltungsgrenzen", "parent": -1 },
|
||||
{ "title": "Geofachdaten", "parent": -1 },
|
||||
{ "title": "Kompensationsverzeichnis", "parent": 3 },
|
||||
{ "title": "Kompensationen", "parent": 4 },
|
||||
{ "title": "Eingriffe", "parent": 4 },
|
||||
{ "title": "Ökokonten", "parent": 4 },
|
||||
{ "title": "EMA", "parent": 4 },
|
||||
{ "title": "MAE", "parent": 4 },
|
||||
{ "title": "Schutzgebiete", "parent": 3 },
|
||||
{ "title": "Nationalparke", "parent": 10 },
|
||||
{ "title": "Naturräume", "parent": 10 }
|
||||
],
|
||||
|
||||
"projections":
|
||||
[
|
||||
[ "EPSG:25832", "+proj=utm +zone=32 +ellps=GRS80 +units=m +no_defs" ]
|
||||
],
|
||||
|
||||
"map":
|
||||
{
|
||||
"projection": "EPSG:25832",
|
||||
"center": [ 385000, 5543000 ],
|
||||
"minZoom": 5,
|
||||
"maxZoom": 22,
|
||||
"zoom": 9,
|
||||
"attribution": "LANIS RLP"
|
||||
},
|
||||
|
||||
"output":
|
||||
{
|
||||
"id": "netgis-storage"
|
||||
},
|
||||
|
||||
"search":
|
||||
{
|
||||
"url": "/client/proxy?https://www.geoportal.rlp.de/mapbender/geoportal/gaz_geom_mobile.php?outputFormat=json&resultTarget=web&searchEPSG={epsg}&maxResults=5&maxRows=5&featureClass=P&style=full&searchText={q}&name_startsWith={q}"
|
||||
},
|
||||
|
||||
"export":
|
||||
{
|
||||
"logo": "/static/assets/logo.png",
|
||||
"gifWebWorker": "/static/libs/gifjs/0.2.0/gif.worker.js",
|
||||
"defaultFilename": "Export",
|
||||
"defaultMargin": 10
|
||||
},
|
||||
|
||||
"tools":
|
||||
{
|
||||
"buffer":
|
||||
{
|
||||
"defaultRadius": 2,
|
||||
"defaultSegments": 2
|
||||
}
|
||||
},
|
||||
|
||||
"styles":
|
||||
{
|
||||
"editLayer":
|
||||
{
|
||||
"fill": "rgba( 255, 0, 0, 0.2 )",
|
||||
"stroke": "#ff0000",
|
||||
"strokeWidth": 3,
|
||||
"pointRadius": 6
|
||||
},
|
||||
|
||||
"select":
|
||||
{
|
||||
"fill": "rgba( 0, 127, 255, 0.5 )",
|
||||
"stroke": "#007fff",
|
||||
"strokeWidth": 3,
|
||||
"pointRadius": 6
|
||||
},
|
||||
|
||||
"sketch":
|
||||
{
|
||||
"fill": "rgba( 0, 127, 255, 0.2 )",
|
||||
"stroke": "#0080ff",
|
||||
"strokeWidth": 3,
|
||||
"pointRadius": 6
|
||||
},
|
||||
|
||||
"modify":
|
||||
{
|
||||
"fill": "rgba( 0, 127, 255, 0.5 )",
|
||||
"stroke": "#0080ff",
|
||||
"strokeWidth": 3,
|
||||
"pointRadius": 6
|
||||
}
|
||||
}
|
||||
}
|
33
templates/map/client/index.html
Normal file
33
templates/map/client/index.html
Normal file
@ -0,0 +1,33 @@
|
||||
{% load static %}
|
||||
<!-- Library Styles -->
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'fontawesome/5.12.0/css/all.min.css' %}" />
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'openlayers/6.14.1/ol.css' %}" />
|
||||
|
||||
<!-- Client Styles -->
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'netgis.min.css' %}" />
|
||||
|
||||
<main id="container" {% if geom_form.read_only %}data-editable="false"{% else %}data-editable="true"{% endif %} style="position: relative; width: 100%; height: 100%;">
|
||||
</main>
|
||||
|
||||
<!--<main id="container" contenteditable="false" style="position: absolute; width: 100%; height: 100%; left: 0mm; top: 0mm;">
|
||||
</main>-->
|
||||
|
||||
<input type="hidden" id="netgis-storage" name="geom" value="{{geom_form.fields.geom.initial}}"/>
|
||||
|
||||
<!-- Library Scripts -->
|
||||
<script type="text/javascript" src="{% static 'openlayers/6.14.1/ol.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'proj4js/2.6.0/proj4.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'jsts/1.6.1/jsts.min.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'shapefilejs/4.0.2/shp.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'jspdf/1.3.2/jspdf.min.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'gifjs/0.2.0/gif.js' %}"></script>
|
||||
|
||||
<!-- Client Scripts -->
|
||||
<script type="text/javascript" src="{% static 'netgis.min.js' %}"></script>
|
||||
|
||||
<script type="text/javascript">
|
||||
|
||||
// Create Client Instance
|
||||
var client = new netgis.Client( "container", "{% static 'config.json' %}" );
|
||||
|
||||
</script>
|
34
templates/map/client/libs/fontawesome/5.12.0/LICENSE.txt
Normal file
34
templates/map/client/libs/fontawesome/5.12.0/LICENSE.txt
Normal file
@ -0,0 +1,34 @@
|
||||
Font Awesome Free License
|
||||
-------------------------
|
||||
|
||||
Font Awesome Free is free, open source, and GPL friendly. You can use it for
|
||||
commercial projects, open source projects, or really almost whatever you want.
|
||||
Full Font Awesome Free license: https://fontawesome.com/license/free.
|
||||
|
||||
# Icons: CC BY 4.0 License (https://creativecommons.org/licenses/by/4.0/)
|
||||
In the Font Awesome Free download, the CC BY 4.0 license applies to all icons
|
||||
packaged as SVG and JS file types.
|
||||
|
||||
# Fonts: SIL OFL 1.1 License (https://scripts.sil.org/OFL)
|
||||
In the Font Awesome Free download, the SIL OFL license applies to all icons
|
||||
packaged as web and desktop font files.
|
||||
|
||||
# Code: MIT License (https://opensource.org/licenses/MIT)
|
||||
In the Font Awesome Free download, the MIT license applies to all non-font and
|
||||
non-icon files.
|
||||
|
||||
# Attribution
|
||||
Attribution is required by MIT, SIL OFL, and CC BY licenses. Downloaded Font
|
||||
Awesome Free files already contain embedded comments with sufficient
|
||||
attribution, so you shouldn't need to do anything additional when using these
|
||||
files normally.
|
||||
|
||||
We've kept attribution comments terse, so we ask that you do not actively work
|
||||
to remove them from files, especially code. They're a great way for folks to
|
||||
learn about Font Awesome.
|
||||
|
||||
# Brand Icons
|
||||
All brand icons are trademarks of their respective owners. The use of these
|
||||
trademarks does not indicate endorsement of the trademark holder by Font
|
||||
Awesome, nor vice versa. **Please do not use brand logos for any purpose except
|
||||
to represent the company, product, or service to which they refer.**
|
4450
templates/map/client/libs/fontawesome/5.12.0/css/all.css
vendored
Normal file
4450
templates/map/client/libs/fontawesome/5.12.0/css/all.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
5
templates/map/client/libs/fontawesome/5.12.0/css/all.min.css
vendored
Normal file
5
templates/map/client/libs/fontawesome/5.12.0/css/all.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
14
templates/map/client/libs/fontawesome/5.12.0/css/brands.css
vendored
Normal file
14
templates/map/client/libs/fontawesome/5.12.0/css/brands.css
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
/*!
|
||||
* Font Awesome Free 5.12.0 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||
*/
|
||||
@font-face {
|
||||
font-family: 'Font Awesome 5 Brands';
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-display: auto;
|
||||
src: url("../webfonts/fa-brands-400.eot");
|
||||
src: url("../webfonts/fa-brands-400.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-brands-400.woff2") format("woff2"), url("../webfonts/fa-brands-400.woff") format("woff"), url("../webfonts/fa-brands-400.ttf") format("truetype"), url("../webfonts/fa-brands-400.svg#fontawesome") format("svg"); }
|
||||
|
||||
.fab {
|
||||
font-family: 'Font Awesome 5 Brands'; }
|
5
templates/map/client/libs/fontawesome/5.12.0/css/brands.min.css
vendored
Normal file
5
templates/map/client/libs/fontawesome/5.12.0/css/brands.min.css
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
/*!
|
||||
* Font Awesome Free 5.12.0 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||
*/
|
||||
@font-face{font-family:"Font Awesome 5 Brands";font-style:normal;font-weight:normal;font-display:auto;src:url(../webfonts/fa-brands-400.eot);src:url(../webfonts/fa-brands-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.woff) format("woff"),url(../webfonts/fa-brands-400.ttf) format("truetype"),url(../webfonts/fa-brands-400.svg#fontawesome) format("svg")}.fab{font-family:"Font Awesome 5 Brands"}
|
4417
templates/map/client/libs/fontawesome/5.12.0/css/fontawesome.css
vendored
Normal file
4417
templates/map/client/libs/fontawesome/5.12.0/css/fontawesome.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user