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:
mpeltriaux 2022-06-01 13:12:35 +02:00
commit dd5cbbef10
160 changed files with 62451 additions and 794 deletions

View File

@ -6,6 +6,7 @@
"title": "Test_compensation", "title": "Test_compensation",
"is_cef": false, "is_cef": false,
"is_coherence_keeping": false, "is_coherence_keeping": false,
"is_pik": false,
"intervention": "MUST_BE_SET_IN_TEST", "intervention": "MUST_BE_SET_IN_TEST",
"before_states": [ "before_states": [
], ],

View File

@ -5,6 +5,7 @@
"properties": { "properties": {
"title": "Test_ecoaccount", "title": "Test_ecoaccount",
"deductable_surface": 10000.0, "deductable_surface": 10000.0,
"is_pik": false,
"responsible": { "responsible": {
"conservation_office": null, "conservation_office": null,
"conservation_file_number": null, "conservation_file_number": null,

View File

@ -4,6 +4,7 @@
], ],
"properties": { "properties": {
"title": "Test_ema", "title": "Test_ema",
"is_pik": false,
"responsible": { "responsible": {
"conservation_office": null, "conservation_office": null,
"conservation_file_number": null, "conservation_file_number": null,

View File

@ -122,6 +122,7 @@ class APIV1GetTestCase(BaseAPIV1TestCase):
props = geojson["properties"] props = geojson["properties"]
props["is_cef"] props["is_cef"]
props["is_coherence_keeping"] props["is_coherence_keeping"]
props["is_pik"]
props["intervention"] props["intervention"]
props["intervention"]["id"] props["intervention"]["id"]
props["intervention"]["identifier"] props["intervention"]["identifier"]

View File

@ -46,6 +46,7 @@
"title": "TEST_compensation_CHANGED", "title": "TEST_compensation_CHANGED",
"is_cef": true, "is_cef": true,
"is_coherence_keeping": true, "is_coherence_keeping": true,
"is_pik": true,
"intervention": "CHANGE_BEFORE_RUN!!!", "intervention": "CHANGE_BEFORE_RUN!!!",
"before_states": [], "before_states": [],
"after_states": [], "after_states": [],

View File

@ -45,6 +45,7 @@
"properties": { "properties": {
"title": "TEST_account_CHANGED", "title": "TEST_account_CHANGED",
"deductable_surface": "100000.0", "deductable_surface": "100000.0",
"is_pik": true,
"responsible": { "responsible": {
"conservation_office": null, "conservation_office": null,
"conservation_file_number": "123-TEST", "conservation_file_number": "123-TEST",

View File

@ -52,6 +52,7 @@
"detail": "TEST_HANDLER_CHANGED" "detail": "TEST_HANDLER_CHANGED"
} }
}, },
"is_pik": true,
"before_states": [], "before_states": [],
"after_states": [], "after_states": [],
"actions": [], "actions": [],

View File

@ -97,6 +97,7 @@ class APIV1UpdateTestCase(BaseAPIV1TestCase):
self.assertNotEqual(modified_on, self.compensation.modified) self.assertNotEqual(modified_on, self.compensation.modified)
self.assertEqual(put_props["is_cef"], self.compensation.is_cef) 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_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["actions"]), self.compensation.actions.count())
self.assertEqual(len(put_props["before_states"]), self.compensation.before_states.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()) self.assertEqual(len(put_props["after_states"]), self.compensation.after_states.count())

View File

@ -34,6 +34,7 @@ class CompensationAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensa
def _extend_properties_data(self, entry): def _extend_properties_data(self, entry):
self.properties_data["is_cef"] = entry.is_cef self.properties_data["is_cef"] = entry.is_cef
self.properties_data["is_coherence_keeping"] = entry.is_coherence_keeping 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["intervention"] = self.intervention_to_json(entry.intervention)
self.properties_data["before_states"] = self._compensation_state_to_json(entry.before_states.all()) 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()) 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.title = properties["title"]
obj.is_cef = properties["is_cef"] obj.is_cef = properties["is_cef"]
obj.is_coherence_keeping = properties["is_coherence_keeping"] 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 = self.set_intervention(obj, properties["intervention"], user)
obj.geometry.save() obj.geometry.save()
@ -149,6 +151,7 @@ class CompensationAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensa
obj.title = properties["title"] obj.title = properties["title"]
obj.is_cef = properties["is_cef"] obj.is_cef = properties["is_cef"]
obj.is_coherence_keeping = properties["is_coherence_keeping"] obj.is_coherence_keeping = properties["is_coherence_keeping"]
obj.is_pik = properties.get("is_pik", False)
obj.modified = update_action obj.modified = update_action
obj.geometry.geom = self._create_geometry_from_json(json_model) obj.geometry.geom = self._create_geometry_from_json(json_model)
obj.geometry.modified = update_action obj.geometry.modified = update_action

View File

@ -25,6 +25,7 @@ class EcoAccountAPISerializerV1(AbstractModelAPISerializerV1,
model = EcoAccount model = EcoAccount
def _extend_properties_data(self, entry): 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"] = entry.deductable_surface
self.properties_data["deductable_surface_available"] = entry.deductable_surface - entry.get_deductions_surface() self.properties_data["deductable_surface_available"] = entry.deductable_surface - entry.get_deductions_surface()
self.properties_data["responsible"] = self._responsible_to_json(entry.responsible) self.properties_data["responsible"] = self._responsible_to_json(entry.responsible)
@ -122,6 +123,7 @@ class EcoAccountAPISerializerV1(AbstractModelAPISerializerV1,
properties = json_model["properties"] properties = json_model["properties"]
obj.identifier = obj.generate_new_identifier() obj.identifier = obj.generate_new_identifier()
obj.title = properties["title"] obj.title = properties["title"]
obj.is_pik = properties.get("is_pik", False)
try: try:
obj.deductable_surface = float(properties["deductable_surface"]) obj.deductable_surface = float(properties["deductable_surface"])
@ -169,6 +171,7 @@ class EcoAccountAPISerializerV1(AbstractModelAPISerializerV1,
# Fill in data to objects # Fill in data to objects
properties = json_model["properties"] properties = json_model["properties"]
obj.title = properties["title"] obj.title = properties["title"]
obj.is_pik = properties.get("is_pik", False)
obj.deductable_surface = float(properties["deductable_surface"]) obj.deductable_surface = float(properties["deductable_surface"])
obj.modified = update_action obj.modified = update_action
obj.geometry.geom = self._create_geometry_from_json(json_model) obj.geometry.geom = self._create_geometry_from_json(json_model)

View File

@ -21,6 +21,7 @@ class EmaAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensationAPISe
model = Ema model = Ema
def _extend_properties_data(self, entry): 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["responsible"] = self._responsible_to_json(entry.responsible)
self.properties_data["before_states"] = self._compensation_state_to_json(entry.before_states.all()) 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()) 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"] properties = json_model["properties"]
obj.identifier = obj.generate_new_identifier() obj.identifier = obj.generate_new_identifier()
obj.title = properties["title"] obj.title = properties["title"]
obj.is_pik = properties.get("is_pik", False)
obj = self._set_responsibility(obj, properties["responsible"]) obj = self._set_responsibility(obj, properties["responsible"])
obj.geometry.save() obj.geometry.save()
@ -141,6 +143,7 @@ class EmaAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensationAPISe
# Fill in data to objects # Fill in data to objects
properties = json_model["properties"] properties = json_model["properties"]
obj.title = properties["title"] obj.title = properties["title"]
obj.is_pik = properties.get("is_pik", False)
obj.modified = update_action obj.modified = update_action
obj.geometry.geom = self._create_geometry_from_json(json_model) obj.geometry.geom = self._create_geometry_from_json(json_model)
obj.geometry.modified = update_action obj.geometry.modified = update_action

View File

@ -132,6 +132,7 @@ class InterventionAPISerializerV1(AbstractModelAPISerializerV1,
id__in=payments id__in=payments
) )
obj.payments.set(payments) obj.payments.set(payments)
obj.send_data_to_egon()
return obj return obj
def create_model_from_json(self, json_model, user): def create_model_from_json(self, json_model, user):
@ -197,7 +198,7 @@ class InterventionAPISerializerV1(AbstractModelAPISerializerV1,
obj.legal.save() obj.legal.save()
obj.save() obj.save()
obj.mark_as_edited(user) obj.mark_as_edited(user, edit_comment="API update")
celery_update_parcels.delay(obj.geometry.id) celery_update_parcels.delay(obj.geometry.id)

View File

@ -75,7 +75,10 @@ class AbstractModelAPISerializerV1(AbstractModelAPISerializer):
Returns: 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 return None
code = KonovaCode.objects.get( code = KonovaCode.objects.get(
atom_id=json_str, atom_id=json_str,

View File

@ -33,6 +33,7 @@ class KonovaCodeAdmin(admin.ModelAdmin):
"is_selectable", "is_selectable",
"is_leaf", "is_leaf",
"parent", "parent",
"found_in_codelists",
] ]
search_fields = [ search_fields = [
@ -42,6 +43,12 @@ class KonovaCodeAdmin(admin.ModelAdmin):
"short_name", "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(KonovaCodeList, KonovaCodeListAdmin)
admin.site.register(KonovaCode, KonovaCodeAdmin) admin.site.register(KonovaCode, KonovaCodeAdmin)

View File

@ -65,24 +65,23 @@ class KonovaCode(models.Model):
ret_val += ", " + self.parent.long_name ret_val += ", " + self.parent.long_name
return ret_val 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 """ Adds all children (resurcively until leaf) as .children to the KonovaCode
Returns: Returns:
code (KonovaCode): The manipulated KonovaCode instance code (KonovaCode): The manipulated KonovaCode instance
""" """
if self.is_leaf: if self.is_leaf:
return None return self
children = KonovaCode.objects.filter( children = KonovaCode.objects.filter(
code_lists__in=self.code_lists.all(),
parent=self parent=self
).order_by( ).order_by(
"long_name" order_by
) )
self.children = children self.children = children
for child in children: for child in children:
child.add_children() child.add_children(order_by)
return self return self

View File

@ -21,16 +21,30 @@ class AbstractCompensationAdmin(BaseObjectAdmin):
"identifier", "identifier",
"title", "title",
"comment", "comment",
"after_states", "list_after_states",
"before_states", "list_before_states",
"geometry",
] ]
def get_readonly_fields(self, request, obj=None): def get_readonly_fields(self, request, obj=None):
return super().get_readonly_fields(request, obj) + [ return super().get_readonly_fields(request, obj) + [
"after_states", "list_after_states",
"before_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): class CompensationAdmin(AbstractCompensationAdmin):
autocomplete_fields = [ autocomplete_fields = [
@ -41,6 +55,7 @@ class CompensationAdmin(AbstractCompensationAdmin):
return super().get_fields(request, obj) + [ return super().get_fields(request, obj) + [
"is_cef", "is_cef",
"is_coherence_keeping", "is_coherence_keeping",
"is_pik",
"intervention", "intervention",
] ]

View File

@ -60,7 +60,7 @@ class CheckboxCompensationTableFilter(CheckboxTableFilter):
if not value: if not value:
return queryset.filter( return queryset.filter(
Q(intervention__users__in=[self.user]) | # requesting user has access 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() ).distinct()
else: else:
return queryset return queryset

View File

@ -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. """ Form for creating new compensations.
Can be initialized with an intervention id for preselecting the related intervention. Can be initialized with an intervention id for preselecting the related intervention.
@ -191,6 +207,7 @@ class NewCompensationForm(AbstractCompensationForm, CEFCompensationFormMixin, Co
"identifier", "identifier",
"title", "title",
"intervention", "intervention",
"is_pik",
"is_cef", "is_cef",
"is_coherence_keeping", "is_coherence_keeping",
"comment", "comment",
@ -234,6 +251,7 @@ class NewCompensationForm(AbstractCompensationForm, CEFCompensationFormMixin, Co
intervention = self.cleaned_data.get("intervention", None) intervention = self.cleaned_data.get("intervention", None)
is_cef = self.cleaned_data.get("is_cef", None) is_cef = self.cleaned_data.get("is_cef", None)
is_coherence_keeping = self.cleaned_data.get("is_coherence_keeping", 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) comment = self.cleaned_data.get("comment", None)
# Create log entry # Create log entry
@ -249,6 +267,7 @@ class NewCompensationForm(AbstractCompensationForm, CEFCompensationFormMixin, Co
created=action, created=action,
is_cef=is_cef, is_cef=is_cef,
is_coherence_keeping=is_coherence_keeping, is_coherence_keeping=is_coherence_keeping,
is_pik=is_pik,
geometry=geometry, geometry=geometry,
comment=comment, comment=comment,
) )
@ -281,6 +300,7 @@ class EditCompensationForm(NewCompensationForm):
"intervention": self.instance.intervention, "intervention": self.instance.intervention,
"is_cef": self.instance.is_cef, "is_cef": self.instance.is_cef,
"is_coherence_keeping": self.instance.is_coherence_keeping, "is_coherence_keeping": self.instance.is_coherence_keeping,
"is_pik": self.instance.is_pik,
"comment": self.instance.comment, "comment": self.instance.comment,
} }
disabled_fields = [] disabled_fields = []
@ -297,6 +317,7 @@ class EditCompensationForm(NewCompensationForm):
intervention = self.cleaned_data.get("intervention", None) intervention = self.cleaned_data.get("intervention", None)
is_cef = self.cleaned_data.get("is_cef", None) is_cef = self.cleaned_data.get("is_cef", None)
is_coherence_keeping = self.cleaned_data.get("is_coherence_keeping", 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) comment = self.cleaned_data.get("comment", None)
# Create log entry # Create log entry
@ -313,6 +334,7 @@ class EditCompensationForm(NewCompensationForm):
self.instance.is_cef = is_cef self.instance.is_cef = is_cef
self.instance.is_coherence_keeping = is_coherence_keeping self.instance.is_coherence_keeping = is_coherence_keeping
self.instance.comment = comment self.instance.comment = comment
self.instance.is_pik = is_pik
self.instance.modified = action self.instance.modified = action
self.instance.save() self.instance.save()
@ -322,7 +344,7 @@ class EditCompensationForm(NewCompensationForm):
return self.instance return self.instance
class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMixin): class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMixin, PikCompensationFormMixin):
""" Form for creating eco accounts """ Form for creating eco accounts
Inherits from basic AbstractCompensationForm and further form fields from CompensationResponsibleFormMixin Inherits from basic AbstractCompensationForm and further form fields from CompensationResponsibleFormMixin
@ -363,6 +385,7 @@ class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMix
"registration_date", "registration_date",
"surface", "surface",
"conservation_file_number", "conservation_file_number",
"is_pik",
"handler_type", "handler_type",
"handler_detail", "handler_detail",
"comment", "comment",
@ -392,6 +415,7 @@ class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMix
surface = self.cleaned_data.get("surface", None) surface = self.cleaned_data.get("surface", None)
conservation_office = self.cleaned_data.get("conservation_office", None) conservation_office = self.cleaned_data.get("conservation_office", None)
conservation_file_number = self.cleaned_data.get("conservation_file_number", 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) comment = self.cleaned_data.get("comment", None)
# Create log entry # Create log entry
@ -423,6 +447,7 @@ class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMix
created=action, created=action,
geometry=geometry, geometry=geometry,
comment=comment, comment=comment,
is_pik=is_pik,
legal=legal legal=legal
) )
acc.share_with_user(user) acc.share_with_user(user)
@ -458,6 +483,7 @@ class EditEcoAccountForm(NewEcoAccountForm):
"registration_date": reg_date, "registration_date": reg_date,
"conservation_office": self.instance.responsible.conservation_office, "conservation_office": self.instance.responsible.conservation_office,
"conservation_file_number": self.instance.responsible.conservation_file_number, "conservation_file_number": self.instance.responsible.conservation_file_number,
"is_pik": self.instance.is_pik,
"comment": self.instance.comment, "comment": self.instance.comment,
} }
disabled_fields = [] disabled_fields = []
@ -478,6 +504,7 @@ class EditEcoAccountForm(NewEcoAccountForm):
conservation_office = self.cleaned_data.get("conservation_office", None) conservation_office = self.cleaned_data.get("conservation_office", None)
conservation_file_number = self.cleaned_data.get("conservation_file_number", None) conservation_file_number = self.cleaned_data.get("conservation_file_number", None)
comment = self.cleaned_data.get("comment", None) comment = self.cleaned_data.get("comment", None)
is_pik = self.cleaned_data.get("is_pik", None)
# Create log entry # Create log entry
action = UserActionLogEntry.get_edited_action(user) action = UserActionLogEntry.get_edited_action(user)
@ -503,6 +530,7 @@ class EditEcoAccountForm(NewEcoAccountForm):
self.instance.deductable_surface = surface self.instance.deductable_surface = surface
self.instance.geometry = geometry self.instance.geometry = geometry
self.instance.comment = comment self.instance.comment = comment
self.instance.is_pik = is_pik
self.instance.modified = action self.instance.modified = action
self.instance.save() self.instance.save()

View File

@ -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, \ from codelist.settings import CODELIST_BIOTOPES_ID, CODELIST_COMPENSATION_ACTION_ID, CODELIST_BIOTOPES_EXTRA_CODES_ID, \
CODELIST_COMPENSATION_ACTION_DETAIL_ID CODELIST_COMPENSATION_ACTION_DETAIL_ID
from compensation.models import CompensationDocument, EcoAccountDocument from compensation.models import CompensationDocument, EcoAccountDocument
from intervention.inputs import CompensationActionTreeCheckboxSelectMultiple from intervention.inputs import CompensationActionTreeCheckboxSelectMultiple, \
CompensationStateTreeRadioSelect
from konova.contexts import BaseContext from konova.contexts import BaseContext
from konova.forms import BaseModalForm, NewDocumentModalForm, RemoveModalForm from konova.forms import BaseModalForm, NewDocumentModalForm, RemoveModalForm
from konova.models import DeadlineType from konova.models import DeadlineType
@ -128,6 +129,7 @@ class EditPaymentModalForm(NewPaymentForm):
payment.comment = self.cleaned_data.get("comment", None) payment.comment = self.cleaned_data.get("comment", None)
payment.save() payment.save()
self.instance.mark_as_edited(self.user, self.request, edit_comment=PAYMENT_EDITED) self.instance.mark_as_edited(self.user, self.request, edit_comment=PAYMENT_EDITED)
self.instance.send_data_to_egon()
return payment 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')? 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=_("Biotope Type"),
label_suffix="", label_suffix="",
required=True, required=True,
help_text=_("Select the biotope type"), help_text=_("Select the biotope type"),
queryset=KonovaCode.objects.filter( widget=CompensationStateTreeRadioSelect(),
is_archived=False,
is_leaf=True,
code_lists__in=[CODELIST_BIOTOPES_ID],
),
widget=autocomplete.ModelSelect2(
url="codes-biotope-autocomplete",
attrs={
"data-placeholder": _("Biotope Type"),
}
),
) )
biotope_extra = forms.ModelMultipleChoiceField( biotope_extra = forms.ModelMultipleChoiceField(
label=_("Biotope additional type"), label=_("Biotope additional type"),
@ -208,6 +200,16 @@ class NewStateModalForm(BaseModalForm):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.form_title = _("New state") self.form_title = _("New state")
self.form_caption = _("Insert data for the 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): def save(self, is_before_state: bool = False):
state = self.instance.add_state(self, is_before_state) state = self.instance.add_state(self, is_before_state)
@ -270,8 +272,9 @@ class EditCompensationStateModalForm(NewStateModalForm):
self.state = kwargs.pop("state", None) self.state = kwargs.pop("state", None)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.form_title = _("Edit state") self.form_title = _("Edit state")
biotope_type_id = self.state.biotope_type.id if self.state.biotope_type else None
form_data = { form_data = {
"biotope_type": self.state.biotope_type, "biotope_type": biotope_type_id,
"biotope_extra": self.state.biotope_type_details.all(), "biotope_extra": self.state.biotope_type_details.all(),
"surface": self.state.surface, "surface": self.state.surface,
} }
@ -279,7 +282,8 @@ class EditCompensationStateModalForm(NewStateModalForm):
def save(self, is_before_state: bool = False): def save(self, is_before_state: bool = False):
state = self.state 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.biotope_type_details.set(self.cleaned_data.get("biotope_extra", []))
state.surface = self.cleaned_data.get("surface", None) state.surface = self.cleaned_data.get("surface", None)
state.save() state.save()

View 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),
),
]

View File

@ -8,6 +8,8 @@ Created on: 16.11.21
import shutil import shutil
from django.contrib import messages from django.contrib import messages
from codelist.models import KonovaCode
from user.models import User, Team from user.models import User, Team
from django.db import models, transaction from django.db import models, transaction
from django.db.models import QuerySet, Sum from django.db.models import QuerySet, Sum
@ -142,8 +144,10 @@ class AbstractCompensation(BaseObject, GeoReferencedMixin):
""" """
form_data = form.cleaned_data form_data = form.cleaned_data
with transaction.atomic(): with transaction.atomic():
biotope_type_id = form_data["biotope_type"]
code = KonovaCode.objects.get(id=biotope_type_id)
state = CompensationState.objects.create( state = CompensationState.objects.create(
biotope_type=form_data["biotope_type"], biotope_type=code,
surface=form_data["surface"], surface=form_data["surface"],
) )
state_additional_types = form_data["biotope_extra"] state_additional_types = form_data["biotope_extra"]
@ -253,7 +257,22 @@ class CoherenceMixin(models.Model):
abstract = True 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 Regular compensation, linked to an intervention
""" """
@ -418,6 +437,18 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin):
super().set_status_messages(request) super().set_status_messages(request)
return 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): class CompensationDocument(AbstractDocument):
""" """

View File

@ -17,14 +17,13 @@ from django.db.models import Sum, QuerySet
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from compensation.managers import EcoAccountManager, EcoAccountDeductionManager 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 compensation.utils.quality import EcoAccountQualityChecker
from konova.models import ShareableObjectMixin, RecordableObjectMixin, AbstractDocument, BaseResource, \ from konova.models import ShareableObjectMixin, RecordableObjectMixin, AbstractDocument, BaseResource, \
generate_document_file_upload_path 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 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. with some kind of currency. From this account one is able to deduct currency for current projects.

View File

@ -5,17 +5,15 @@ Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 01.12.20 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.http import HttpRequest
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.urls import reverse from django.urls import reverse
from django.utils.html import format_html from django.utils.html import format_html
from django.utils.timezone import localtime
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from compensation.filters import CompensationTableFilter, EcoAccountTableFilter from compensation.filters import CompensationTableFilter, EcoAccountTableFilter
from compensation.models import Compensation, EcoAccount from compensation.models import Compensation, EcoAccount
from konova.sub_settings.django_settings import DEFAULT_DATE_TIME_FORMAT
from konova.utils.tables import BaseTable, TableRenderMixin from konova.utils.tables import BaseTable, TableRenderMixin
import django_tables2 as tables import django_tables2 as tables
@ -111,16 +109,21 @@ class CompensationTable(BaseTable, TableRenderMixin):
""" """
html = "" html = ""
checked = value is not None checked = value is not None
tooltip = _("Not checked yet") tooltip = DATA_IS_UNCHECKED
previously_checked = record.intervention.get_last_checked_action()
if checked: if checked:
value = value.timestamp checked_on = value.get_timestamp_str_formatted()
value = localtime(value) tooltip = DATA_CHECKED_ON_TEMPLATE.format(checked_on, record.intervention.checked.user)
checked_on = value.strftime(DEFAULT_DATE_TIME_FORMAT)
tooltip = _("Checked on {} by {}").format(checked_on, record.intervention.checked.user)
html += self.render_checked_star( html += self.render_checked_star(
tooltip=tooltip, tooltip=tooltip,
icn_filled=checked, 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) return format_html(html)
def render_d(self, value, record: Compensation): def render_d(self, value, record: Compensation):
@ -134,7 +137,7 @@ class CompensationTable(BaseTable, TableRenderMixin):
""" """
parcels = value.get_underlying_parcels().values_list( parcels = value.get_underlying_parcels().values_list(
"gmrkng", "parcel_group__name",
flat=True flat=True
).distinct() ).distinct()
html = render_to_string( html = render_to_string(
@ -159,9 +162,7 @@ class CompensationTable(BaseTable, TableRenderMixin):
recorded = value is not None recorded = value is not None
tooltip = _("Not recorded yet") tooltip = _("Not recorded yet")
if recorded: if recorded:
value = value.timestamp on = value.get_timestamp_str_formatted()
value = localtime(value)
on = value.strftime(DEFAULT_DATE_TIME_FORMAT)
tooltip = _("Recorded on {} by {}").format(on, record.intervention.recorded.user) tooltip = _("Recorded on {} by {}").format(on, record.intervention.recorded.user)
html += self.render_bookmark( html += self.render_bookmark(
tooltip=tooltip, tooltip=tooltip,
@ -179,11 +180,7 @@ class CompensationTable(BaseTable, TableRenderMixin):
Returns: Returns:
""" """
if value is None: has_access = record.is_shared_with(self.user)
value = User.objects.none()
has_access = value.filter(
id=self.user.id
).exists()
html = self.render_icn( html = self.render_icn(
tooltip=_("Full access granted") if has_access else _("Access not granted"), 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( parcels = value.get_underlying_parcels().values_list(
"gmrkng", "parcel_group__name",
flat=True flat=True
).distinct() ).distinct()
html = render_to_string( html = render_to_string(
@ -320,9 +317,7 @@ class EcoAccountTable(BaseTable, TableRenderMixin):
checked = value is not None checked = value is not None
tooltip = _("Not recorded yet. Can not be used for deductions, yet.") tooltip = _("Not recorded yet. Can not be used for deductions, yet.")
if checked: if checked:
value = value.timestamp on = value.get_timestamp_str_formatted()
value = localtime(value)
on = value.strftime(DEFAULT_DATE_TIME_FORMAT)
tooltip = _("Recorded on {} by {}").format(on, record.recorded.user) tooltip = _("Recorded on {} by {}").format(on, record.recorded.user)
html += self.render_bookmark( html += self.render_bookmark(
tooltip=tooltip, tooltip=tooltip,
@ -343,7 +338,7 @@ class EcoAccountTable(BaseTable, TableRenderMixin):
html = "" html = ""
# Do not use value in here, since value does use unprefetched 'users' manager, where record has already # Do not use value in here, since value does use unprefetched 'users' manager, where record has already
# prefetched users data # prefetched users data
has_access = self.user in record.users.all() has_access = record.is_shared_with(self.user)
html += self.render_icn( html += self.render_icn(
tooltip=_("Full access granted") if has_access else _("Access not granted"), 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", icn_class="fas fa-edit rlp-r-inv" if has_access else "far fa-edit",

View File

@ -39,6 +39,16 @@
</a> </a>
</td> </td>
</tr> </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> <tr>
<th scope="row">{% trans 'Is CEF compensation' %}</th> <th scope="row">{% trans 'Is CEF compensation' %}</th>
<td class="align-middle"> <td class="align-middle">
@ -66,6 +76,11 @@
<span> <span>
{% fa5_icon 'star' 'far' %} {% fa5_icon 'star' 'far' %}
</span> </span>
{% if last_checked %}
<span class="rlp-gd-inv" title="{{last_checked_tooltip}}">
{% fa5_icon 'star' 'fas' %}
</span>
{% endif %}
{% else %} {% else %}
<span class="check-star" title="{% trans 'Checked on '%} {{obj.intervention.checked.timestamp}} {% trans 'by' %} {{obj.intervention.checked.user}}"> <span class="check-star" title="{% trans 'Checked on '%} {{obj.intervention.checked.timestamp}} {% trans 'by' %} {{obj.intervention.checked.user}}">
{% fa5_icon 'star' %} {% fa5_icon 'star' %}
@ -90,15 +105,21 @@
<tr> <tr>
<th scope="row">{% trans 'Last modified' %}</th> <th scope="row">{% trans 'Last modified' %}</th>
<td class="align-middle"> <td class="align-middle">
{{obj.modified.timestamp|default_if_none:""|naturalday}} {% if obj.modified %}
<br> {{obj.modified.timestamp|default_if_none:""}}
{{obj.modified.user.username}} <br>
{{obj.modified.user.username}}
{% else %}
{{obj.created.timestamp|default_if_none:""}}
<br>
{{obj.created.user.username}}
{% endif %}
</td> </td>
</tr> </tr>
<tr> <tr>
<th scope="row">{% trans 'Shared with' %}</th> <th scope="row">{% trans 'Shared with' %}</th>
<td class="align-middle"> <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' %} {% include 'user/includes/team_data_modal_button.html' %}
{% endfor %} {% endfor %}
<hr> <hr>
@ -113,10 +134,12 @@
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6"> <div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
<div class="col"> <div class="col">
<div class="row"> <div class="row">
{% include 'map/geom_form.html' %} <div class="col-sm-12">
{% include 'map/geom_form.html' %}
</div>
</div> </div>
<div class="row"> <div class="row">
{% include 'konova/includes/parcels.html' %} {% include 'konova/includes/parcels/parcels.html' %}
</div> </div>
<div class="row"> <div class="row">
{% include 'konova/includes/comment_card.html' %} {% include 'konova/includes/comment_card.html' %}

View File

@ -70,18 +70,34 @@
<th scope="row">{% trans 'Action handler' %}</th> <th scope="row">{% trans 'Action handler' %}</th>
<td class="align-middle">{{obj.responsible.handler|default_if_none:""}}</td> <td class="align-middle">{{obj.responsible.handler|default_if_none:""}}</td>
</tr> </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> <tr>
<th scope="row">{% trans 'Last modified' %}</th> <th scope="row">{% trans 'Last modified' %}</th>
<td class="align-middle"> <td class="align-middle">
{{obj.modified.timestamp|default_if_none:""|naturalday}} {% if obj.modified %}
<br> {{obj.modified.timestamp|default_if_none:""}}
{{obj.modified.user.username}} <br>
{{obj.modified.user.username}}
{% else %}
{{obj.created.timestamp|default_if_none:""}}
<br>
{{obj.created.user.username}}
{% endif %}
</td> </td>
</tr> </tr>
<tr> <tr>
<th scope="row">{% trans 'Shared with' %}</th> <th scope="row">{% trans 'Shared with' %}</th>
<td class="align-middle"> <td class="align-middle">
{% for team in obj.teams.all %} {% for team in obj.shared_teams %}
{% include 'user/includes/team_data_modal_button.html' %} {% include 'user/includes/team_data_modal_button.html' %}
{% endfor %} {% endfor %}
<hr> <hr>
@ -95,10 +111,12 @@
</div> </div>
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6"> <div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
<div class="row"> <div class="row">
{% include 'map/geom_form.html' %} <div class="col-sm-12">
{% include 'map/geom_form.html' %}
</div>
</div> </div>
<div class="row"> <div class="row">
{% include 'konova/includes/parcels.html' %} {% include 'konova/includes/parcels/parcels.html' %}
</div> </div>
<div class="row"> <div class="row">
{% include 'konova/includes/comment_card.html' %} {% include 'konova/includes/comment_card.html' %}

View File

@ -20,6 +20,36 @@
</a> </a>
</td> </td>
</tr> </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> <tr>
<th scope="row">{% trans 'Last modified' %}</th> <th scope="row">{% trans 'Last modified' %}</th>
<td class="align-middle"> <td class="align-middle">
@ -35,20 +65,15 @@
</div> </div>
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6"> <div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
<div class="row"> <div class="row">
{% include 'map/geom_form.html' %} <div class="col-sm-12">
{% include 'map/geom_form.html' %}
</div>
</div> </div>
<div class="row"> <div class="row">
{% include 'konova/includes/parcels.html' %} {% include 'konova/includes/parcels/parcels.html' %}
</div> </div>
<div class="row"> <div class="row">
<div class="col-sm-6 col-md-6 col-lg-6"> {% include 'konova/includes/report/qrcodes.html' %}
<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 }}
</div>
</div> </div>
</div> </div>

View File

@ -20,6 +20,16 @@
<th scope="row">{% trans 'Conservation office file number' %}</th> <th scope="row">{% trans 'Conservation office file number' %}</th>
<td class="align-middle">{{obj.responsible.conservation_file_number|default_if_none:""}}</td> <td class="align-middle">{{obj.responsible.conservation_file_number|default_if_none:""}}</td>
</tr> </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> <tr>
<th scope="row">{% trans 'Deductions for' %}</th> <th scope="row">{% trans 'Deductions for' %}</th>
<td class="align-middle"> <td class="align-middle">
@ -48,20 +58,15 @@
</div> </div>
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6"> <div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
<div class="row"> <div class="row">
{% include 'map/geom_form.html' %} <div class="col-sm-12">
{% include 'map/geom_form.html' %}
</div>
</div> </div>
<div class="row"> <div class="row">
{% include 'konova/includes/parcels.html' %} {% include 'konova/includes/parcels/parcels.html' %}
</div> </div>
<div class="row"> <div class="row">
<div class="col-sm-6 col-md-6 col-lg-6"> {% include 'konova/includes/report/qrcodes.html' %}
<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 }}
</div>
</div> </div>
</div> </div>

View File

@ -50,18 +50,20 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
test_id = self.create_dummy_string() test_id = self.create_dummy_string()
test_title = self.create_dummy_string() test_title = self.create_dummy_string()
test_geom = self.create_dummy_geometry() test_geom = self.create_dummy_geometry()
geom_json = self.create_geojson(test_geom)
post_data = { post_data = {
"identifier": test_id, "identifier": test_id,
"title": test_title, "title": test_title,
"geom": test_geom.geojson, "geom": geom_json,
"intervention": self.intervention.id, "intervention": self.intervention.id,
} }
pre_creation_intervention_log_count = self.intervention.log.count() pre_creation_intervention_log_count = self.intervention.log.count()
# Preserve the current number of intervention's compensations # Preserve the current number of intervention's compensations
num_compensations = self.intervention.compensations.count() 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.intervention.refresh_from_db()
self.assertEqual(num_compensations + 1, self.intervention.compensations.count()) self.assertEqual(num_compensations + 1, self.intervention.compensations.count())
new_compensation = self.intervention.compensations.get(identifier=test_id) new_compensation = self.intervention.compensations.get(identifier=test_id)
@ -87,10 +89,11 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
test_id = self.create_dummy_string() test_id = self.create_dummy_string()
test_title = self.create_dummy_string() test_title = self.create_dummy_string()
test_geom = self.create_dummy_geometry() test_geom = self.create_dummy_geometry()
geom_json = self.create_geojson(test_geom)
post_data = { post_data = {
"identifier": test_id, "identifier": test_id,
"title": test_title, "title": test_title,
"geom": test_geom.geojson, "geom": geom_json,
} }
pre_creation_intervention_log_count = self.intervention.log.count() pre_creation_intervention_log_count = self.intervention.log.count()
@ -125,6 +128,7 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
new_identifier = self.create_dummy_string() new_identifier = self.create_dummy_string()
new_comment = self.create_dummy_string() new_comment = self.create_dummy_string()
new_geometry = MultiPolygon(srid=4326) # Create an empty geometry new_geometry = MultiPolygon(srid=4326) # Create an empty geometry
geojson = self.create_geojson(new_geometry)
check_on_elements = { check_on_elements = {
self.compensation.title: new_title, self.compensation.title: new_title,
@ -139,7 +143,7 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
"title": new_title, "title": new_title,
"intervention": self.intervention.id, # just keep the intervention as it is "intervention": self.intervention.id, # just keep the intervention as it is
"comment": new_comment, "comment": new_comment,
"geom": new_geometry.geojson, "geom": geojson,
} }
self.client_user.post(url, post_data) self.client_user.post(url, post_data)
self.compensation.refresh_from_db() self.compensation.refresh_from_db()
@ -261,3 +265,26 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
self.assertIn(recorded, self.compensation.log.all()) self.assertIn(recorded, self.compensation.log.all())
self.assertEqual(pre_record_log_count + 1, self.compensation.log.count()) 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)

View File

@ -40,12 +40,13 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
test_id = self.create_dummy_string() test_id = self.create_dummy_string()
test_title = self.create_dummy_string() test_title = self.create_dummy_string()
test_geom = self.create_dummy_geometry() test_geom = self.create_dummy_geometry()
geom_json = self.create_geojson(test_geom)
test_deductable_surface = 1000 test_deductable_surface = 1000
test_conservation_office = self.get_conservation_office_code() test_conservation_office = self.get_conservation_office_code()
post_data = { post_data = {
"identifier": test_id, "identifier": test_id,
"title": test_title, "title": test_title,
"geom": test_geom.geojson, "geom": geom_json,
"deductable_surface": test_deductable_surface, "deductable_surface": test_deductable_surface,
"conservation_office": test_conservation_office.id "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(pre_edit_account_log_count + 1, account.log.count())
self.assertEqual(intervention.log.first().action, UserAction.EDITED) self.assertEqual(intervention.log.first().action, UserAction.EDITED)
self.assertEqual(account.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)

View File

@ -1,4 +1,5 @@
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Sum from django.db.models import Sum
from django.http import HttpRequest, JsonResponse from django.http import HttpRequest, JsonResponse
from django.shortcuts import render 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, \ CHECKED_RECORDED_RESET, COMPENSATION_ADDED_TEMPLATE, COMPENSATION_REMOVED_TEMPLATE, DOCUMENT_ADDED, \
COMPENSATION_STATE_REMOVED, COMPENSATION_STATE_ADDED, COMPENSATION_ACTION_REMOVED, COMPENSATION_ACTION_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_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 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" 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) data_form = NewCompensationForm(request.POST or None, intervention_id=intervention_id)
geom_form = SimpleGeomForm(request.POST or None, read_only=False) geom_form = SimpleGeomForm(request.POST or None, read_only=False)
if request.method == "POST": if request.method == "POST":
@ -134,6 +148,13 @@ def edit_view(request: HttpRequest, id: str):
template = "compensation/form/view.html" template = "compensation/form/view.html"
# Get object from db # Get object from db
comp = get_object_or_404(Compensation, id=id) 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 # Create forms, initialize with values from db/from POST request
data_form = EditCompensationForm(request.POST or None, instance=comp) data_form = EditCompensationForm(request.POST or None, instance=comp)
geom_form = SimpleGeomForm(request.POST or None, read_only=False, 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) 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 = { context = {
"obj": comp, "obj": comp,
"last_checked": last_checked,
"last_checked_tooltip": last_checked_tooltip,
"geom_form": geom_form, "geom_form": geom_form,
"parcels": parcels, "parcels": parcels,
"has_access": is_data_shared, "has_access": is_data_shared,
@ -596,14 +624,12 @@ def report_view(request: HttpRequest, id: str):
instance=comp instance=comp
) )
parcels = comp.get_underlying_parcels() parcels = comp.get_underlying_parcels()
qrcode_img = generate_qr_code(
request.build_absolute_uri(reverse("compensation:report", args=(id,))), qrcode_url = request.build_absolute_uri(reverse("compensation:report", args=(id,)))
10 qrcode_img = generate_qr_code(qrcode_url, 10)
) qrcode_lanis_url = comp.get_LANIS_link()
qrcode_img_lanis = generate_qr_code( qrcode_img_lanis = generate_qr_code(qrcode_lanis_url, 7)
comp.get_LANIS_link(),
7
)
# Order states by surface # Order states by surface
before_states = comp.before_states.all().order_by("-surface").prefetch_related("biotope_type") 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") 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 = { context = {
"obj": comp, "obj": comp,
"qrcode": qrcode_img, "qrcode": {
"qrcode_lanis": qrcode_img_lanis, "img": qrcode_img,
"url": qrcode_url,
},
"qrcode_lanis": {
"img": qrcode_img_lanis,
"url": qrcode_lanis_url,
},
"has_access": False, # disables action buttons during rendering "has_access": False, # disables action buttons during rendering
"before_states": before_states, "before_states": before_states,
"after_states": after_states, "after_states": after_states,

View File

@ -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, \ 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, \ 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, \ 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 from konova.utils.user_checks import in_group
@ -145,6 +146,13 @@ def edit_view(request: HttpRequest, id: str):
template = "compensation/form/view.html" template = "compensation/form/view.html"
# Get object from db # Get object from db
acc = get_object_or_404(EcoAccount, id=id) 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 # Create forms, initialize with values from db/from POST request
data_form = EditEcoAccountForm(request.POST or None, instance=acc) data_form = EditEcoAccountForm(request.POST or None, instance=acc)
geom_form = SimpleGeomForm(request.POST or None, read_only=False, 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 instance=acc
) )
parcels = acc.get_underlying_parcels() parcels = acc.get_underlying_parcels()
qrcode_img = generate_qr_code(
request.build_absolute_uri(reverse("ema:report", args=(id,))), qrcode_url = request.build_absolute_uri(reverse("ema:report", args=(id,)))
10 qrcode_img = generate_qr_code(qrcode_url, 10)
) qrcode_lanis_url = acc.get_LANIS_link()
qrcode_img_lanis = generate_qr_code( qrcode_img_lanis = generate_qr_code(qrcode_lanis_url, 7)
acc.get_LANIS_link(),
7
)
# Order states by surface # Order states by surface
before_states = acc.before_states.all().order_by("-surface").select_related("biotope_type__parent") 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") 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) # 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()\ deductions = acc.deductions.all()\
@ -752,8 +758,14 @@ def report_view(request:HttpRequest, id: str):
context = { context = {
"obj": acc, "obj": acc,
"qrcode": qrcode_img, "qrcode": {
"qrcode_lanis": qrcode_img_lanis, "img": qrcode_img,
"url": qrcode_url,
},
"qrcode_lanis": {
"img": qrcode_img_lanis,
"url": qrcode_lanis_url,
},
"has_access": False, # disables action buttons during rendering "has_access": False, # disables action buttons during rendering
"before_states": before_states, "before_states": before_states,
"after_states": after_states, "after_states": after_states,

View File

@ -12,14 +12,15 @@ from django.db import transaction
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
from django.utils.translation import gettext_lazy as _ 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 ema.models import Ema, EmaDocument
from intervention.models import Responsibility, Handler from intervention.models import Responsibility, Handler
from konova.forms import SimpleGeomForm, NewDocumentModalForm from konova.forms import SimpleGeomForm, NewDocumentModalForm
from user.models import UserActionLogEntry from user.models import UserActionLogEntry
class NewEmaForm(AbstractCompensationForm, CompensationResponsibleFormMixin): class NewEmaForm(AbstractCompensationForm, CompensationResponsibleFormMixin, PikCompensationFormMixin):
""" Form for creating new EMA objects. """ Form for creating new EMA objects.
Inherits basic form fields from AbstractCompensationForm and additional from CompensationResponsibleFormMixin. Inherits basic form fields from AbstractCompensationForm and additional from CompensationResponsibleFormMixin.
@ -31,6 +32,7 @@ class NewEmaForm(AbstractCompensationForm, CompensationResponsibleFormMixin):
"title", "title",
"conservation_office", "conservation_office",
"conservation_file_number", "conservation_file_number",
"is_pik",
"handler_type", "handler_type",
"handler_detail", "handler_detail",
"comment", "comment",
@ -58,6 +60,7 @@ class NewEmaForm(AbstractCompensationForm, CompensationResponsibleFormMixin):
handler_detail = self.cleaned_data.get("handler_detail", None) handler_detail = self.cleaned_data.get("handler_detail", None)
conservation_office = self.cleaned_data.get("conservation_office", None) conservation_office = self.cleaned_data.get("conservation_office", None)
conservation_file_number = self.cleaned_data.get("conservation_file_number", 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) comment = self.cleaned_data.get("comment", None)
# Create log entry # Create log entry
@ -83,6 +86,7 @@ class NewEmaForm(AbstractCompensationForm, CompensationResponsibleFormMixin):
created=action, created=action,
geometry=geometry, geometry=geometry,
comment=comment, comment=comment,
is_pik=is_pik,
) )
# Add the creating user to the list of shared users # 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_office": self.instance.responsible.conservation_office,
"conservation_file_number": self.instance.responsible.conservation_file_number, "conservation_file_number": self.instance.responsible.conservation_file_number,
"comment": self.instance.comment, "comment": self.instance.comment,
"is_pik": self.instance.is_pik,
} }
disabled_fields = [] disabled_fields = []
self.load_initial_data( self.load_initial_data(
@ -133,6 +138,7 @@ class EditEmaForm(NewEmaForm):
conservation_office = self.cleaned_data.get("conservation_office", None) conservation_office = self.cleaned_data.get("conservation_office", None)
conservation_file_number = self.cleaned_data.get("conservation_file_number", None) conservation_file_number = self.cleaned_data.get("conservation_file_number", None)
comment = self.cleaned_data.get("comment", None) comment = self.cleaned_data.get("comment", None)
is_pik = self.cleaned_data.get("is_pik", None)
# Create log entry # Create log entry
action = UserActionLogEntry.get_edited_action(user) action = UserActionLogEntry.get_edited_action(user)
@ -152,6 +158,7 @@ class EditEmaForm(NewEmaForm):
self.instance.title = title self.instance.title = title
self.instance.geometry = geometry self.instance.geometry = geometry
self.instance.comment = comment self.instance.comment = comment
self.instance.is_pik = is_pik
self.instance.modified = action self.instance.modified = action
self.instance.save() self.instance.save()

View 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),
),
]

View File

@ -13,15 +13,14 @@ from django.db.models import QuerySet
from django.http import HttpRequest from django.http import HttpRequest
from django.urls import reverse from django.urls import reverse
from compensation.models import AbstractCompensation from compensation.models import AbstractCompensation, PikMixin
from ema.managers import EmaManager from ema.managers import EmaManager
from ema.utils.quality import EmaQualityChecker from ema.utils.quality import EmaQualityChecker
from konova.models import AbstractDocument, generate_document_file_upload_path, RecordableObjectMixin, ShareableObjectMixin 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 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 EMA = Ersatzzahlungsmaßnahme
(compensation actions from payments) (compensation actions from payments)

View File

@ -104,7 +104,7 @@ class EmaTable(BaseTable, TableRenderMixin):
""" """
parcels = value.get_underlying_parcels().values_list( parcels = value.get_underlying_parcels().values_list(
"gmrkng", "parcel_group__name",
flat=True flat=True
).distinct() ).distinct()
html = render_to_string( html = render_to_string(
@ -115,7 +115,6 @@ class EmaTable(BaseTable, TableRenderMixin):
) )
return html return html
def render_r(self, value, record: Ema): def render_r(self, value, record: Ema):
""" Renders the registered column for a EMA """ Renders the registered column for a EMA
@ -130,9 +129,7 @@ class EmaTable(BaseTable, TableRenderMixin):
recorded = value is not None recorded = value is not None
tooltip = _("Not recorded yet") tooltip = _("Not recorded yet")
if recorded: if recorded:
value = value.timestamp on = value.get_timestamp_str_formatted()
value = localtime(value)
on = value.strftime(DEFAULT_DATE_TIME_FORMAT)
tooltip = _("Recorded on {} by {}").format(on, record.recorded.user) tooltip = _("Recorded on {} by {}").format(on, record.recorded.user)
html += self.render_bookmark( html += self.render_bookmark(
tooltip=tooltip, tooltip=tooltip,
@ -151,9 +148,7 @@ class EmaTable(BaseTable, TableRenderMixin):
""" """
html = "" html = ""
has_access = value.filter( has_access = record.is_shared_with(self.user)
id=self.user.id
).exists()
html += self.render_icn( html += self.render_icn(
tooltip=_("Full access granted") if has_access else _("Access not granted"), tooltip=_("Full access granted") if has_access else _("Access not granted"),

View File

@ -56,29 +56,38 @@
<th scope="row">{% trans 'Action handler' %}</th> <th scope="row">{% trans 'Action handler' %}</th>
<td class="align-middle">{{obj.responsible.handler|default_if_none:""}}</td> <td class="align-middle">{{obj.responsible.handler|default_if_none:""}}</td>
</tr> </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> <tr>
<th scope="row">{% trans 'Last modified' %}</th> <th scope="row">{% trans 'Last modified' %}</th>
<td class="align-middle"> <td class="align-middle">
{% if obj.modified %} {% if obj.modified %}
{{obj.modified.timestamp|default_if_none:""|naturalday}} {{obj.modified.timestamp|default_if_none:""}}
<br> <br>
{{obj.modified.user.username}} {{obj.modified.user.username}}
{% else %} {% else %}
{{obj.created.timestamp|default_if_none:""|naturalday}} {{obj.created.timestamp|default_if_none:""}}
<br> <br>
{{obj.created.user.username}} {{obj.created.user.username}}
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
<tr> <tr>
<th scope="row">{% trans 'Shared with' %}</th> <th scope="row">{% trans 'Shared with' %}</th>
<td class="align-middle"> <td class="align-middle">
{% for team in obj.teams.all %} {% for team in obj.shared_teams %}
{% include 'user/includes/team_data_modal_button.html' %} {% include 'user/includes/team_data_modal_button.html' %}
{% endfor %} {% endfor %}
<hr> <hr>
{% for user in obj.users.all %} {% for user in obj.user.all %}
{% include 'user/includes/contact_modal_button.html' %} {% include 'user/includes/contact_modal_button.html' %}
{% endfor %} {% endfor %}
</td> </td>
@ -88,10 +97,12 @@
</div> </div>
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6"> <div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
<div class="row"> <div class="row">
{% include 'map/geom_form.html' %} <div class="col-sm-12">
{% include 'map/geom_form.html' %}
</div>
</div> </div>
<div class="row"> <div class="row">
{% include 'konova/includes/parcels.html' %} {% include 'konova/includes/parcels/parcels.html' %}
</div> </div>
<div class="row"> <div class="row">
{% include 'konova/includes/comment_card.html' %} {% include 'konova/includes/comment_card.html' %}

View File

@ -20,6 +20,16 @@
<th scope="row">{% trans 'Conservation office file number' %}</th> <th scope="row">{% trans 'Conservation office file number' %}</th>
<td class="align-middle">{{obj.responsible.conservation_file_number|default_if_none:""}}</td> <td class="align-middle">{{obj.responsible.conservation_file_number|default_if_none:""}}</td>
</tr> </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> <tr>
<th scope="row">{% trans 'Last modified' %}</th> <th scope="row">{% trans 'Last modified' %}</th>
<td class="align-middle"> <td class="align-middle">
@ -35,20 +45,15 @@
</div> </div>
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6"> <div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
<div class="row"> <div class="row">
{% include 'map/geom_form.html' %} <div class="col-sm-12">
{% include 'map/geom_form.html' %}
</div>
</div> </div>
<div class="row"> <div class="row">
{% include 'konova/includes/parcels.html' %} {% include 'konova/includes/parcels/parcels.html' %}
</div> </div>
<div class="row"> <div class="row">
<div class="col-sm-6 col-md-6 col-lg-6"> {% include 'konova/includes/report/qrcodes.html' %}
<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 }}
</div>
</div> </div>
</div> </div>

View File

@ -41,11 +41,12 @@ class EmaWorkflowTestCase(BaseWorkflowTestCase):
test_id = self.create_dummy_string() test_id = self.create_dummy_string()
test_title = self.create_dummy_string() test_title = self.create_dummy_string()
test_geom = self.create_dummy_geometry() test_geom = self.create_dummy_geometry()
geom_json = self.create_geojson(test_geom)
test_conservation_office = self.get_conservation_office_code() test_conservation_office = self.get_conservation_office_code()
post_data = { post_data = {
"identifier": test_id, "identifier": test_id,
"title": test_title, "title": test_title,
"geom": test_geom.geojson, "geom": geom_json,
"conservation_office": test_conservation_office.id "conservation_office": test_conservation_office.id
} }
self.client_user.post(new_url, post_data) 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(pre_edit_log_count + 1, self.ema.log.count())
self.assertEqual(self.ema.log.first().action, UserAction.EDITED) 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): 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) This tests if the recordability of the Ema is triggered by the quality of it's data (e.g. not all fields filled)

View File

@ -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, \ 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, \ 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_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 from konova.utils.user_checks import in_group
@ -213,6 +213,13 @@ def edit_view(request: HttpRequest, id: str):
template = "compensation/form/view.html" template = "compensation/form/view.html"
# Get object from db # Get object from db
ema = get_object_or_404(Ema, id=id) 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 # Create forms, initialize with values from db/from POST request
data_form = EditEmaForm(request.POST or None, instance=ema) data_form = EditEmaForm(request.POST or None, instance=ema)
geom_form = SimpleGeomForm(request.POST or None, read_only=False, 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, instance=ema,
) )
parcels = ema.get_underlying_parcels() parcels = ema.get_underlying_parcels()
qrcode_img = generate_qr_code(
request.build_absolute_uri(reverse("ema:report", args=(id,))), qrcode_url = request.build_absolute_uri(reverse("ema:report", args=(id,)))
10 qrcode_img = generate_qr_code(qrcode_url, 10)
) qrcode_lanis_url = ema.get_LANIS_link()
qrcode_img_lanis = generate_qr_code( qrcode_img_lanis = generate_qr_code(qrcode_lanis_url, 7)
ema.get_LANIS_link(),
7
)
# Order states by surface # Order states by surface
before_states = ema.before_states.all().order_by("-surface").prefetch_related("biotope_type") 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") 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 = { context = {
"obj": ema, "obj": ema,
"qrcode": qrcode_img, "qrcode": {
"qrcode_lanis": qrcode_img_lanis, "img": qrcode_img,
"url": qrcode_url
},
"qrcode_lanis": {
"img": qrcode_img_lanis,
"url": qrcode_lanis_url
},
"has_access": False, # disables action buttons during rendering "has_access": False, # disables action buttons during rendering
"before_states": before_states, "before_states": before_states,
"after_states": after_states, "after_states": after_states,

View File

@ -25,12 +25,14 @@ class InterventionAdmin(BaseObjectAdmin):
"checked", "checked",
"recorded", "recorded",
"users", "users",
"geometry",
] ]
def get_readonly_fields(self, request, obj=None): def get_readonly_fields(self, request, obj=None):
return super().get_readonly_fields(request, obj) + [ return super().get_readonly_fields(request, obj) + [
"checked", "checked",
"recorded", "recorded",
"geometry",
] ]

View File

@ -216,6 +216,10 @@ class NewInterventionForm(BaseForm):
identifier = tmp_intervention.generate_new_identifier() identifier = tmp_intervention.generate_new_identifier()
self.initialize_form_field("identifier", 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): def save(self, user: User, geom_form: SimpleGeomForm):
with transaction.atomic(): with transaction.atomic():
# Fetch data from cleaned POST values # Fetch data from cleaned POST values

View File

@ -427,13 +427,22 @@ class NewDeductionModalForm(BaseModalForm):
""" """
super_result = super().is_valid() super_result = super().is_valid()
acc = self.cleaned_data["account"] acc = self.cleaned_data["account"]
intervention = self.cleaned_data["intervention"]
objects_valid = True
if not acc.recorded: if not acc.recorded:
self.add_error( self.add_error(
"account", "account",
_("Eco-account {} is not recorded yet. You can only deduct from recorded accounts.").format(acc.identifier) _("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) rest_surface = self._get_available_surface(acc)
form_surface = float(self.cleaned_data["surface"]) form_surface = float(self.cleaned_data["surface"])
@ -447,7 +456,7 @@ class NewDeductionModalForm(BaseModalForm):
format_german_float(rest_surface), 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): def __create_deduction(self):
""" Creates the deduction """ Creates the deduction

View File

@ -1,6 +1,6 @@
from django import forms from django import forms
from codelist.models import KonovaCode 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): class DummyFilterInput(forms.HiddenInput):
@ -38,7 +38,17 @@ class TreeCheckboxSelectMultiple(forms.CheckboxSelectMultiple):
""" Provides multiple selection of parent-child data """ 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: class meta:
abstract = True abstract = True
@ -68,6 +78,30 @@ class KonovaCodeTreeCheckboxSelectMultiple(TreeCheckboxSelectMultiple):
return context 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): class CompensationActionTreeCheckboxSelectMultiple(KonovaCodeTreeCheckboxSelectMultiple):
""" Provides multiple selection of CompensationActions """ Provides multiple selection of CompensationActions
@ -79,4 +113,31 @@ class CompensationActionTreeCheckboxSelectMultiple(KonovaCodeTreeCheckboxSelectM
self.filter = { self.filter = {
"code_lists__in": [CODELIST_COMPENSATION_ACTION_ID], "code_lists__in": [CODELIST_COMPENSATION_ACTION_ID],
"parent": None, "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

View File

@ -13,6 +13,7 @@ from django.db.models.fields.files import FieldFile
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from intervention.tasks import celery_export_to_egon
from user.models import User from user.models import User
from django.db import models, transaction from django.db import models, transaction
from django.db.models import QuerySet from django.db.models import QuerySet
@ -131,6 +132,16 @@ class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, Chec
self.add_log_entry_to_compensations(log_entry) self.add_log_entry_to_compensations(log_entry)
return 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: def set_recorded(self, user: User) -> UserActionLogEntry:
log_entry = super().set_recorded(user) log_entry = super().set_recorded(user)
self.add_log_entry_to_compensations(log_entry) self.add_log_entry_to_compensations(log_entry)
@ -171,6 +182,8 @@ class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, Chec
intervention=self, intervention=self,
) )
self.mark_as_edited(user, form.request, edit_comment=PAYMENT_ADDED) self.mark_as_edited(user, form.request, edit_comment=PAYMENT_ADDED)
self.send_data_to_egon()
return pay return pay
def add_revocation(self, form): def add_revocation(self, form):
@ -335,6 +348,7 @@ class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, Chec
with transaction.atomic(): with transaction.atomic():
payment.delete() payment.delete()
self.mark_as_edited(user, request=form.request, edit_comment=PAYMENT_REMOVED) self.mark_as_edited(user, request=form.request, edit_comment=PAYMENT_REMOVED)
self.send_data_to_egon()
class InterventionDocument(AbstractDocument): class InterventionDocument(AbstractDocument):

View File

@ -6,4 +6,11 @@ Created on: 30.11.20
""" """
INTERVENTION_IDENTIFIER_LENGTH = 6 INTERVENTION_IDENTIFIER_LENGTH = 6
INTERVENTION_IDENTIFIER_TEMPLATE = "EIV-{}" 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"

View File

@ -9,12 +9,11 @@ from django.http import HttpRequest
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.urls import reverse from django.urls import reverse
from django.utils.html import format_html from django.utils.html import format_html
from django.utils.timezone import localtime
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from intervention.filters import InterventionTableFilter from intervention.filters import InterventionTableFilter
from intervention.models import Intervention 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 from konova.utils.tables import BaseTable, TableRenderMixin
import django_tables2 as tables import django_tables2 as tables
@ -108,16 +107,21 @@ class InterventionTable(BaseTable, TableRenderMixin):
""" """
html = "" html = ""
checked = value is not None checked = value is not None
tooltip = _("Not checked yet") previously_checked = record.get_last_checked_action()
tooltip = DATA_IS_UNCHECKED
if checked: if checked:
value = value.timestamp checked_on = value.get_timestamp_str_formatted()
value = localtime(value) tooltip = DATA_CHECKED_ON_TEMPLATE.format(checked_on, record.checked.user)
checked_on = value.strftime(DEFAULT_DATE_TIME_FORMAT)
tooltip = _("Checked on {} by {}").format(checked_on, record.checked.user)
html += self.render_checked_star( html += self.render_checked_star(
tooltip=tooltip, tooltip=tooltip,
icn_filled=checked, 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) return format_html(html)
def render_d(self, value, record: Intervention): def render_d(self, value, record: Intervention):
@ -131,7 +135,7 @@ class InterventionTable(BaseTable, TableRenderMixin):
""" """
parcels = value.get_underlying_parcels().values_list( parcels = value.get_underlying_parcels().values_list(
"gmrkng", "parcel_group__name",
flat=True flat=True
).distinct() ).distinct()
html = render_to_string( html = render_to_string(
@ -156,9 +160,7 @@ class InterventionTable(BaseTable, TableRenderMixin):
checked = value is not None checked = value is not None
tooltip = _("Not recorded yet") tooltip = _("Not recorded yet")
if checked: if checked:
value = value.timestamp on = value.get_timestamp_str_formatted()
value = localtime(value)
on = value.strftime(DEFAULT_DATE_TIME_FORMAT)
tooltip = _("Recorded on {} by {}").format(on, record.recorded.user) tooltip = _("Recorded on {} by {}").format(on, record.recorded.user)
html += self.render_bookmark( html += self.render_bookmark(
tooltip=tooltip, tooltip=tooltip,
@ -177,9 +179,7 @@ class InterventionTable(BaseTable, TableRenderMixin):
""" """
html = "" html = ""
has_access = value.filter( has_access = record.is_shared_with(self.user)
id=self.user.id
).exists()
html += self.render_icn( html += self.render_icn(
tooltip=_("Full access granted") if has_access else _("Access not granted"), tooltip=_("Full access granted") if has_access else _("Access not granted"),

18
intervention/tasks.py Normal file
View 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()

View File

@ -1,5 +1,5 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load i18n l10n static fontawesome_5 humanize %} {% load i18n l10n static fontawesome_5 %}
{% block head %} {% block head %}
{% comment %} {% comment %}
@ -70,6 +70,11 @@
<span> <span>
{% fa5_icon 'star' 'far' %} {% fa5_icon 'star' 'far' %}
</span> </span>
{% if last_checked %}
<span class="rlp-gd-inv" title="{{last_checked_tooltip}}">
{% fa5_icon 'star' 'fas' %}
</span>
{% endif %}
{% else %} {% else %}
<span class="check-star" title="{% trans 'Checked on '%} {{obj.checked.timestamp}} {% trans 'by' %} {{obj.checked.user}}"> <span class="check-star" title="{% trans 'Checked on '%} {{obj.checked.timestamp}} {% trans 'by' %} {{obj.checked.user}}">
{% fa5_icon 'star' %} {% fa5_icon 'star' %}
@ -106,15 +111,21 @@
<tr> <tr>
<th scope="row">{% trans 'Last modified' %}</th> <th scope="row">{% trans 'Last modified' %}</th>
<td class="align-middle"> <td class="align-middle">
{{obj.created.timestamp|default_if_none:""|naturalday}} {% if obj.modified %}
<br> {{obj.modified.timestamp|default_if_none:""}}
{{obj.created.user.username}} <br>
{{obj.modified.user.username}}
{% else %}
{{obj.created.timestamp|default_if_none:""}}
<br>
{{obj.created.user.username}}
{% endif %}
</td> </td>
</tr> </tr>
<tr> <tr>
<th scope="row">{% trans 'Shared with' %}</th> <th scope="row">{% trans 'Shared with' %}</th>
<td class="align-middle"> <td class="align-middle">
{% for team in obj.teams.all %} {% for team in obj.shared_teams %}
{% include 'user/includes/team_data_modal_button.html' %} {% include 'user/includes/team_data_modal_button.html' %}
{% endfor %} {% endfor %}
<hr> <hr>
@ -128,10 +139,12 @@
</div> </div>
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6"> <div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
<div class="row"> <div class="row">
{% include 'map/geom_form.html' %} <div class="col-sm-12">
{% include 'map/geom_form.html' %}
</div>
</div> </div>
<div class="row"> <div class="row">
{% include 'konova/includes/parcels.html' %} {% include 'konova/includes/parcels/parcels.html' %}
</div> </div>
<div class="row"> <div class="row">
{% include 'konova/includes/comment_card.html' %} {% include 'konova/includes/comment_card.html' %}

View File

@ -94,20 +94,15 @@
</div> </div>
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6"> <div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
<div class="row"> <div class="row">
{% include 'map/geom_form.html' %} <div class="col-sm-12">
{% include 'map/geom_form.html' %}
</div>
</div> </div>
<div class="row"> <div class="row">
{% include 'konova/includes/parcels.html' %} {% include 'konova/includes/parcels/parcels.html' %}
</div> </div>
<div class="row"> <div class="row">
<div class="col-sm-6 col-md-6 col-lg-6"> {% include 'konova/includes/report/qrcodes.html' %}
<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 }}
</div>
</div> </div>
</div> </div>

View File

@ -46,6 +46,7 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase):
test_id = self.create_dummy_string() test_id = self.create_dummy_string()
test_title = self.create_dummy_string() test_title = self.create_dummy_string()
test_geom = self.create_dummy_geometry() test_geom = self.create_dummy_geometry()
geom_json = self.create_geojson(test_geom)
new_url = reverse("intervention:new", args=()) new_url = reverse("intervention:new", args=())
@ -59,7 +60,7 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase):
post_data = { post_data = {
"identifier": test_id, "identifier": test_id,
"title": test_title, "title": test_title,
"geom": test_geom.geojson, "geom": geom_json,
} }
response = self.client_user.post( response = self.client_user.post(
new_url, new_url,
@ -89,6 +90,30 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase):
self.assertIn(self.superuser, obj.users.all()) self.assertIn(self.superuser, obj.users.all())
self.assertEqual(1, obj.users.count()) 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): def test_checkability(self):
""" Tests that the intervention can only be checked if all required data has been added """ Tests that the intervention can only be checked if all required data has been added

View 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

View File

@ -18,7 +18,8 @@ from konova.utils.documents import remove_document, get_document
from konova.utils.generators import generate_qr_code from konova.utils.generators import generate_qr_code
from konova.utils.message_templates import INTERVENTION_INVALID, FORM_INVALID, IDENTIFIER_REPLACED, \ from konova.utils.message_templates import INTERVENTION_INVALID, FORM_INVALID, IDENTIFIER_REPLACED, \
CHECKED_RECORDED_RESET, DEDUCTION_REMOVED, DEDUCTION_ADDED, REVOCATION_ADDED, REVOCATION_REMOVED, \ 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 from konova.utils.user_checks import in_group
@ -264,15 +265,18 @@ def detail_view(request: HttpRequest, id: str):
geom_form = SimpleGeomForm( geom_form = SimpleGeomForm(
instance=intervention, instance=intervention,
) )
last_checked = intervention.get_last_checked_action()
parcels = intervention.get_underlying_parcels() last_checked_tooltip = ""
if last_checked:
last_checked_tooltip = DATA_CHECKED_PREVIOUSLY_TEMPLATE.format(last_checked.get_timestamp_str_formatted(), last_checked.user)
context = { context = {
"obj": intervention, "obj": intervention,
"last_checked": last_checked,
"last_checked_tooltip": last_checked_tooltip,
"compensations": compensations, "compensations": compensations,
"has_access": is_data_shared, "has_access": is_data_shared,
"geom_form": geom_form, "geom_form": geom_form,
"parcels": parcels,
"is_default_member": in_group(_user, DEFAULT_GROUP), "is_default_member": in_group(_user, DEFAULT_GROUP),
"is_zb_member": in_group(_user, ZB_GROUP), "is_zb_member": in_group(_user, ZB_GROUP),
"is_ets_member": in_group(_user, ETS_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" template = "intervention/form/view.html"
# Get object from db # Get object from db
intervention = get_object_or_404(Intervention, id=id) 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 # Create forms, initialize with values from db/from POST request
data_form = EditInterventionForm(request.POST or None, instance=intervention) data_form = EditInterventionForm(request.POST or None, instance=intervention)
geom_form = SimpleGeomForm(request.POST or None, read_only=False, 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( distinct_deductions = intervention.deductions.all().distinct(
"account" "account"
) )
qrcode_img = generate_qr_code( qrcode_url = request.build_absolute_uri(reverse("intervention:report", args=(id,)))
request.build_absolute_uri(reverse("intervention:report", args=(id,))), qrcode_img = generate_qr_code(qrcode_url, 10)
10 qrcode_lanis_url = intervention.get_LANIS_link()
) qrcode_img_lanis = generate_qr_code(qrcode_lanis_url, 7)
qrcode_img_lanis = generate_qr_code(
intervention.get_LANIS_link(),
7
)
context = { context = {
"obj": intervention, "obj": intervention,
"deductions": distinct_deductions, "deductions": distinct_deductions,
"qrcode": qrcode_img, "qrcode": {
"qrcode_lanis": qrcode_img_lanis, "img": qrcode_img,
"url": qrcode_url,
},
"qrcode_lanis": {
"img": qrcode_img_lanis,
"url": qrcode_lanis_url,
},
"geom_form": geom_form, "geom_form": geom_form,
"parcels": parcels, "parcels": parcels,
TAB_TITLE_IDENTIFIER: tab_title, TAB_TITLE_IDENTIFIER: tab_title,

View File

@ -7,7 +7,8 @@ Created on: 22.07.21
""" """
from django.contrib import admin 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 konova.utils.message_templates import COMPENSATION_REMOVED_TEMPLATE
from user.models import UserAction from user.models import UserAction
@ -16,13 +17,28 @@ class GeometryAdmin(admin.ModelAdmin):
list_display = [ list_display = [
"id", "id",
"created", "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): class ParcelAdmin(admin.ModelAdmin):
list_display = [ list_display = [
"id", "id",
"gmrkng", "parcel_group",
"flr", "flr",
"flrstck_nnr", "flrstck_nnr",
"flrstck_zhlr", "flrstck_zhlr",
@ -32,9 +48,27 @@ class ParcelAdmin(admin.ModelAdmin):
class DistrictAdmin(admin.ModelAdmin): class DistrictAdmin(admin.ModelAdmin):
list_display = [ 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", "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): class BaseResourceAdmin(admin.ModelAdmin):
fields = [ fields = [
"created", "created",
@ -75,7 +121,7 @@ class BaseResourceAdmin(admin.ModelAdmin):
] ]
class BaseObjectAdmin(BaseResourceAdmin): class BaseObjectAdmin(BaseResourceAdmin, DeletableObjectMixinAdmin):
search_fields = [ search_fields = [
"identifier", "identifier",
"title", "title",
@ -92,18 +138,13 @@ class BaseObjectAdmin(BaseResourceAdmin):
"deleted", "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 # Outcommented for a cleaner admin backend on production
#admin.site.register(Geometry, GeometryAdmin) #admin.site.register(Geometry, GeometryAdmin)
#admin.site.register(Parcel, ParcelAdmin) #admin.site.register(Parcel, ParcelAdmin)
#admin.site.register(District, DistrictAdmin) #admin.site.register(District, DistrictAdmin)
#admin.site.register(Municipal, MunicipalAdmin)
#admin.site.register(ParcelGroup, ParcelGroupAdmin)
#admin.site.register(GeometryConflict, GeometryConflictAdmin) #admin.site.register(GeometryConflict, GeometryConflictAdmin)
#admin.site.register(Deadline, DeadlineAdmin) #admin.site.register(Deadline, DeadlineAdmin)

View File

@ -53,14 +53,16 @@ class InterventionAutocomplete(Select2QuerySetView):
""" """
def get_queryset(self): def get_queryset(self):
if self.request.user.is_anonymous: user = self.request.user
if user.is_anonymous:
return Intervention.objects.none() return Intervention.objects.none()
qs = Intervention.objects.filter( qs = Intervention.objects.filter(
deleted=None, Q(deleted=None) &
users__in=[self.request.user], Q(users__in=[user]) |
Q(teams__in=user.teams.all())
).order_by( ).order_by(
"identifier" "identifier"
) ).distinct()
if self.q: if self.q:
qs = qs.filter( qs = qs.filter(
Q(identifier__icontains=self.q) | Q(identifier__icontains=self.q) |
@ -95,7 +97,9 @@ class ShareTeamAutocomplete(Select2QuerySetView):
def get_queryset(self): def get_queryset(self):
if self.request.user.is_anonymous: if self.request.user.is_anonymous:
return Team.objects.none() return Team.objects.none()
qs = Team.objects.all() qs = Team.objects.filter(
deleted__isnull=True
)
if self.q: if self.q:
# Due to privacy concerns only a full username match will return the proper user entry # Due to privacy concerns only a full username match will return the proper user entry
qs = qs.filter( qs = qs.filter(
@ -107,6 +111,29 @@ class ShareTeamAutocomplete(Select2QuerySetView):
return qs 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): class KonovaCodeAutocomplete(Select2GroupQuerySetView):
""" """
Provides simple autocomplete functionality for codes Provides simple autocomplete functionality for codes

View File

@ -145,26 +145,20 @@ class GeoReferencedTableFilterMixin(django_filters.FilterSet):
class Meta: class Meta:
abstract = True abstract = True
def _filter_parcel_reference(self, queryset, name, value, filter_value) -> QuerySet: def _filter_parcel_reference(self, queryset, filter_q) -> QuerySet:
""" Filters the parcel entries by a given filter_value. """ Filters the parcel entries by a given filter_q
filter_value may already include further filter annotations like 'xy__icontains'
Args: Args:
queryset (): queryset (QuerySet): The queryset
name (): filter_q (Q): The Q-style filter expression
value ():
filter_value ():
Returns: Returns:
""" """
_filter = {
filter_value: value
}
matching_parcels = Parcel.objects.filter( matching_parcels = Parcel.objects.filter(
**_filter filter_q
) )
related_geoms = matching_parcels.values( related_geoms = matching_parcels.values(
"geometries" "geometries"
).distinct() ).distinct()
@ -185,8 +179,9 @@ class GeoReferencedTableFilterMixin(django_filters.FilterSet):
""" """
matching_districts = District.objects.filter( matching_districts = District.objects.filter(
krs__icontains=value Q(name__icontains=value) |
) Q(key__icontains=value)
).distinct()
matching_parcels = Parcel.objects.filter( matching_parcels = Parcel.objects.filter(
district__in=matching_districts district__in=matching_districts
) )
@ -209,7 +204,10 @@ class GeoReferencedTableFilterMixin(django_filters.FilterSet):
Returns: 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 return queryset
def filter_parcel(self, queryset, name, value) -> QuerySet: def filter_parcel(self, queryset, name, value) -> QuerySet:
@ -224,7 +222,10 @@ class GeoReferencedTableFilterMixin(django_filters.FilterSet):
""" """
value = value.replace("-", "") value = value.replace("-", "")
queryset = self._filter_parcel_reference(queryset, name, value, "flr") queryset = self._filter_parcel_reference(
queryset,
Q(flr=value),
)
return queryset return queryset
def filter_parcel_counter(self, queryset, name, value) -> QuerySet: def filter_parcel_counter(self, queryset, name, value) -> QuerySet:
@ -239,7 +240,10 @@ class GeoReferencedTableFilterMixin(django_filters.FilterSet):
""" """
value = value.replace("-", "") 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 return queryset
def filter_parcel_number(self, queryset, name, value) -> QuerySet: def filter_parcel_number(self, queryset, name, value) -> QuerySet:
@ -254,7 +258,10 @@ class GeoReferencedTableFilterMixin(django_filters.FilterSet):
""" """
value = value.replace("-", "") 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 return queryset
@ -298,7 +305,7 @@ class ShareableTableFilterMixin(django_filters.FilterSet):
if not value: if not value:
return queryset.filter( return queryset.filter(
Q(users__in=[self.user]) | # requesting user has access Q(users__in=[self.user]) | # requesting user has access
Q(teams__users__in=[self.user]) Q(teams__in=self.user.shared_teams)
).distinct() ).distinct()
else: else:
return queryset return queryset

View File

@ -5,18 +5,20 @@ Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 16.11.20 Created on: 16.11.20
""" """
import json
from abc import abstractmethod from abc import abstractmethod
from bootstrap_modal_forms.forms import BSModalForm from bootstrap_modal_forms.forms import BSModalForm
from bootstrap_modal_forms.utils import is_ajax from bootstrap_modal_forms.utils import is_ajax
from django import forms from django import forms
from django.contrib import messages from django.contrib import messages
from django.contrib.gis import gdal
from django.db.models.fields.files import FieldFile from django.db.models.fields.files import FieldFile
from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP
from user.models import User from user.models import User
from django.contrib.gis.forms import OSMWidget, MultiPolygonField from django.contrib.gis.forms import MultiPolygonField
from django.contrib.gis.geos import MultiPolygon from django.contrib.gis.geos import MultiPolygon, Polygon
from django.db import transaction from django.db import transaction
from django.http import HttpRequest, HttpResponseRedirect from django.http import HttpRequest, HttpResponseRedirect
from django.shortcuts import render from django.shortcuts import render
@ -57,6 +59,8 @@ class BaseForm(forms.Form):
self.has_required_fields = True self.has_required_fields = True
break break
self.check_for_recorded_instance()
@abstractmethod @abstractmethod
def save(self): def save(self):
# To be implemented in subclasses! # To be implemented in subclasses!
@ -136,6 +140,38 @@ class BaseForm(forms.Form):
set_class = set_class.replace(cls, "") set_class = set_class.replace(cls, "")
self.fields[field].widget.attrs["class"] = set_class 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): class RemoveForm(BaseForm):
check = forms.BooleanField( check = forms.BooleanField(
@ -238,41 +274,85 @@ class SimpleGeomForm(BaseForm):
""" A geometry form for rendering geometry read-only using a widget """ A geometry form for rendering geometry read-only using a widget
""" """
read_only = True
geom = MultiPolygonField( geom = MultiPolygonField(
srid=DEFAULT_SRID, srid=DEFAULT_SRID_RLP,
label=_("Geometry"), label=_("Geometry"),
help_text=_(""), help_text=_(""),
label_suffix="", label_suffix="",
required=False, required=False,
disabled=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): def __init__(self, *args, **kwargs):
read_only = kwargs.pop("read_only", True) self.read_only = kwargs.pop("read_only", True)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
# Initialize geometry # Initialize geometry
try: try:
geom = self.instance.geometry.geom geom = self.instance.geometry.geom
self.empty = geom.empty 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: except AttributeError:
# If no geometry exists for this form, we simply set the value to None and zoom to the maximum level # 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.empty = True
self.fields["geom"].widget.attrs["default_zoom"] = 1
self.initialize_form_field("geom", geom) 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): def save(self, action: UserActionLogEntry):
""" Saves the form's geometry """ Saves the form's geometry
@ -410,7 +490,6 @@ class NewDocumentModalForm(BaseModalForm):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.form_title = _("Add new document") self.form_title = _("Add new document")
self.form_caption = _("") self.form_caption = _("")
self.template = "modal/modal_form.html"
self.form_attrs = { self.form_attrs = {
"enctype": "multipart/form-data", # important for file upload "enctype": "multipart/form-data", # important for file upload
} }
@ -597,4 +676,12 @@ class RecordModalForm(BaseModalForm):
self.instance.set_unrecorded(self.user) self.instance.set_unrecorded(self.user)
else: else:
self.instance.set_recorded(self.user) self.instance.set_recorded(self.user)
return self.instance return self.instance
def check_for_recorded_instance(self):
""" Overwrite the check method for doing nothing on the RecordModalForm
Returns:
"""
pass

View File

@ -9,7 +9,7 @@ from compensation.models import CompensationState, Compensation, EcoAccount, Com
from ema.models import Ema from ema.models import Ema
from intervention.models import Intervention from intervention.models import Intervention
from konova.management.commands.setup import BaseKonovaCommand 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 from user.models import UserActionLogEntry, UserAction
@ -271,13 +271,26 @@ class Command(BaseKonovaCommand):
self._write_success("No unused states found.") self._write_success("No unused states found.")
self._break_line() 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): def sanitize_parcels_and_districts(self):
""" Removes unattached parcels and districts """ Removes unattached parcels and districts
Returns: Returns:
""" """
self._write_warning("=== Sanitize parcels and districts ===") self._write_warning("=== Sanitize administrative spatial references ===")
unrelated_parcels = Parcel.objects.filter( unrelated_parcels = Parcel.objects.filter(
geometries=None, geometries=None,
) )
@ -289,16 +302,12 @@ class Command(BaseKonovaCommand):
else: else:
self._write_success("No unrelated parcels found.") self._write_success("No unrelated parcels found.")
unrelated_districts = District.objects.filter( sub_types = [
parcels=None, District,
) Municipal,
num_unrelated_districts = unrelated_districts.count() ParcelGroup
if num_unrelated_districts > 0: ]
self._write_error(f"Found {num_unrelated_districts} unrelated district entries. Delete now...") for sub_type in sub_types:
unrelated_districts.delete() self.__sanitize_parcel_sub_type(sub_type)
self._write_success("Unrelated districts deleted.")
else:
self._write_success("No unrelated districts found.")
self._break_line()
self._break_line()

View File

@ -5,10 +5,13 @@ Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 04.01.22 Created on: 04.01.22
""" """
import datetime
from pyexpat import ExpatError from pyexpat import ExpatError
from requests.exceptions import ProxyError from requests.exceptions import ProxyError
from django.contrib.gis.db.models.functions import Area
from konova.management.commands.setup import BaseKonovaCommand from konova.management.commands.setup import BaseKonovaCommand
from konova.models import Geometry, Parcel, District from konova.models import Geometry, Parcel, District
@ -32,8 +35,11 @@ class Command(BaseKonovaCommand):
num_parcels_before = Parcel.objects.count() num_parcels_before = Parcel.objects.count()
num_districts_before = District.objects.count() num_districts_before = District.objects.count()
self._write_warning("=== Update parcels and districts ===") self._write_warning("=== Update parcels and districts ===")
# Order geometries by size to process smaller once at first
geometries = Geometry.objects.all().exclude( geometries = Geometry.objects.all().exclude(
geom=None geom=None
).annotate(area=Area("geom")).order_by(
'area'
) )
if ids is not None: if ids is not None:

View 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'),
),
]

View 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),
),
]

View 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),
),
]

View 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'),
),
]

View File

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

View File

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

View File

@ -5,11 +5,15 @@ Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 15.11.21 Created on: 15.11.21
""" """
import json
from django.contrib.gis.db.models import MultiPolygonField 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 django.utils import timezone
from konova.models import BaseResource, UuidModel from konova.models import BaseResource, UuidModel
from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP
from konova.utils.wfs.spatial import ParcelWFSFetcher from konova.utils.wfs.spatial import ParcelWFSFetcher
@ -20,6 +24,9 @@ class Geometry(BaseResource):
from konova.settings import DEFAULT_SRID from konova.settings import DEFAULT_SRID
geom = MultiPolygonField(null=True, blank=True, srid=DEFAULT_SRID) geom = MultiPolygonField(null=True, blank=True, srid=DEFAULT_SRID)
def __str__(self):
return str(self.id)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
super().save(*args, **kwargs) super().save(*args, **kwargs)
self.check_for_conflicts() self.check_for_conflicts()
@ -93,13 +100,14 @@ class Geometry(BaseResource):
objs += set_objs objs += set_objs
return objs return objs
@transaction.atomic
def update_parcels(self): def update_parcels(self):
""" Updates underlying parcel information """ Updates underlying parcel information
Returns: Returns:
""" """
from konova.models import Parcel, District, ParcelIntersection from konova.models import Parcel, District, ParcelIntersection, Municipal, ParcelGroup
parcel_fetcher = ParcelWFSFetcher( parcel_fetcher = ParcelWFSFetcher(
geometry_id=self.id, geometry_id=self.id,
) )
@ -110,20 +118,38 @@ class Geometry(BaseResource):
_now = timezone.now() _now = timezone.now()
underlying_parcels = [] underlying_parcels = []
for result in fetched_parcels: for result in fetched_parcels:
fetched_parcel = result[typename] parcel_properties = result["properties"]
# There could be parcels which include the word 'Flur', # There could be parcels which include the word 'Flur',
# which needs to be deleted and just keep the numerical values # which needs to be deleted and just keep the numerical values
## THIS CAN BE REMOVED IN THE FUTURE, WHEN 'Flur' WON'T OCCUR ANYMORE! ## THIS CAN BE REMOVED IN THE FUTURE, WHEN 'Flur' WON'T OCCUR ANYMORE!
flr_val = fetched_parcel["ave:flur"].replace("Flur ", "") flr_val = parcel_properties["flur"].replace("Flur ", "")
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]
district = District.objects.get_or_create( district = District.objects.get_or_create(
gmnd=fetched_parcel["ave:gemeinde"], key=parcel_properties["kreisschl"],
krs=fetched_parcel["ave:kreis"], 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] )[0]
parcel_obj.district = district parcel_obj.district = district
parcel_obj.updated_on = _now parcel_obj.updated_on = _now
@ -131,6 +157,7 @@ class Geometry(BaseResource):
underlying_parcels.append(parcel_obj) underlying_parcels.append(parcel_obj)
# Update the linked parcels # Update the linked parcels
self.parcels.clear()
self.parcels.set(underlying_parcels) self.parcels.set(underlying_parcels)
# Set the calculated_on intermediate field, so this related data will be found on lookups # Set the calculated_on intermediate field, so this related data will be found on lookups
@ -151,17 +178,55 @@ class Geometry(BaseResource):
Returns: Returns:
parcels (QuerySet): The related parcels as queryset parcels (QuerySet): The related parcels as queryset
""" """
parcels = self.parcels.filter( parcels = self.parcels.filter(
parcelintersection__calculated_on__isnull=False, parcelintersection__calculated_on__isnull=False,
).prefetch_related( ).prefetch_related(
"district" "district",
"municipal",
).order_by( ).order_by(
"gmrkng", "municipal__name",
) )
return parcels 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): class GeometryConflict(UuidModel):
""" """

View File

@ -87,25 +87,15 @@ class BaseResource(UuidModel):
super().delete() super().delete()
class BaseObject(BaseResource): class DeletableObjectMixin(models.Model):
""" """ Wraps deleted field and related functionality
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)
deleted = models.ForeignKey("user.UserActionLogEntry", on_delete=models.SET_NULL, null=True, blank=True, related_name='+') 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: class Meta:
abstract = True abstract = True
@abstractmethod
def set_status_messages(self, request: HttpRequest):
raise NotImplementedError
def mark_as_deleted(self, user, send_mail: bool = True): def mark_as_deleted(self, user, send_mail: bool = True):
""" Mark an entry as deleted """ Mark an entry as deleted
@ -140,6 +130,25 @@ class BaseObject(BaseResource):
self.save() 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): 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 """ 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 from user.models import UserActionLogEntry
if self.recorded: if self.recorded:
return None return None
self.unshare_with_default_users()
action = UserActionLogEntry.get_recorded_action(user) action = UserActionLogEntry.get_recorded_action(user)
self.recorded = action self.recorded = action
self.save() self.save()
@ -335,6 +346,15 @@ class RecordableObjectMixin(models.Model):
""" """
raise NotImplementedError("Implement this in the subclass!") 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): class CheckableObjectMixin(models.Model):
# Checks - Refers to "Genehmigen" but optional # Checks - Refers to "Genehmigen" but optional
@ -397,6 +417,20 @@ class CheckableObjectMixin(models.Model):
self.log.add(action) self.log.add(action)
return 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): class ShareableObjectMixin(models.Model):
# Users having access on this object # Users having access on this object
@ -459,8 +493,8 @@ class ShareableObjectMixin(models.Model):
Returns: Returns:
""" """
directly_shared = self.users.filter(id=user.id).exists() directly_shared = self.shared_users.filter(id=user.id).exists()
team_shared = self.teams.filter( team_shared = self.shared_teams.filter(
users__in=[user] users__in=[user]
).exists() ).exists()
is_shared = directly_shared or team_shared is_shared = directly_shared or team_shared
@ -597,7 +631,9 @@ class ShareableObjectMixin(models.Model):
Returns: Returns:
teams (QuerySet) teams (QuerySet)
""" """
return self.teams.all() return self.teams.filter(
deleted__isnull=True
)
@abstractmethod @abstractmethod
def get_share_url(self): def get_share_url(self):
@ -608,6 +644,26 @@ class ShareableObjectMixin(models.Model):
""" """
raise NotImplementedError("Must be implemented in subclasses!") 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): class GeoReferencedMixin(models.Model):
geometry = models.ForeignKey("konova.Geometry", null=True, blank=True, on_delete=models.SET_NULL) geometry = models.ForeignKey("konova.Geometry", null=True, blank=True, on_delete=models.SET_NULL)
@ -621,10 +677,21 @@ class GeoReferencedMixin(models.Model):
Returns: Returns:
parcels (Iterable): An empty list or a Queryset parcels (Iterable): An empty list or a Queryset
""" """
result = []
if self.geometry is not None: if self.geometry is not None:
return self.geometry.get_underlying_parcels() result = self.geometry.get_underlying_parcels()
else: return result
return []
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): def set_geometry_conflict_message(self, request: HttpRequest):
if self.geometry is None: if self.geometry is None:

View File

@ -10,8 +10,98 @@ from django.db import models
from konova.models import UuidModel 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): 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 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. 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') 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") district = models.ForeignKey("konova.District", on_delete=models.SET_NULL, null=True, blank=True, related_name="parcels")
gmrkng = models.CharField( municipal = models.ForeignKey("konova.Municipal", on_delete=models.SET_NULL, null=True, blank=True, related_name="parcels")
max_length=1000, parcel_group = models.ForeignKey(
"konova.ParcelGroup",
on_delete=models.SET_NULL,
help_text="Gemarkung", help_text="Gemarkung",
null=True, null=True,
blank=True, blank=True,
related_name="parcels"
) )
flrstck_nnr = models.CharField( flr = models.IntegerField(
max_length=1000, help_text="Flur",
null=True,
blank=True,
)
flrstck_nnr = models.IntegerField(
help_text="Flurstücksnenner", help_text="Flurstücksnenner",
null=True, null=True,
blank=True, blank=True,
) )
flrstck_zhlr = models.CharField( flrstck_zhlr = models.IntegerField(
max_length=1000,
help_text="Flurstückszähler", help_text="Flurstückszähler",
null=True, null=True,
blank=True, blank=True,
) )
flr = models.CharField(
max_length=1000,
help_text="Flur",
null=True,
blank=True,
)
updated_on = models.DateTimeField(auto_now_add=True) updated_on = models.DateTimeField(auto_now_add=True)
def __str__(self): class Meta:
return f"{self.gmrkng} | {self.flr} | {self.flrstck_zhlr} | {self.flrstck_nnr}" constraints = [
models.UniqueConstraint(
fields=[
class District(UuidModel): "district",
""" The model District holds more coarse information, such as Kreis, Verbandsgemeinde and Gemeinde. "municipal",
"parcel_group",
There might be the case that a geometry lies on a hundred Parcel entries but only on one District entry. "flr",
Therefore a geometry can have a lot of relations to Parcel entries but only a few or only a single one to one "flrstck_nnr",
District. "flrstck_zhlr",
],
""" name="Unique parcel constraint"
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,
)
def __str__(self): 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): class ParcelIntersection(UuidModel):

View File

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

View File

@ -11,6 +11,7 @@ https://docs.djangoproject.com/en/3.1/ref/settings/
""" """
import os import os
from django.utils.translation import gettext_lazy as _ 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'. # Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = os.path.dirname( BASE_DIR = os.path.dirname(
@ -45,8 +46,8 @@ ALLOWED_HOSTS = [
LOGIN_URL = "/login/" LOGIN_URL = "/login/"
# Session settings # Session settings
#SESSION_COOKIE_AGE = 30 * 60 # 30 minutes SESSION_COOKIE_AGE = 60 * 60 # 60 minutes
#SESSION_SAVE_EVERY_REQUEST = True SESSION_SAVE_EVERY_REQUEST = True
# Application definition # Application definition
@ -162,9 +163,15 @@ LANGUAGES = [
USE_THOUSAND_SEPARATOR = True USE_THOUSAND_SEPARATOR = True
# Regular python relevant date/datetime formatting
DEFAULT_DATE_TIME_FORMAT = '%d.%m.%Y %H:%M:%S' DEFAULT_DATE_TIME_FORMAT = '%d.%m.%Y %H:%M:%S'
DEFAULT_DATE_FORMAT = '%d.%m.%Y' 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' TIME_ZONE = 'Europe/Berlin'
USE_I18N = True USE_I18N = True
@ -184,6 +191,8 @@ STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, "static") STATIC_ROOT = os.path.join(BASE_DIR, "static")
STATICFILES_DIRS = [ STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'konova/static'), 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 # DJANGO DEBUG TOOLBAR

View File

@ -19,6 +19,6 @@ PAGE_SIZE_OPTIONS_TUPLES = [
(50, 50), (50, 50),
(100, 100), (100, 100),
] ]
PAGE_SIZE_DEFAULT = 5 PAGE_SIZE_DEFAULT = 10
PAGE_SIZE_MAX = 100 PAGE_SIZE_MAX = 100
PAGE_DEFAULT = 1 PAGE_DEFAULT = 1

View File

@ -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>

View File

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

View File

@ -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>

View File

@ -8,7 +8,7 @@
</h5> </h5>
</div> </div>
<div class="card-body"> <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"> <div class="row justify-content-center">
<span class="spinner-border rlp-r-inv" role="status"></span> <span class="spinner-border rlp-r-inv" role="status"></span>
</div> </div>

View 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>

View File

@ -2,18 +2,23 @@
{% for code in codes %} {% for code in codes %}
<div class="ml-4 tree-element"> <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%} {% 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 %}/> <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 %} {% else %}
{% fa5_icon 'angle-right' %} <span class="collapse-icn">
{% fa5_icon 'angle-down' %}
</span>
{% endif %}
{% if code.short_name %}
({{code.short_name}})
{% endif %} {% endif %}
{{code.long_name}} {{code.long_name}}
</label> </label>
{% if not code.is_leaf %} {% if not code.is_leaf %}
<div id="children_{{code.pk|unlocalize}}" data-toggle="collapse" class="collapse tree-element-children"> <div id="children_{{code.pk|unlocalize}}" data-toggle="collapse" class="collapse tree-element-children">
{% with code.children as codes %} {% with code.children as codes %}
{% include 'konova/widgets/checkbox-tree-select-content.html' %} {% include 'konova/widgets/tree/checkbox/checkbox-tree-select-content.html' %}
{% endwith %} {% endwith %}
</div> </div>
{% endif %} {% endif %}

View File

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

View File

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

View File

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

View File

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

View File

@ -6,9 +6,11 @@ Created on: 26.10.21
""" """
import datetime import datetime
import json
from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID
from ema.models import Ema from ema.models import Ema
from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP
from user.models import User, Team from user.models import User, Team
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from django.contrib.gis.geos import MultiPolygon, Polygon from django.contrib.gis.geos import MultiPolygon, Polygon
@ -272,7 +274,6 @@ class BaseTestCase(TestCase):
team = Team.objects.get_or_create( team = Team.objects.get_or_create(
name="Testteam", name="Testteam",
description="Testdescription", description="Testdescription",
admin=self.superuser,
)[0] )[0]
team.users.add(self.superuser) 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 = Polygon.from_bbox((7.592449, 50.359385, 7.593382, 50.359874))
polygon.srid = 4326 polygon.srid = 4326
polygon = polygon.transform(3857, clone=True) polygon = polygon.transform(DEFAULT_SRID_RLP, clone=True)
return MultiPolygon(polygon, srid=3857) # 3857 is the default srid used for MultiPolygonField in the form 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: def create_dummy_handler(self) -> Handler:
""" Creates a Handler """ Creates a Handler
@ -410,11 +431,12 @@ class BaseTestCase(TestCase):
return return
if geom1.srid != geom2.srid: 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 # 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 # transformation from one coordinate system into the other, which is valid
geom1_t = geom1.transform(geom2.srid, clone=True) geom1_t = geom1.transform(geom2.srid, clone=True)
geom2_t = geom2.transform(geom1.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: else:
self.assertTrue(geom1.equals(geom2)) self.assertTrue(geom1.equals(geom2))

View File

@ -21,10 +21,10 @@ from konova.autocompletes import EcoAccountAutocomplete, \
InterventionAutocomplete, CompensationActionCodeAutocomplete, BiotopeCodeAutocomplete, LawCodeAutocomplete, \ InterventionAutocomplete, CompensationActionCodeAutocomplete, BiotopeCodeAutocomplete, LawCodeAutocomplete, \
RegistrationOfficeCodeAutocomplete, ConservationOfficeCodeAutocomplete, ProcessTypeCodeAutocomplete, \ RegistrationOfficeCodeAutocomplete, ConservationOfficeCodeAutocomplete, ProcessTypeCodeAutocomplete, \
ShareUserAutocomplete, BiotopeExtraCodeAutocomplete, CompensationActionDetailCodeAutocomplete, \ 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.settings import SSO_SERVER, SSO_PUBLIC_KEY, SSO_PRIVATE_KEY, DEBUG
from konova.sso.sso import KonovaSSOClient 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) sso_client = KonovaSSOClient(SSO_SERVER, SSO_PUBLIC_KEY, SSO_PRIVATE_KEY)
urlpatterns = [ urlpatterns = [
@ -40,7 +40,9 @@ urlpatterns = [
path('cl/', include("codelist.urls")), path('cl/', include("codelist.urls")),
path('analysis/', include("analysis.urls")), path('analysis/', include("analysis.urls")),
path('api/', include("api.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 # Autocomplete paths for all apps
path("atcmplt/eco-accounts", EcoAccountAutocomplete.as_view(), name="accounts-autocomplete"), 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/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/u", ShareUserAutocomplete.as_view(), name="share-user-autocomplete"),
path("atcmplt/share/t", ShareTeamAutocomplete.as_view(), name="share-team-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: if DEBUG:

View File

@ -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.") 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.") MISSING_GROUP_PERMISSION = _("You need to be part of another user group.")
CHECKED_RECORDED_RESET = _("Status of Checked and Recorded reseted") 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 # SHARE
DATA_UNSHARED = _("This data is not shared with you") DATA_UNSHARED = _("This data is not shared with you")
@ -80,3 +81,8 @@ GEOMETRY_CONFLICT_WITH_TEMPLATE = _("Geometry conflict detected with {}")
# INTERVENTION # INTERVENTION
INTERVENTION_HAS_REVOCATIONS_TEMPLATE = _("This intervention has {} revocations") INTERVENTION_HAS_REVOCATIONS_TEMPLATE = _("This intervention has {} revocations")
# CHECKED
DATA_CHECKED_ON_TEMPLATE = _("Checked on {} by {}")
DATA_CHECKED_PREVIOUSLY_TEMPLATE = _("Data has changed since last check on {} by {}")
DATA_IS_UNCHECKED = _("Current data not checked yet")

View File

@ -112,6 +112,17 @@ class BaseTable(tables.tables.Table):
icon 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): def render_bookmark(self, tooltip: str = None, icn_filled: bool = False):
""" """
Returns a bookmark icon Returns a bookmark icon

View File

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

View File

@ -5,9 +5,12 @@ Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 16.11.20 Created on: 16.11.20
""" """
import json
import requests
from django.contrib.auth import logout from django.contrib.auth import logout
from django.contrib.auth.decorators import login_required 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.shortcuts import redirect, render, get_object_or_404
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils import timezone from django.utils import timezone
@ -17,7 +20,7 @@ from compensation.models import Compensation, EcoAccount
from intervention.models import Intervention from intervention.models import Intervention
from konova.contexts import BaseContext from konova.contexts import BaseContext
from konova.decorators import any_group_check 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 konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from news.models import ServerMessage from news.models import ServerMessage
from konova.settings import SSO_SERVER_BASE 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 id (str): The geometry's id
Returns: Returns:
A rendered piece of HTML
""" """
# HTTP code 286 states that the HTMX should stop polling for updates # HTTP code 286 states that the HTMX should stop polling for updates
# https://htmx.org/docs/#polling # https://htmx.org/docs/#polling
status_code = 286 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) geom = get_object_or_404(Geometry, id=id)
parcels = geom.get_underlying_parcels() parcels = geom.get_underlying_parcels()
geos_geom = geom.geom geos_geom = geom.geom
@ -130,8 +133,23 @@ def get_geom_parcels(request: HttpRequest, id: str):
status_code = 200 status_code = 200
if parcels_available or no_geometry_given: 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 = { context = {
"num_parcels": num_all_parcels,
"parcels": parcels, "parcels": parcels,
"municipals": municipals,
"geom_id": str(id),
"next_page": next_page,
} }
html = render_to_string(template, context, request) html = render_to_string(template, context, request)
return HttpResponse(html, status=status_code) return HttpResponse(html, status=status_code)
@ -139,6 +157,49 @@ def get_geom_parcels(request: HttpRequest, id: str):
return HttpResponse(None, status=404) 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): def get_404_view(request: HttpRequest, exception=None):
""" Returns a 404 handling view """ Returns a 404 handling view
@ -164,3 +225,26 @@ def get_500_view(request: HttpRequest):
""" """
context = BaseContext.context context = BaseContext.context
return render(request, "500.html", context, status=500) 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

View File

@ -28,6 +28,7 @@ kombu==5.2.3
openpyxl==3.0.9 openpyxl==3.0.9
OWSLib==0.25.0 OWSLib==0.25.0
packaging==21.3 packaging==21.3
pika==1.2.0
prompt-toolkit==3.0.24 prompt-toolkit==3.0.24
psycopg2-binary==2.9.1 psycopg2-binary==2.9.1
pyparsing==3.0.6 pyparsing==3.0.6

View 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>

View File

@ -11,7 +11,7 @@
</h4> </h4>
{% if form.form_caption is not None %} {% if form.form_caption is not None %}
<small> <small>
{{ form.form_caption }} {{ form.form_caption|linebreaks }}
</small> </small>
{% endif %} {% endif %}
<form method="post" action="{{ form.action_url }}" {% for attr_key, attr_val in form.form_attrs.items %} {{attr_key}}="{{attr_val}}"{% endfor %}> <form method="post" action="{{ form.action_url }}" {% for attr_key, attr_val in form.form_attrs.items %} {{attr_key}}="{{attr_val}}"{% endfor %}>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

View 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
}
}
}

View 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>

View 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.**

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View 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'; }

View 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"}

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