7_New_forms #28

Merged
mpeltriaux merged 22 commits from 7_New_forms into master 3 years ago

@ -13,7 +13,7 @@ from codelist.models import KonovaCode, KonovaCodeList
from codelist.settings import CODELIST_INTERVENTION_HANDLER_ID, CODELIST_CONSERVATION_OFFICE_ID, \ from codelist.settings import CODELIST_INTERVENTION_HANDLER_ID, CODELIST_CONSERVATION_OFFICE_ID, \
CODELIST_REGISTRATION_OFFICE_ID, CODELIST_BIOTOPES_ID, CODELIST_LAW_ID, CODELIST_COMPENSATION_HANDLER_ID, \ CODELIST_REGISTRATION_OFFICE_ID, CODELIST_BIOTOPES_ID, CODELIST_LAW_ID, CODELIST_COMPENSATION_HANDLER_ID, \
CODELIST_COMPENSATION_ACTION_ID, CODELIST_COMPENSATION_ACTION_CLASS_ID, CODELIST_COMPENSATION_ADDITIONAL_TYPE_ID, \ CODELIST_COMPENSATION_ACTION_ID, CODELIST_COMPENSATION_ACTION_CLASS_ID, CODELIST_COMPENSATION_ADDITIONAL_TYPE_ID, \
CODELIST_COMPENSATION_COMBINATION_ID, CODELIST_BASE_URL, CODELIST_PROCESS_TYPE_ID CODELIST_COMPENSATION_FUNDING_ID, CODELIST_BASE_URL, CODELIST_PROCESS_TYPE_ID
bool_map = { bool_map = {
"true": True, "true": True,
@ -35,7 +35,7 @@ class Command(BaseCommand):
CODELIST_COMPENSATION_ACTION_ID, CODELIST_COMPENSATION_ACTION_ID,
CODELIST_COMPENSATION_ACTION_CLASS_ID, CODELIST_COMPENSATION_ACTION_CLASS_ID,
CODELIST_COMPENSATION_ADDITIONAL_TYPE_ID, CODELIST_COMPENSATION_ADDITIONAL_TYPE_ID,
CODELIST_COMPENSATION_COMBINATION_ID, CODELIST_COMPENSATION_FUNDING_ID,
CODELIST_PROCESS_TYPE_ID, CODELIST_PROCESS_TYPE_ID,
] ]
self._write_warning("Fetching codes...") self._write_warning("Fetching codes...")

@ -53,8 +53,9 @@ class KonovaCode(models.Model):
if self.parent: if self.parent:
ret_val += self.parent.long_name + " > " ret_val += self.parent.long_name + " > "
ret_val += self.long_name ret_val += self.long_name
if self.short_name: if self.short_name and self.short_name != self.long_name:
ret_val += " ({})".format(self.short_name) # Only add short name, if we won't have stupid repition like 'thing a (thing a)' due to misused long-short names
ret_val += f" ({self.short_name})"
return ret_val return ret_val
@property @property

@ -21,4 +21,4 @@ CODELIST_COMPENSATION_HANDLER_ID = 1052 # CLEingreifer
CODELIST_COMPENSATION_ACTION_ID = 1026 # CLMassnahmedetail CODELIST_COMPENSATION_ACTION_ID = 1026 # CLMassnahmedetail
CODELIST_COMPENSATION_ACTION_CLASS_ID = 1034 # CLMassnahmeklasse CODELIST_COMPENSATION_ACTION_CLASS_ID = 1034 # CLMassnahmeklasse
CODELIST_COMPENSATION_ADDITIONAL_TYPE_ID = 1028 # CLMassnahmetyp, CEF and stuff CODELIST_COMPENSATION_ADDITIONAL_TYPE_ID = 1028 # CLMassnahmetyp, CEF and stuff
CODELIST_COMPENSATION_COMBINATION_ID = 1049 # CLKombimassnahme CODELIST_COMPENSATION_FUNDING_ID = 1049 # CLKombimassnahme

@ -11,6 +11,7 @@ from compensation.views.eco_account_views import *
urlpatterns = [ urlpatterns = [
path("", index_view, name="acc-index"), path("", index_view, name="acc-index"),
path('new/', new_view, name='acc-new'), path('new/', new_view, name='acc-new'),
path('new/id', new_id_view, name='acc-new-id'),
path('<id>', open_view, name='acc-open'), path('<id>', open_view, name='acc-open'),
path('<id>/log', log_view, name='acc-log'), path('<id>/log', log_view, name='acc-log'),
path('<id>/record', record_view, name='acc-record'), path('<id>/record', record_view, name='acc-record'),

@ -11,6 +11,8 @@ from compensation.views.compensation_views import *
urlpatterns = [ urlpatterns = [
# Main compensation # Main compensation
path("", index_view, name="index"), path("", index_view, name="index"),
path('new/id', new_id_view, name='new-id'),
path('new/<intervention_id>', new_view, name='new'),
path('new', new_view, name='new'), path('new', new_view, name='new'),
path('<id>', open_view, name='open'), path('<id>', open_view, name='open'),
path('<id>/log', log_view, name='log'), path('<id>/log', log_view, name='log'),

@ -0,0 +1,444 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 04.12.20
"""
from dal import autocomplete
from django.contrib.auth.models import User
from django.db import transaction
from django.urls import reverse_lazy, reverse
from django.utils.translation import gettext_lazy as _
from django import forms
from codelist.models import KonovaCode
from codelist.settings import CODELIST_COMPENSATION_FUNDING_ID, CODELIST_CONSERVATION_OFFICE_ID
from compensation.models import Compensation, EcoAccount
from intervention.inputs import GenerateInput
from intervention.models import Intervention, ResponsibilityData
from konova.forms import BaseForm, SimpleGeomForm
from user.models import UserActionLogEntry, UserAction
class AbstractCompensationForm(BaseForm):
""" Abstract form for compensations
Holds all important form fields, which are used in compensation and eco account forms
"""
identifier = forms.CharField(
label=_("Identifier"),
label_suffix="",
max_length=255,
help_text=_("Generated automatically"),
widget=GenerateInput(
attrs={
"class": "form-control",
"url": None, # Needs to be set in inheriting constructors
}
)
)
title = forms.CharField(
label=_("Title"),
label_suffix="",
help_text=_("An explanatory name"),
max_length=255,
widget=forms.TextInput(
attrs={
"placeholder": _("Compensation XY; Location ABC"),
"class": "form-control",
}
)
)
fundings = forms.ModelMultipleChoiceField(
label=_("Fundings"),
label_suffix="",
required=False,
help_text=_("Select fundings for this compensation"),
queryset=KonovaCode.objects.filter(
is_archived=False,
is_leaf=True,
code_lists__in=[CODELIST_COMPENSATION_FUNDING_ID],
),
widget=autocomplete.ModelSelect2Multiple(
url="codes-compensation-funding-autocomplete",
attrs={
"data-placeholder": _("Click for selection"),
}
),
)
comment = forms.CharField(
label_suffix="",
label=_("Comment"),
required=False,
help_text=_("Additional comment"),
widget=forms.Textarea(
attrs={
"rows": 5,
"class": "form-control"
}
)
)
class Meta:
abstract = True
class CompensationResponsibleFormMixin(forms.Form):
""" Encapsulates form fields used in different compensation related models like EcoAccount or EMA
"""
conservation_office = forms.ModelChoiceField(
label=_("Conservation office"),
label_suffix="",
help_text=_("Select the responsible office"),
queryset=KonovaCode.objects.filter(
is_archived=False,
is_leaf=True,
code_lists__in=[CODELIST_CONSERVATION_OFFICE_ID],
),
widget=autocomplete.ModelSelect2(
url="codes-conservation-office-autocomplete",
attrs={
"data-placeholder": _("Click for selection")
}
),
)
conservation_file_number = forms.CharField(
label=_("Conservation office file number"),
label_suffix="",
max_length=255,
required=False,
widget=forms.TextInput(
attrs={
"placeholder": _("ETS-123/ABC.456"),
"class": "form-control",
}
)
)
handler = forms.CharField(
label=_("Eco-account handler"),
label_suffix="",
max_length=255,
required=False,
help_text=_("Who handles the eco-account"),
widget=forms.TextInput(
attrs={
"placeholder": _("Company Mustermann"),
"class": "form-control",
}
)
)
class NewCompensationForm(AbstractCompensationForm):
""" Form for creating new compensations.
Can be initialized with an intervention id for preselecting the related intervention.
form = NewCompensationForm(request.POST or None, intervention_id=intervention_id)
...
The intervention id will not be resolved into the intervention ORM object but instead will be used to initialize
the related form field.
"""
intervention = forms.ModelChoiceField(
label=_("compensates intervention"),
label_suffix="",
help_text=_("Select the intervention for which this compensation compensates"),
queryset=Intervention.objects.filter(
deleted=None,
),
widget=autocomplete.ModelSelect2(
url="interventions-autocomplete",
attrs={
"data-placeholder": _("Click for selection"),
"data-minimum-input-length": 3,
}
),
)
# Define a field order for a nicer layout instead of running with the inheritance result
field_order = [
"identifier",
"title",
"intervention",
"fundings",
"comment",
]
def __init__(self, *args, **kwargs):
intervention_id = kwargs.pop("intervention_id", None)
super().__init__(*args, **kwargs)
self.form_title = _("New compensation")
# If the compensation shall directly be initialized from an intervention, we need to fill in the intervention id
# and disable the form field.
# Furthermore the action_url needs to be set accordingly.
if intervention_id is not None:
self.initialize_form_field("intervention", intervention_id)
self.disable_form_field("intervention")
self.action_url = reverse("compensation:new", args=(intervention_id,))
self.cancel_redirect = reverse("intervention:open", args=(intervention_id,))
else:
self.action_url = reverse("compensation:new")
self.cancel_redirect = reverse("compensation:index")
tmp = Compensation()
identifier = tmp.generate_new_identifier()
self.initialize_form_field("identifier", identifier)
self.fields["identifier"].widget.attrs["url"] = reverse_lazy("compensation:new-id")
def save(self, user: User, geom_form: SimpleGeomForm):
with transaction.atomic():
# Fetch data from cleaned POST values
identifier = self.cleaned_data.get("identifier", None)
title = self.cleaned_data.get("title", None)
fundings = self.cleaned_data.get("fundings", None)
intervention = self.cleaned_data.get("intervention", None)
comment = self.cleaned_data.get("comment", None)
# Create log entry
action = UserActionLogEntry.objects.create(
user=user,
action=UserAction.CREATED,
)
# Process the geometry form
geometry = geom_form.save(action)
# Finally create main object
comp = Compensation.objects.create(
identifier=identifier,
title=title,
intervention=intervention,
created=action,
geometry=geometry,
comment=comment,
)
comp.fundings.set(fundings)
# Add the log entry to the main objects log list
comp.log.add(action)
return comp
class EditCompensationForm(NewCompensationForm):
""" Form for editing compensations
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_title = _("Edit compensation")
self.action_url = reverse("compensation:edit", args=(self.instance.id,))
self.cancel_redirect = reverse("compensation:open", args=(self.instance.id,))
# Initialize form data
form_data = {
"identifier": self.instance.identifier,
"title": self.instance.title,
"intervention": self.instance.intervention,
"fundings": self.instance.fundings.all(),
"comment": self.instance.comment,
}
disabled_fields = []
self.load_initial_data(
form_data,
disabled_fields
)
def save(self, user: User, geom_form: SimpleGeomForm):
with transaction.atomic():
# Fetch data from cleaned POST values
identifier = self.cleaned_data.get("identifier", None)
title = self.cleaned_data.get("title", None)
fundings = self.cleaned_data.get("fundings", None)
intervention = self.cleaned_data.get("intervention", None)
comment = self.cleaned_data.get("comment", None)
# Create log entry
action = UserActionLogEntry.objects.create(
user=user,
action=UserAction.EDITED,
)
# Process the geometry form
geometry = geom_form.save(action)
# Finally create main object
self.instance.identifier = identifier
self.instance.title = title
self.instance.intervention = intervention
self.instance.geometry = geometry
self.instance.comment = comment
self.instance.modified = action
self.instance.fundings.set(fundings)
self.instance.save()
self.instance.log.add(action)
return self.instance
class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMixin):
""" Form for creating eco accounts
Inherits from basic AbstractCompensationForm and further form fields from CompensationResponsibleFormMixin
"""
field_order = [
"identifier",
"title",
"conservation_office",
"conservation_file_number",
"handler",
"fundings",
"comment",
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_title = _("New Eco-Account")
self.action_url = reverse("compensation:acc-new")
self.cancel_redirect = reverse("compensation:acc-index")
tmp = EcoAccount()
identifier = tmp.generate_new_identifier()
self.initialize_form_field("identifier", identifier)
self.fields["identifier"].widget.attrs["url"] = reverse_lazy("compensation:acc-new-id")
self.fields["title"].widget.attrs["placeholder"] = _("Eco-Account XY; Location ABC")
def save(self, user: User, geom_form: SimpleGeomForm):
with transaction.atomic():
# Fetch data from cleaned POST values
identifier = self.cleaned_data.get("identifier", None)
title = self.cleaned_data.get("title", None)
fundings = self.cleaned_data.get("fundings", None)
handler = self.cleaned_data.get("handler", None)
conservation_office = self.cleaned_data.get("conservation_office", None)
conservation_file_number = self.cleaned_data.get("conservation_file_number", None)
comment = self.cleaned_data.get("comment", None)
# Create log entry
action = UserActionLogEntry.objects.create(
user=user,
action=UserAction.CREATED,
)
# Process the geometry form
geometry = geom_form.save(action)
responsible = ResponsibilityData.objects.create(
handler=handler,
conservation_file_number=conservation_file_number,
conservation_office=conservation_office,
)
# Finally create main object
acc = EcoAccount.objects.create(
identifier=identifier,
title=title,
responsible=responsible,
deductable_surface=0.00,
created=action,
geometry=geometry,
comment=comment,
)
acc.fundings.set(fundings)
acc.users.add(user)
# Add the log entry to the main objects log list
acc.log.add(action)
return acc
class EditEcoAccountForm(NewEcoAccountForm):
""" Form for editing eco accounts
"""
surface = forms.DecimalField(
min_value=0.00,
decimal_places=2,
label=_("Available Surface"),
label_suffix="",
required=False,
help_text=_("The amount that can be used for deductions"),
widget=forms.NumberInput(
attrs={
"class": "form-control",
"placeholder": "0,00"
}
)
)
field_order = [
"identifier",
"title",
"conservation_office",
"surface",
"conservation_file_number",
"handler",
"fundings",
"comment",
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_title = _("Edit Eco-Account")
self.action_url = reverse("compensation:acc-edit", args=(self.instance.id,))
self.cancel_redirect = reverse("compensation:acc-open", args=(self.instance.id,))
# Initialize form data
form_data = {
"identifier": self.instance.identifier,
"title": self.instance.title,
"surface": self.instance.deductable_surface,
"handler": self.instance.responsible.handler,
"conservation_office": self.instance.responsible.conservation_office,
"conservation_file_number": self.instance.responsible.conservation_file_number,
"fundings": self.instance.fundings.all(),
"comment": self.instance.comment,
}
disabled_fields = []
self.load_initial_data(
form_data,
disabled_fields
)
def save(self, user: User, geom_form: SimpleGeomForm):
with transaction.atomic():
# Fetch data from cleaned POST values
identifier = self.cleaned_data.get("identifier", None)
title = self.cleaned_data.get("title", None)
fundings = self.cleaned_data.get("fundings", None)
handler = self.cleaned_data.get("handler", None)
surface = self.cleaned_data.get("surface", None)
conservation_office = self.cleaned_data.get("conservation_office", None)
conservation_file_number = self.cleaned_data.get("conservation_file_number", None)
comment = self.cleaned_data.get("comment", None)
# Create log entry
action = UserActionLogEntry.objects.create(
user=user,
action=UserAction.EDITED,
)
# Process the geometry form
geometry = geom_form.save(action)
# Update responsible data
self.instance.responsible.handler = handler
self.instance.responsible.conservation_office = conservation_office
self.instance.responsible.conservation_file_number = conservation_file_number
self.instance.responsible.save()
# Update main oject data
self.instance.identifier = identifier
self.instance.title = title
self.instance.deductable_surface = surface
self.instance.geometry = geometry
self.instance.comment = comment
self.instance.modified = action
self.instance.save()
self.instance.fundings.set(fundings)
# Add the log entry to the main objects log list
self.instance.log.add(action)
return self.instance

@ -2,7 +2,7 @@
Author: Michel Peltriaux Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 04.12.20 Created on: 04.10.21
""" """
from bootstrap_modal_forms.utils import is_ajax from bootstrap_modal_forms.utils import is_ajax
@ -12,40 +12,34 @@ from django.contrib import messages
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
from django.utils.translation import gettext_lazy as _ from django.utils.translation import pgettext_lazy as _con, gettext_lazy as _
from django.utils.translation import pgettext_lazy as _con
from codelist.models import KonovaCode from codelist.models import KonovaCode
from codelist.settings import CODELIST_BIOTOPES_ID, CODELIST_COMPENSATION_ACTION_ID from codelist.settings import CODELIST_BIOTOPES_ID, CODELIST_COMPENSATION_ACTION_ID
from compensation.models import Payment, CompensationState, CompensationAction, UnitChoices from compensation.models import Payment, CompensationState, UnitChoices, CompensationAction
from konova.contexts import BaseContext from konova.contexts import BaseContext
from konova.forms import BaseForm, BaseModalForm from konova.forms import BaseModalForm
from konova.models import Deadline, DeadlineType from konova.models import DeadlineType, Deadline
from konova.utils.message_templates import FORM_INVALID from konova.utils.message_templates import FORM_INVALID
from user.models import UserActionLogEntry, UserAction from user.models import UserActionLogEntry, UserAction
class NewCompensationForm(BaseForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def save(self):
with transaction.atomic():
user_action = UserActionLogEntry.objects.create(
user=self.user,
action=UserAction.CREATED
)
# Save action to log
class NewPaymentForm(BaseModalForm): class NewPaymentForm(BaseModalForm):
""" Form handling payment related input
"""
amount = forms.DecimalField( amount = forms.DecimalField(
min_value=0.00, min_value=0.00,
decimal_places=2, decimal_places=2,
label=_con("money", "Amount"), # contextual translation label=_con("money", "Amount"), # contextual translation
label_suffix=_(""), label_suffix=_(""),
help_text=_("in Euro"), help_text=_("in Euro"),
widget=forms.NumberInput(
attrs={
"class": "form-control",
"placeholder": "0,00",
}
)
) )
due = forms.DateField( due = forms.DateField(
label=_("Due on"), label=_("Due on"),
@ -56,6 +50,7 @@ class NewPaymentForm(BaseModalForm):
attrs={ attrs={
"type": "date", "type": "date",
"data-provide": "datepicker", "data-provide": "datepicker",
"class": "form-control",
}, },
format="%d.%m.%Y" format="%d.%m.%Y"
) )
@ -69,7 +64,7 @@ class NewPaymentForm(BaseModalForm):
widget=forms.Textarea( widget=forms.Textarea(
attrs={ attrs={
"rows": 5, "rows": 5,
"class": "w-100" "class": "form-control"
} }
) )
) )
@ -79,7 +74,6 @@ class NewPaymentForm(BaseModalForm):
self.intervention = self.instance self.intervention = self.instance
self.form_title = _("Payment") self.form_title = _("Payment")
self.form_caption = _("Add a payment for intervention '{}'").format(self.intervention.title) self.form_caption = _("Add a payment for intervention '{}'").format(self.intervention.title)
self.add_placeholder_for_field("amount", "0,00")
def is_valid(self): def is_valid(self):
""" """
@ -129,6 +123,12 @@ class NewPaymentForm(BaseModalForm):
class NewStateModalForm(BaseModalForm): class NewStateModalForm(BaseModalForm):
""" Form handling state related input
Compensation states refer to 'before' and 'after' states of a compensated surface. Basically it means:
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.ModelChoiceField(
label=_("Biotope Type"), label=_("Biotope Type"),
label_suffix="", label_suffix="",
@ -152,14 +152,19 @@ class NewStateModalForm(BaseModalForm):
label=_("Surface"), label=_("Surface"),
label_suffix="", label_suffix="",
required=True, required=True,
help_text=_("in m²") help_text=_("in m²"),
widget=forms.NumberInput(
attrs={
"class": "form-control",
"placeholder": "0,00"
}
)
) )
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
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")
self.add_placeholder_for_field("surface", "0,00")
def save(self, is_before_state: bool = False): def save(self, is_before_state: bool = False):
with transaction.atomic(): with transaction.atomic():
@ -232,6 +237,9 @@ class NewStateModalForm(BaseModalForm):
class NewDeadlineModalForm(BaseModalForm): class NewDeadlineModalForm(BaseModalForm):
""" Form handling deadline related input
"""
type = forms.ChoiceField( type = forms.ChoiceField(
label=_("Deadline Type"), label=_("Deadline Type"),
label_suffix="", label_suffix="",
@ -240,7 +248,7 @@ class NewDeadlineModalForm(BaseModalForm):
choices=DeadlineType.choices, choices=DeadlineType.choices,
widget=forms.Select( widget=forms.Select(
attrs={ attrs={
"class": "custom-select" "class": "form-control"
} }
) )
) )
@ -253,6 +261,7 @@ class NewDeadlineModalForm(BaseModalForm):
attrs={ attrs={
"type": "date", "type": "date",
"data-provide": "datepicker", "data-provide": "datepicker",
"class": "form-control",
}, },
format="%d.%m.%Y" format="%d.%m.%Y"
) )
@ -267,6 +276,7 @@ class NewDeadlineModalForm(BaseModalForm):
attrs={ attrs={
"cols": 30, "cols": 30,
"rows": 5, "rows": 5,
"class": "form-control",
} }
) )
) )
@ -301,6 +311,13 @@ class NewDeadlineModalForm(BaseModalForm):
class NewActionModalForm(BaseModalForm): class NewActionModalForm(BaseModalForm):
""" Form handling action related input
Compensation actions are the actions performed on the area, which shall be compensated. Actions will change the
surface of the area, the biotopes, and have an environmental impact. With actions the before-after states can change
(not in the process logic in Konova, but in the real world).
"""
action_type = forms.ModelChoiceField( action_type = forms.ModelChoiceField(
label=_("Action Type"), label=_("Action Type"),
label_suffix="", label_suffix="",
@ -326,7 +343,7 @@ class NewActionModalForm(BaseModalForm):
choices=UnitChoices.choices, choices=UnitChoices.choices,
widget=forms.Select( widget=forms.Select(
attrs={ attrs={
"class": "custom-select" "class": "form-control"
} }
) )
) )
@ -337,6 +354,12 @@ class NewActionModalForm(BaseModalForm):
help_text=_("Insert the amount"), help_text=_("Insert the amount"),
decimal_places=2, decimal_places=2,
min_value=0.00, min_value=0.00,
widget=forms.NumberInput(
attrs={
"class": "form-control",
"placeholder": "0,00",
}
)
) )
comment = forms.CharField( comment = forms.CharField(
required=False, required=False,
@ -347,7 +370,7 @@ class NewActionModalForm(BaseModalForm):
widget=forms.Textarea( widget=forms.Textarea(
attrs={ attrs={
"rows": 5, "rows": 5,
"class": "w-100" "class": "form-control",
} }
) )
) )
@ -356,7 +379,6 @@ class NewActionModalForm(BaseModalForm):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.form_title = _("New action") self.form_title = _("New action")
self.form_caption = _("Insert data for the new action") self.form_caption = _("Insert data for the new action")
self.add_placeholder_for_field("amount", "0,00")
def save(self): def save(self):
with transaction.atomic(): with transaction.atomic():
@ -381,4 +403,3 @@ class NewActionModalForm(BaseModalForm):
self.instance.log.add(edited_action) self.instance.log.add(edited_action)
self.instance.actions.add(comp_action) self.instance.actions.add(comp_action)
return comp_action return comp_action

@ -16,7 +16,7 @@ from django.utils.translation import gettext_lazy as _
from codelist.models import KonovaCode from codelist.models import KonovaCode
from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_BIOTOPES_ID, \ from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_BIOTOPES_ID, \
CODELIST_COMPENSATION_COMBINATION_ID CODELIST_COMPENSATION_FUNDING_ID
from intervention.models import Intervention, ResponsibilityData from intervention.models import Intervention, ResponsibilityData
from konova.models import BaseObject, BaseResource, Geometry, UuidModel, AbstractDocument, \ from konova.models import BaseObject, BaseResource, Geometry, UuidModel, AbstractDocument, \
generate_document_file_upload_path generate_document_file_upload_path
@ -144,7 +144,7 @@ class AbstractCompensation(BaseObject):
null=True, null=True,
blank=True, blank=True,
limit_choices_to={ limit_choices_to={
"code_lists__in": [CODELIST_COMPENSATION_COMBINATION_ID], "code_lists__in": [CODELIST_COMPENSATION_FUNDING_ID],
"is_selectable": True, "is_selectable": True,
"is_archived": False, "is_archived": False,
}, },
@ -185,9 +185,9 @@ class Compensation(AbstractCompensation):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if self.identifier is None or len(self.identifier) == 0: if self.identifier is None or len(self.identifier) == 0:
# Create new identifier # Create new identifier
new_id = self._generate_new_identifier() new_id = self.generate_new_identifier()
while Compensation.objects.filter(identifier=new_id).exists(): while Compensation.objects.filter(identifier=new_id).exists():
new_id = self._generate_new_identifier() new_id = self.generate_new_identifier()
self.identifier = new_id self.identifier = new_id
super().save(*args, **kwargs) super().save(*args, **kwargs)
@ -322,9 +322,9 @@ class EcoAccount(AbstractCompensation):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if self.identifier is None or len(self.identifier) == 0: if self.identifier is None or len(self.identifier) == 0:
# Create new identifier # Create new identifier
new_id = self._generate_new_identifier() new_id = self.generate_new_identifier()
while EcoAccount.objects.filter(identifier=new_id).exists(): while EcoAccount.objects.filter(identifier=new_id).exists():
new_id = self._generate_new_identifier() new_id = self.generate_new_identifier()
self.identifier = new_id self.identifier = new_id
super().save(*args, **kwargs) super().save(*args, **kwargs)
@ -355,28 +355,28 @@ class EcoAccount(AbstractCompensation):
""" """
return self.after_states.all().aggregate(Sum("surface"))["surface__sum"] or 0 return self.after_states.all().aggregate(Sum("surface"))["surface__sum"] or 0
def get_available_rest(self, as_percentage: bool = False) -> float: def get_available_rest(self) -> (float, float):
""" Calculates available rest surface of the eco account """ Calculates available rest surface of the eco account
Args: Args:
as_percentage (bool): Whether to return the result as or %
Returns: Returns:
ret_val_total (float): Total amount
ret_val_relative (float): Amount as percentage (0-100)
""" """
deductions = self.deductions.filter( deductions = self.deductions.filter(
intervention__deleted=None, intervention__deleted=None,
) )
deductions_surfaces = deductions.aggregate(Sum("surface"))["surface__sum"] or 0 deductions_surfaces = deductions.aggregate(Sum("surface"))["surface__sum"] or 0
available_surfaces = self.deductable_surface or deductions_surfaces ## no division by zero available_surfaces = self.deductable_surface or deductions_surfaces ## no division by zero
ret_val = available_surfaces - deductions_surfaces ret_val_total = available_surfaces - deductions_surfaces
if as_percentage:
if available_surfaces > 0: if available_surfaces > 0:
ret_val = int((ret_val / available_surfaces) * 100) ret_val_relative = int((ret_val_total / available_surfaces) * 100)
else: else:
ret_val = 0 ret_val_relative = 0
return ret_val
return ret_val_total, ret_val_relative
def get_LANIS_link(self) -> str: def get_LANIS_link(self) -> str:
""" Generates a link for LANIS depending on the geometry """ Generates a link for LANIS depending on the geometry

@ -5,8 +5,8 @@ Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 18.12.20 Created on: 18.12.20
""" """
COMPENSATION_IDENTIFIER_LENGTH = 10 COMPENSATION_IDENTIFIER_LENGTH = 6
COMPENSATION_IDENTIFIER_TEMPLATE = "KOM-{}" COMPENSATION_IDENTIFIER_TEMPLATE = "KOM-{}"
ECO_ACCOUNT_IDENTIFIER_LENGTH = 10 ECO_ACCOUNT_IDENTIFIER_LENGTH = 6
ECO_ACCOUNT_IDENTIFIER_TEMPLATE = "OEK-{}" ECO_ACCOUNT_IDENTIFIER_TEMPLATE = "OEK-{}"

@ -5,6 +5,7 @@ Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 01.12.20 Created on: 01.12.20
""" """
from django.contrib.auth.models import User
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
@ -148,6 +149,8 @@ class CompensationTable(BaseTable):
""" """
html = "" html = ""
if value is None:
value = User.objects.none()
has_access = value.filter( has_access = value.filter(
username=self.user.username username=self.user.username
).exists() ).exists()
@ -236,8 +239,8 @@ class EcoAccountTable(BaseTable):
Returns: Returns:
""" """
value = record.get_available_rest(as_percentage=True) value_total, value_relative = record.get_available_rest()
html = render_to_string("konova/custom_widgets/progressbar.html", {"value": value}) html = render_to_string("konova/widgets/progressbar.html", {"value": value_relative})
return format_html(html) return format_html(html)
def render_r(self, value, record: EcoAccount): def render_r(self, value, record: EcoAccount):

@ -0,0 +1,23 @@
{% load i18n fontawesome_5 %}
{% if obj.comment %}
<div class="w-100">
<div class="card mt-3">
<div class="card-header rlp-gd">
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-12">
<h5 class="card-title">
{% fa5_icon 'info-circle' %}
{% trans 'Comment' %}
</h5>
</div>
</div>
</div>
<div class="card-body">
<div class="card-text font-italic">
{{obj.comment}}
</div>
</div>
</div>
</div>
{% endif %}

@ -13,7 +13,7 @@
</a> </a>
{% if has_access %} {% if has_access %}
{% if is_default_member %} {% if is_default_member %}
<a href="{% url 'home' %}" class="mr-2"> <a href="{% url 'compensation:edit' obj.id %}" class="mr-2">
<button class="btn btn-default" title="{% trans 'Edit' %}"> <button class="btn btn-default" title="{% trans 'Edit' %}">
{% fa5_icon 'edit' %} {% fa5_icon 'edit' %}
</button> </button>

@ -75,7 +75,7 @@
</div> </div>
<br> <br>
{% empty %} {% empty %}
None {% trans 'None' %}
{% endfor %} {% endfor %}
</td> </td>
</tr> </tr>
@ -99,8 +99,13 @@
</div> </div>
</div> </div>
<div class="col-sm-12 col-md-12 col-lg-6"> <div class="col-sm-12 col-md-12 col-lg-6">
<div class="row">
{% include 'map/geom_form.html' %} {% include 'map/geom_form.html' %}
</div> </div>
<div class="row">
{% include 'compensation/detail/compensation/includes/comment.html' %}
</div>
</div>
</div> </div>
<hr> <hr>

@ -0,0 +1,23 @@
{% load i18n fontawesome_5 %}
{% if obj.comment %}
<div class="w-100">
<div class="card mt-3">
<div class="card-header rlp-gd">
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-12">
<h5 class="card-title">
{% fa5_icon 'info-circle' %}
{% trans 'Comment' %}
</h5>
</div>
</div>
</div>
<div class="card-body">
<div class="card-text font-italic">
{{obj.comment}}
</div>
</div>
</div>
</div>
{% endif %}

@ -24,7 +24,7 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
{% if is_default_member %} {% if is_default_member %}
<a href="{% url 'home' %}" class="mr-2"> <a href="{% url 'compensation:acc-edit' obj.id %}" class="mr-2">
<button class="btn btn-default" title="{% trans 'Edit' %}"> <button class="btn btn-default" title="{% trans 'Edit' %}">
{% fa5_icon 'edit' %} {% fa5_icon 'edit' %}
</button> </button>

@ -30,12 +30,12 @@
<th class="w-25" scope="row">{% trans 'Title' %}</th> <th class="w-25" scope="row">{% trans 'Title' %}</th>
<td class="align-middle">{{obj.title}}</td> <td class="align-middle">{{obj.title}}</td>
</tr> </tr>
<tr> <tr {% if not obj.deductable_surface %}class="alert alert-danger" title="{% trans 'No surface deductable' %}" {% endif %}>
<th scope="row">{% trans 'Available' %}</th> <th scope="row">{% trans 'Available' %}</th>
<td class="align-middle"> <td class="align-middle">
{{obj.deductions_surface_sum|floatformat:2}} / {{obj.deductable_surface|floatformat:2}} m² {{available_total|floatformat:2}} / {{obj.deductable_surface|default_if_none:0.00|floatformat:2}} m²
{% with available as value %} {% with available as value %}
{% include 'konova/custom_widgets/progressbar.html' %} {% include 'konova/widgets/progressbar.html' %}
{% endwith %} {% endwith %}
</td> </td>
</tr> </tr>
@ -58,7 +58,7 @@
<td class="align-middle">{{obj.responsible.conservation_office.str_as_office|default_if_none:""}}</td> <td class="align-middle">{{obj.responsible.conservation_office.str_as_office|default_if_none:""}}</td>
</tr> </tr>
<tr {% if not obj.responsible.conservation_file_number %}class="alert alert-danger" title="{% trans 'Missing' %}" {% endif %}> <tr {% if not obj.responsible.conservation_file_number %}class="alert alert-danger" title="{% trans 'Missing' %}" {% endif %}>
<th scope="row">{% trans 'Conversation 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 {% if not obj.responsible.handler %}class="alert alert-danger" title="{% trans 'Missing' %}" {% endif %}> <tr {% if not obj.responsible.handler %}class="alert alert-danger" title="{% trans 'Missing' %}" {% endif %}>
@ -98,8 +98,13 @@
</div> </div>
</div> </div>
<div class="col-sm-12 col-md-12 col-lg-6"> <div class="col-sm-12 col-md-12 col-lg-6">
<div class="row">
{% include 'map/geom_form.html' %} {% include 'map/geom_form.html' %}
</div> </div>
<div class="row">
{% include 'compensation/detail/compensation/includes/comment.html' %}
</div>
</div>
</div> </div>
<hr> <hr>

@ -0,0 +1,6 @@
{% extends 'base.html' %}
{% load i18n l10n %}
{% block body %}
{% include 'form/collapsable/form.html' %}
{% endblock %}

@ -1,16 +1,19 @@
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.db.models import Sum from django.db.models import Sum
from django.http import HttpRequest from django.http import HttpRequest, JsonResponse
from django.shortcuts import render, get_object_or_404 from django.shortcuts import render
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from compensation.forms import NewStateModalForm, NewDeadlineModalForm, NewActionModalForm from compensation.forms.forms import NewCompensationForm, EditCompensationForm
from compensation.forms.modalForms import NewStateModalForm, NewDeadlineModalForm, NewActionModalForm
from compensation.models import Compensation, CompensationState, CompensationAction, CompensationDocument from compensation.models import Compensation, CompensationState, CompensationAction, CompensationDocument
from compensation.tables import CompensationTable from compensation.tables import CompensationTable
from intervention.models import Intervention
from konova.contexts import BaseContext from konova.contexts import BaseContext
from konova.decorators import * from konova.decorators import *
from konova.forms import RemoveModalForm, SimpleGeomForm, NewDocumentForm from konova.forms import RemoveModalForm, SimpleGeomForm, NewDocumentForm
from konova.utils.documents import get_document, remove_document from konova.utils.documents import get_document, remove_document
from konova.utils.message_templates import FORM_INVALID, IDENTIFIER_REPLACED
from konova.utils.user_checks import in_group from konova.utils.user_checks import in_group
@ -44,16 +47,100 @@ def index_view(request: HttpRequest):
@login_required @login_required
@default_group_required @default_group_required
def new_view(request: HttpRequest): @shared_access_required(Intervention, "intervention_id")
# ToDo def new_view(request: HttpRequest, intervention_id: str = None):
"""
Renders a view for a new compensation creation
Args:
request (HttpRequest): The incoming request
Returns:
"""
template = "compensation/form/view.html"
data_form = NewCompensationForm(request.POST or None, intervention_id=intervention_id)
geom_form = SimpleGeomForm(request.POST or None, read_only=False)
if request.method == "POST":
if data_form.is_valid() and geom_form.is_valid():
generated_identifier = data_form.cleaned_data.get("identifier", None)
comp = data_form.save(request.user, geom_form)
if generated_identifier != comp.identifier:
messages.info(
request,
IDENTIFIER_REPLACED.format(
generated_identifier,
comp.identifier
)
)
messages.success(request, _("Compensation {} added").format(comp.identifier))
return redirect("compensation:open", id=comp.id)
else:
messages.error(request, FORM_INVALID)
else:
# For clarification: nothing in this case
pass pass
context = {
"form": data_form,
"geom_form": geom_form,
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required
def new_id_view(request: HttpRequest):
""" JSON endpoint
Provides fetching of free identifiers for e.g. AJAX calls
"""
tmp = Compensation()
identifier = tmp.generate_new_identifier()
while Compensation.objects.filter(identifier=identifier).exists():
identifier = tmp.generate_new_identifier()
return JsonResponse(
data={
"identifier": identifier
}
)
@login_required @login_required
@default_group_required @default_group_required
def edit_view(request: HttpRequest, id: str): def edit_view(request: HttpRequest, id: str):
# ToDo """
Renders a view for editing compensations
Args:
request (HttpRequest): The incoming request
Returns:
"""
template = "compensation/form/view.html"
# Get object from db
comp = get_object_or_404(Compensation, id=id)
# Create forms, initialize with values from db/from POST request
data_form = EditCompensationForm(request.POST or None, instance=comp)
geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=comp)
if request.method == "POST":
if data_form.is_valid() and geom_form.is_valid():
# The data form takes the geom form for processing, as well as the performing user
comp = data_form.save(request.user, geom_form)
messages.success(request, _("Compensation {} edited").format(comp.identifier))
return redirect("compensation:open", id=comp.id)
else:
messages.error(request, FORM_INVALID)
else:
# For clarification: nothing in this case
pass pass
context = {
"form": data_form,
"geom_form": geom_form,
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required @login_required

@ -5,23 +5,26 @@ Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 09.08.21 Created on: 09.08.21
""" """
from django.contrib import messages
from django.db.models import Sum from django.db.models import Sum
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.http import HttpRequest, Http404 from django.http import HttpRequest, Http404, JsonResponse
from django.shortcuts import render, get_object_or_404 from django.shortcuts import render, get_object_or_404, redirect
from compensation.forms import NewStateModalForm, NewActionModalForm, NewDeadlineModalForm from compensation.forms.forms import NewEcoAccountForm, EditEcoAccountForm
from compensation.forms.modalForms import NewStateModalForm, NewActionModalForm, NewDeadlineModalForm
from compensation.models import EcoAccount, EcoAccountDocument from compensation.models import EcoAccount, EcoAccountDocument
from compensation.tables import EcoAccountTable from compensation.tables import EcoAccountTable
from intervention.forms import NewDeductionForm from intervention.forms.modalForms import NewDeductionModalForm
from konova.contexts import BaseContext from konova.contexts import BaseContext
from konova.decorators import any_group_check, default_group_required, conservation_office_group_required from konova.decorators import any_group_check, default_group_required, conservation_office_group_required
from konova.forms import RemoveModalForm, SimpleGeomForm, NewDocumentForm, RecordModalForm from konova.forms import RemoveModalForm, SimpleGeomForm, NewDocumentForm, RecordModalForm
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
from konova.utils.documents import get_document, remove_document from konova.utils.documents import get_document, remove_document
from konova.utils.message_templates import IDENTIFIER_REPLACED, FORM_INVALID
from konova.utils.user_checks import in_group from konova.utils.user_checks import in_group
@ -56,15 +59,98 @@ def index_view(request: HttpRequest):
@login_required @login_required
@default_group_required @default_group_required
def new_view(request: HttpRequest): def new_view(request: HttpRequest):
# ToDo """
Renders a view for a new eco account creation
Args:
request (HttpRequest): The incoming request
Returns:
"""
template = "compensation/form/view.html"
data_form = NewEcoAccountForm(request.POST or None)
geom_form = SimpleGeomForm(request.POST or None, read_only=False)
if request.method == "POST":
if data_form.is_valid() and geom_form.is_valid():
generated_identifier = data_form.cleaned_data.get("identifier", None)
acc = data_form.save(request.user, geom_form)
if generated_identifier != acc.identifier:
messages.info(
request,
IDENTIFIER_REPLACED.format(
generated_identifier,
acc.identifier
)
)
messages.success(request, _("Eco-Account {} added").format(acc.identifier))
return redirect("compensation:acc-open", id=acc.id)
else:
messages.error(request, FORM_INVALID)
else:
# For clarification: nothing in this case
pass pass
context = {
"form": data_form,
"geom_form": geom_form,
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required
def new_id_view(request: HttpRequest):
""" JSON endpoint
Provides fetching of free identifiers for e.g. AJAX calls
"""
tmp = EcoAccount()
identifier = tmp.generate_new_identifier()
while EcoAccount.objects.filter(identifier=identifier).exists():
identifier = tmp.generate_new_identifier()
return JsonResponse(
data={
"identifier": identifier
}
)
@login_required @login_required
@default_group_required @default_group_required
def edit_view(request: HttpRequest, id: str): def edit_view(request: HttpRequest, id: str):
# ToDo """
Renders a view for editing compensations
Args:
request (HttpRequest): The incoming request
Returns:
"""
template = "compensation/form/view.html"
# Get object from db
acc = get_object_or_404(EcoAccount, id=id)
# Create forms, initialize with values from db/from POST request
data_form = EditEcoAccountForm(request.POST or None, instance=acc)
geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=acc)
if request.method == "POST":
if data_form.is_valid() and geom_form.is_valid():
# The data form takes the geom form for processing, as well as the performing user
acc = data_form.save(request.user, geom_form)
messages.success(request, _("Eco-Account {} edited").format(acc.identifier))
return redirect("compensation:acc-open", id=acc.id)
else:
messages.error(request, FORM_INVALID)
else:
# For clarification: nothing in this case
pass pass
context = {
"form": data_form,
"geom_form": geom_form,
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required @login_required
@ -96,7 +182,7 @@ def open_view(request: HttpRequest, id: str):
diff_states = abs(sum_before_states - sum_after_states) diff_states = abs(sum_before_states - sum_after_states)
# Calculate rest of available surface for deductions # Calculate rest of available surface for deductions
available = acc.get_available_rest(as_percentage=True) available_total, available_relative = acc.get_available_rest()
deductions = acc.deductions.filter( deductions = acc.deductions.filter(
intervention__deleted=None, intervention__deleted=None,
@ -111,7 +197,8 @@ def open_view(request: HttpRequest, id: str):
"sum_before_states": sum_before_states, "sum_before_states": sum_before_states,
"sum_after_states": sum_after_states, "sum_after_states": sum_after_states,
"diff_states": diff_states, "diff_states": diff_states,
"available": available, "available": available_relative,
"available_total": available_total,
"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),
@ -340,7 +427,7 @@ def new_deduction_view(request: HttpRequest, id: str):
""" """
acc = get_object_or_404(EcoAccount, id=id) acc = get_object_or_404(EcoAccount, id=id)
form = NewDeductionForm(request.POST or None, instance=acc, user=request.user) form = NewDeductionModalForm(request.POST or None, instance=acc, user=request.user)
return form.process_request( return form.process_request(
request, request,
msg_success=_("Deduction added") msg_success=_("Deduction added")

@ -10,7 +10,7 @@ from django.contrib.auth.decorators import login_required
from django.http import HttpRequest from django.http import HttpRequest
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from compensation.forms import NewPaymentForm from compensation.forms.modalForms import NewPaymentForm
from compensation.models import Payment from compensation.models import Payment
from intervention.models import Intervention from intervention.models import Intervention
from konova.decorators import default_group_required from konova.decorators import default_group_required

@ -0,0 +1,159 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 06.10.21
"""
from django.contrib.auth.models import User
from django.db import transaction
from django.urls import reverse, reverse_lazy
from django.utils.translation import gettext_lazy as _
from compensation.forms.forms import AbstractCompensationForm, CompensationResponsibleFormMixin
from ema.models import Ema
from intervention.models import ResponsibilityData
from konova.forms import SimpleGeomForm
from user.models import UserActionLogEntry, UserAction
class NewEmaForm(AbstractCompensationForm, CompensationResponsibleFormMixin):
""" Form for creating new EMA objects.
Inherits basic form fields from AbstractCompensationForm and additional from CompensationResponsibleFormMixin.
Second holds self.instance.response related fields
"""
field_order = [
"identifier",
"title",
"conservation_office",
"conservation_file_number",
"handler",
"fundings",
"comment",
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_title = _("New EMA")
self.action_url = reverse("ema:new")
self.cancel_redirect = reverse("ema:index")
tmp = Ema()
identifier = tmp.generate_new_identifier()
self.initialize_form_field("identifier", identifier)
self.fields["identifier"].widget.attrs["url"] = reverse_lazy("ema:new-id")
self.fields["title"].widget.attrs["placeholder"] = _("Compensation XY; Location ABC")
def save(self, user: User, geom_form: SimpleGeomForm):
with transaction.atomic():
# Fetch data from cleaned POST values
identifier = self.cleaned_data.get("identifier", None)
title = self.cleaned_data.get("title", None)
fundings = self.cleaned_data.get("fundings", None)
handler = self.cleaned_data.get("handler", None)
conservation_office = self.cleaned_data.get("conservation_office", None)
conservation_file_number = self.cleaned_data.get("conservation_file_number", None)
comment = self.cleaned_data.get("comment", None)
# Create log entry
action = UserActionLogEntry.objects.create(
user=user,
action=UserAction.CREATED,
)
# Process the geometry form
geometry = geom_form.save(action)
responsible = ResponsibilityData.objects.create(
handler=handler,
conservation_file_number=conservation_file_number,
conservation_office=conservation_office,
)
# Finally create main object
acc = Ema.objects.create(
identifier=identifier,
title=title,
responsible=responsible,
created=action,
geometry=geometry,
comment=comment,
)
acc.fundings.set(fundings)
# Add the creating user to the list of shared users
acc.users.add(user)
# Add the log entry to the main objects log list
acc.log.add(action)
return acc
class EditEmaForm(NewEmaForm):
""" Form for editing EMAs
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_title = _("Edit EMA")
self.action_url = reverse("ema:edit", args=(self.instance.id,))
self.cancel_redirect = reverse("ema:open", args=(self.instance.id,))
self.fields["identifier"].widget.attrs["url"] = reverse_lazy("ema:new-id")
self.fields["title"].widget.attrs["placeholder"] = _("Compensation XY; Location ABC")
# Initialize form data
form_data = {
"identifier": self.instance.identifier,
"title": self.instance.title,
"handler": self.instance.responsible.handler,
"conservation_office": self.instance.responsible.conservation_office,
"conservation_file_number": self.instance.responsible.conservation_file_number,
"fundings": self.instance.fundings.all(),
"comment": self.instance.comment,
}
disabled_fields = []
self.load_initial_data(
form_data,
disabled_fields
)
def save(self, user: User, geom_form: SimpleGeomForm):
with transaction.atomic():
# Fetch data from cleaned POST values
identifier = self.cleaned_data.get("identifier", None)
title = self.cleaned_data.get("title", None)
fundings = self.cleaned_data.get("fundings", None)
handler = self.cleaned_data.get("handler", None)
conservation_office = self.cleaned_data.get("conservation_office", None)
conservation_file_number = self.cleaned_data.get("conservation_file_number", None)
comment = self.cleaned_data.get("comment", None)
# Create log entry
action = UserActionLogEntry.objects.create(
user=user,
action=UserAction.EDITED,
)
# Process the geometry form
geometry = geom_form.save(action)
# Update responsible data
self.instance.responsible.handler = handler
self.instance.responsible.conservation_office = conservation_office
self.instance.responsible.conservation_file_number = conservation_file_number
self.instance.responsible.save()
# Update main oject data
self.instance.identifier = identifier
self.instance.title = title
self.instance.geometry = geometry
self.instance.comment = comment
self.instance.modified = action
self.instance.save()
self.instance.fundings.set(fundings)
# Add the log entry to the main objects log list
self.instance.log.add(action)
return self.instance

@ -49,9 +49,9 @@ class Ema(AbstractCompensation):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if self.identifier is None or len(self.identifier) == 0: if self.identifier is None or len(self.identifier) == 0:
# Create new identifier # Create new identifier
new_id = self._generate_new_identifier() new_id = self.generate_new_identifier()
while Ema.objects.filter(identifier=new_id).exists(): while Ema.objects.filter(identifier=new_id).exists():
new_id = self._generate_new_identifier() new_id = self.generate_new_identifier()
self.identifier = new_id self.identifier = new_id
super().save(*args, **kwargs) super().save(*args, **kwargs)

@ -6,5 +6,5 @@ Created on: 19.08.21
""" """
EMA_ACCOUNT_IDENTIFIER_LENGTH = 10 EMA_ACCOUNT_IDENTIFIER_LENGTH = 6
EMA_ACCOUNT_IDENTIFIER_TEMPLATE = "EMA-{}" EMA_ACCOUNT_IDENTIFIER_TEMPLATE = "EMA-{}"

@ -24,7 +24,7 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
{% if is_default_member %} {% if is_default_member %}
<a href="{% url 'home' %}" class="mr-2"> <a href="{% url 'ema:edit' obj.id %}" class="mr-2">
<button class="btn btn-default" title="{% trans 'Edit' %}"> <button class="btn btn-default" title="{% trans 'Edit' %}">
{% fa5_icon 'edit' %} {% fa5_icon 'edit' %}
</button> </button>

@ -43,7 +43,7 @@
<td class="align-middle">{{obj.responsible.conservation_office.str_as_office|default_if_none:""}}</td> <td class="align-middle">{{obj.responsible.conservation_office.str_as_office|default_if_none:""}}</td>
</tr> </tr>
<tr {% if not obj.responsible.conservation_file_number %}class="alert alert-danger" title="{% trans 'Missing' %}" {% endif %}> <tr {% if not obj.responsible.conservation_file_number %}class="alert alert-danger" title="{% trans 'Missing' %}" {% endif %}>
<th scope="row">{% trans 'Conversation 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 {% if not obj.responsible.handler %}class="alert alert-danger" title="{% trans 'Missing' %}" {% endif %}> <tr {% if not obj.responsible.handler %}class="alert alert-danger" title="{% trans 'Missing' %}" {% endif %}>

@ -0,0 +1,6 @@
{% extends 'base.html' %}
{% load i18n l10n %}
{% block body %}
{% include 'form/collapsable/form.html' %}
{% endblock %}

@ -12,6 +12,7 @@ app_name = "ema"
urlpatterns = [ urlpatterns = [
path("", index_view, name="index"), path("", index_view, name="index"),
path("new/", new_view, name="new"), path("new/", new_view, name="new"),
path("new/id", new_id_view, name="new-id"),
path("<id>", open_view, name="open"), path("<id>", open_view, name="open"),
path('<id>/log', log_view, name='log'), path('<id>/log', log_view, name='log'),
path('<id>/edit', edit_view, name='edit'), path('<id>/edit', edit_view, name='edit'),

@ -1,12 +1,14 @@
from django.contrib import messages
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.db.models import Sum from django.db.models import Sum
from django.http import HttpRequest from django.http import HttpRequest, JsonResponse
from django.shortcuts import render, get_object_or_404 from django.shortcuts import render, get_object_or_404, redirect
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
import compensation import compensation
from compensation.forms import NewStateModalForm, NewActionModalForm, NewDeadlineModalForm from compensation.forms.modalForms import NewStateModalForm, NewActionModalForm, NewDeadlineModalForm
from ema.forms import NewEmaForm, EditEmaForm
from ema.tables import EmaTable from ema.tables import EmaTable
from konova.contexts import BaseContext from konova.contexts import BaseContext
from konova.decorators import conservation_office_group_required from konova.decorators import conservation_office_group_required
@ -14,6 +16,7 @@ from ema.models import Ema, EmaDocument
from konova.forms import RemoveModalForm, NewDocumentForm, SimpleGeomForm, RecordModalForm from konova.forms import RemoveModalForm, NewDocumentForm, SimpleGeomForm, RecordModalForm
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
from konova.utils.documents import get_document, remove_document from konova.utils.documents import get_document, remove_document
from konova.utils.message_templates import IDENTIFIER_REPLACED, FORM_INVALID
from konova.utils.user_checks import in_group from konova.utils.user_checks import in_group
@ -47,7 +50,8 @@ def index_view(request: HttpRequest):
@login_required @login_required
@conservation_office_group_required @conservation_office_group_required
def new_view(request: HttpRequest): def new_view(request: HttpRequest):
""" Renders the form for a new EMA """
Renders a view for a new eco account creation
Args: Args:
request (HttpRequest): The incoming request request (HttpRequest): The incoming request
@ -55,12 +59,54 @@ def new_view(request: HttpRequest):
Returns: Returns:
""" """
template = "generic_index.html" template = "ema/form/view.html"
context = {} data_form = NewEmaForm(request.POST or None)
geom_form = SimpleGeomForm(request.POST or None, read_only=False)
if request.method == "POST":
if data_form.is_valid() and geom_form.is_valid():
generated_identifier = data_form.cleaned_data.get("identifier", None)
ema = data_form.save(request.user, geom_form)
if generated_identifier != ema.identifier:
messages.info(
request,
IDENTIFIER_REPLACED.format(
generated_identifier,
ema.identifier
)
)
messages.success(request, _("EMA {} added").format(ema.identifier))
return redirect("ema:open", id=ema.id)
else:
messages.error(request, FORM_INVALID)
else:
# For clarification: nothing in this case
pass
context = {
"form": data_form,
"geom_form": geom_form,
}
context = BaseContext(request, context).context context = BaseContext(request, context).context
return render(request, template, context) return render(request, template, context)
@login_required
def new_id_view(request: HttpRequest):
""" JSON endpoint
Provides fetching of free identifiers for e.g. AJAX calls
"""
tmp = Ema()
identifier = tmp.generate_new_identifier()
while Ema.objects.filter(identifier=identifier).exists():
identifier = tmp.generate_new_identifier()
return JsonResponse(
data={
"identifier": identifier
}
)
@login_required @login_required
def open_view(request: HttpRequest, id: str): def open_view(request: HttpRequest, id: str):
""" Renders the detail view of an EMA """ Renders the detail view of an EMA
@ -133,7 +179,38 @@ def log_view(request: HttpRequest, id: str):
@login_required @login_required
def edit_view(request: HttpRequest, id: str): def edit_view(request: HttpRequest, id: str):
get_object_or_404(Ema, id=id) """
Renders a view for editing compensations
Args:
request (HttpRequest): The incoming request
Returns:
"""
template = "compensation/form/view.html"
# Get object from db
ema = get_object_or_404(Ema, id=id)
# Create forms, initialize with values from db/from POST request
data_form = EditEmaForm(request.POST or None, instance=ema)
geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=ema)
if request.method == "POST":
if data_form.is_valid() and geom_form.is_valid():
# The data form takes the geom form for processing, as well as the performing user
ema = data_form.save(request.user, geom_form)
messages.success(request, _("EMA {} edited").format(ema.identifier))
return redirect("ema:open", id=ema.id)
else:
messages.error(request, FORM_INVALID)
else:
# For clarification: nothing in this case
pass
context = {
"form": data_form,
"geom_form": geom_form,
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required @login_required

@ -12,7 +12,7 @@ from django.db.models import QuerySet, Q
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from intervention.forms import DummyFilterInput from intervention.inputs import DummyFilterInput
from intervention.models import Intervention from intervention.models import Intervention

@ -0,0 +1,359 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 02.12.20
"""
from dal import autocomplete
from django import forms
from django.contrib.auth.models import User
from django.db import transaction
from django.urls import reverse, reverse_lazy
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from codelist.models import KonovaCode
from codelist.settings import CODELIST_PROCESS_TYPE_ID, CODELIST_LAW_ID, \
CODELIST_REGISTRATION_OFFICE_ID, CODELIST_CONSERVATION_OFFICE_ID
from intervention.inputs import GenerateInput
from intervention.models import Intervention, LegalData, ResponsibilityData
from konova.forms import BaseForm, SimpleGeomForm
from user.models import UserActionLogEntry, UserAction
class NewInterventionForm(BaseForm):
identifier = forms.CharField(
label=_("Identifier"),
label_suffix="",
max_length=255,
help_text=_("Generated automatically"),
widget=GenerateInput(
attrs={
"class": "form-control",
"url": reverse_lazy("intervention:new-id"),
}
)
)
title = forms.CharField(
label=_("Title"),
label_suffix="",
help_text=_("An explanatory name"),
max_length=255,
widget=forms.TextInput(
attrs={
"placeholder": _("Construction XY; Location ABC"),
"class": "form-control",
}
)
)
type = forms.ModelChoiceField(
label=_("Process type"),
label_suffix="",
help_text=_(""),
required=False,
queryset=KonovaCode.objects.filter(
is_archived=False,
is_leaf=True,
code_lists__in=[CODELIST_PROCESS_TYPE_ID],
),
widget=autocomplete.ModelSelect2(
url="codes-process-type-autocomplete",
attrs={
"data-placeholder": _("Click for selection"),
}
),
)
laws = forms.ModelMultipleChoiceField(
label=_("Law"),
label_suffix="",
help_text=_("Multiple selection possible"),
required=False,
queryset=KonovaCode.objects.filter(
is_archived=False,
is_leaf=True,
code_lists__in=[CODELIST_LAW_ID],
),
widget=autocomplete.ModelSelect2Multiple(
url="codes-law-autocomplete",
attrs={
"data-placeholder": _("Click for selection"),
}
),
)
registration_office = forms.ModelChoiceField(
label=_("Registration office"),
label_suffix="",
required=False,
queryset=KonovaCode.objects.filter(
is_archived=False,
is_leaf=True,
code_lists__in=[CODELIST_REGISTRATION_OFFICE_ID],
),
widget=autocomplete.ModelSelect2(
url="codes-registration-office-autocomplete",
attrs={
"data-placeholder": _("Click for selection"),
}
),
)
conservation_office = forms.ModelChoiceField(
label=_("Conservation office"),
label_suffix="",
required=False,
queryset=KonovaCode.objects.filter(
is_archived=False,
is_leaf=True,
code_lists__in=[CODELIST_CONSERVATION_OFFICE_ID],
),
widget=autocomplete.ModelSelect2(
url="codes-conservation-office-autocomplete",
attrs={
"data-placeholder": _("Click for selection"),
}
),
)
registration_file_number = forms.CharField(
label=_("Registration office file number"),
label_suffix="",
max_length=255,
required=False,
widget=forms.TextInput(
attrs={
"placeholder": _("ZB-123/ABC.456"),
"class": "form-control",
}
)
)
conservation_file_number = forms.CharField(
label=_("Conservation office file number"),
label_suffix="",
max_length=255,
required=False,
widget=forms.TextInput(
attrs={
"placeholder": _("ETS-123/ABC.456"),
"class": "form-control",
}
)
)
handler = forms.CharField(
label=_("Intervention handler"),
label_suffix="",
max_length=255,
required=False,
help_text=_("Who performs the intervention"),
widget=forms.TextInput(
attrs={
"placeholder": _("Company Mustermann"),
"class": "form-control",
}
)
)
registration_date = forms.DateField(
label=_("Registration date"),
label_suffix=_(""),
required=False,
widget=forms.DateInput(
attrs={
"type": "date",
"class": "form-control",
},
format="%d.%m.%Y"
)
)
binding_date = forms.DateField(
label=_("Binding on"),
label_suffix=_(""),
required=False,
widget=forms.DateInput(
attrs={
"type": "date",
"class": "form-control",
},
format="%d.%m.%Y"
)
)
comment = forms.CharField(
label_suffix="",
label=_("Comment"),
required=False,
help_text=_("Additional comment"),
widget=forms.Textarea(
attrs={
"rows": 5,
"class": "form-control"
}
)
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_title = _("New intervention")
self.action_url = reverse("intervention:new")
self.cancel_redirect = reverse("intervention:index")
tmp_intervention = Intervention()
identifier = tmp_intervention.generate_new_identifier()
self.initialize_form_field("identifier", identifier)
def save(self, user: User, geom_form: SimpleGeomForm):
with transaction.atomic():
# Fetch data from cleaned POST values
identifier = self.cleaned_data.get("identifier", None)
title = self.cleaned_data.get("title", None)
_type = self.cleaned_data.get("type", None)
laws = self.cleaned_data.get("laws", None)
handler = self.cleaned_data.get("handler", None)
registration_office = self.cleaned_data.get("registration_office", None)
conservation_office = self.cleaned_data.get("conservation_office", None)
conservation_file_number = self.cleaned_data.get("conservation_file_number", None)
registration_file_number = self.cleaned_data.get("registration_file_number", None)
binding_date = self.cleaned_data.get("binding_date", None)
registration_date = self.cleaned_data.get("registration_date", None)
comment = self.cleaned_data.get("comment", None)
# Create log entry
action = UserActionLogEntry.objects.create(
user=user,
action=UserAction.CREATED,
)
# Create legal data object (without M2M laws first)
legal_data = LegalData.objects.create(
registration_date=registration_date,
binding_date=binding_date,
process_type=_type,
)
# Then add the M2M laws to the object
legal_data.laws.set(laws)
# Create responsible data object
responsibility_data = ResponsibilityData.objects.create(
registration_office=registration_office,
conservation_office=conservation_office,
registration_file_number=registration_file_number,
conservation_file_number=conservation_file_number,
handler=handler,
)
# Process the geometry form
geometry = geom_form.save(action)
# Finally create main object, holding the other objects
intervention = Intervention.objects.create(
identifier=identifier,
title=title,
responsible=responsibility_data,
legal=legal_data,
created=action,
geometry=geometry,
comment=comment,
)
# Add the log entry to the main objects log list
intervention.log.add(action)
# Add the performing user as the first user having access to the data
intervention.users.add(user)
return intervention
class EditInterventionForm(NewInterventionForm):
""" Subclasses NewInterventionForm
Simply adds initializing of a provided self.instance object into the form fields
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.instance is not None:
self.action_url = reverse("intervention:edit", args=(self.instance.id,))
self.cancel_redirect = reverse("intervention:open", args=(self.instance.id,))
self.form_title = _("Edit intervention")
self.form_caption = ""
reg_date = self.instance.legal.registration_date
bind_date = self.instance.legal.binding_date
if reg_date is not None:
reg_date = reg_date.isoformat()
if bind_date is not None:
bind_date = bind_date.isoformat()
# Initialize form data
form_data = {
"identifier": self.instance.identifier,
"title": self.instance.title,
"type": self.instance.legal.process_type,
"laws": list(self.instance.legal.laws.values_list("id", flat=True)),
"handler": self.instance.responsible.handler,
"registration_office": self.instance.responsible.registration_office,
"registration_file_number": self.instance.responsible.registration_file_number,
"conservation_office": self.instance.responsible.conservation_office,
"conservation_file_number": self.instance.responsible.conservation_file_number,
"registration_date": reg_date,
"binding_date": bind_date,
"comment": self.instance.comment,
}
disabled_fields = []
self.load_initial_data(
form_data,
disabled_fields
)
def save(self, user: User, geom_form: SimpleGeomForm):
""" Overwrite instance with new form data
Args:
user ():
Returns:
"""
with transaction.atomic():
identifier = self.cleaned_data.get("identifier", None)
title = self.cleaned_data.get("title", None)
process_type = self.cleaned_data.get("type", None)
laws = self.cleaned_data.get("laws", None)
handler = self.cleaned_data.get("handler", None)
registration_office = self.cleaned_data.get("registration_office", None)
registration_file_number = self.cleaned_data.get("registration_file_number", None)
conservation_office = self.cleaned_data.get("conservation_office", None)
conservation_file_number = self.cleaned_data.get("conservation_file_number", None)
registration_date = self.cleaned_data.get("registration_date", None)
binding_date = self.cleaned_data.get("binding_date", None)
comment = self.cleaned_data.get("comment", None)
self.instance.legal.process_type = process_type
self.instance.legal.registration_date = registration_date
self.instance.legal.binding_date = binding_date
self.instance.legal.laws.set(laws)
self.instance.legal.save()
self.instance.responsible.handler = handler
self.instance.responsible.registration_office = registration_office
self.instance.responsible.registration_file_number = registration_file_number
self.instance.responsible.conservation_office = conservation_office
self.instance.responsible.conservation_file_number = conservation_file_number
self.instance.responsible.save()
user_action = UserActionLogEntry.objects.create(
user=user,
timestamp=timezone.now(),
action=UserAction.EDITED,
)
geometry = geom_form.save(user_action)
self.instance.geometry = geometry
self.instance.geometry.save()
self.instance.log.add(user_action)
self.instance.identifier = identifier
self.instance.title = title
self.instance.comment = comment
self.instance.modified = user_action
self.instance.save()
return self.instance

@ -2,244 +2,28 @@
Author: Michel Peltriaux Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 02.12.20 Created on: 27.09.21
""" """
from dal import autocomplete from dal import autocomplete
from django import forms
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.gis import forms as gis_forms
from django.contrib.gis.geos import Polygon
from django.db import transaction from django.db import transaction
from django import forms
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from compensation.models import EcoAccountDeduction, EcoAccount from compensation.models import EcoAccount, EcoAccountDeduction
from intervention.models import Intervention, Revocation, RevocationDocument from intervention.inputs import TextToClipboardInput
from konova.forms import BaseForm, BaseModalForm from intervention.models import Revocation, RevocationDocument, Intervention
from konova.settings import DEFAULT_LAT, DEFAULT_LON, DEFAULT_ZOOM, ZB_GROUP, ETS_GROUP from konova.forms import BaseModalForm
from konova.settings import ZB_GROUP, ETS_GROUP
from konova.utils.general import format_german_float from konova.utils.general import format_german_float
from konova.utils.messenger import Messenger from konova.utils.messenger import Messenger
from konova.utils.user_checks import in_group from konova.utils.user_checks import in_group
from organisation.models import Organisation
from user.models import UserActionLogEntry, UserAction from user.models import UserActionLogEntry, UserAction
class NewInterventionForm(BaseForm): class ShareInterventionModalForm(BaseModalForm):
identifier = forms.CharField(
label=_("Identifier"),
label_suffix="",
max_length=255,
help_text=_("Generated automatically if none was given"),
required=False,
)
title = forms.CharField(
label=_("Title"),
label_suffix="",
max_length=255,
)
type = forms.CharField(
label=_("Type"),
label_suffix="",
max_length=255,
help_text=_("Which intervention type is this"),
)
law = forms.CharField(
label=_("Law"),
label_suffix="",
max_length=255,
help_text=_("Based on which law"),
)
handler = forms.CharField(
label=_("Intervention handler"),
label_suffix="",
max_length=255,
help_text=_("Who performs the intervention"),
)
data_provider = forms.ModelChoiceField(
label=_("Data provider"),
label_suffix="",
help_text=_("Who provides the data for the intervention"),
queryset=Organisation.objects.all(),
widget=autocomplete.ModelSelect2(
url="other-orgs-autocomplete",
attrs={
"data-placeholder": _("Organization"),
"data-minimum-input-length": 3,
}
),
)
data_provider_detail = forms.CharField(
label=_("Data provider details"),
label_suffix="",
max_length=255,
help_text=_("Further details"),
required=False,
)
geometry = gis_forms.MultiPolygonField(
widget=gis_forms.OSMWidget(
attrs={
"default_lat": DEFAULT_LAT,
"default_lon": DEFAULT_LON,
"default_zoom": DEFAULT_ZOOM,
'map_width': 800,
'map_height': 500
},
),
label=_("Map"),
label_suffix="",
help_text=_("Where does the intervention take place")
)
documents = forms.FileField(
widget=forms.ClearableFileInput(
attrs={
"multiple": True,
}
),
label=_("Files"),
label_suffix="",
required=False,
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_title = _("New intervention")
self.action_url = reverse("intervention:new")
self.cancel_redirect = reverse("intervention:index")
def save(self, user: User):
with transaction.atomic():
identifier = self.cleaned_data.get("identifier", None)
title = self.cleaned_data.get("title", None)
_type = self.cleaned_data.get("type", None)
law = self.cleaned_data.get("law", None)
handler = self.cleaned_data.get("handler", None)
data_provider = self.cleaned_data.get("data_provider", None)
data_provider_detail = self.cleaned_data.get("data_provider_detail", None)
geometry = self.cleaned_data.get("geometry", Polygon())
documents = self.cleaned_data.get("documents", []) or []
action = UserActionLogEntry.objects.create(
user=user,
action=UserAction.CREATED,
)
intervention = Intervention(
identifier=identifier,
title=title,
type=_type,
law=law,
handler=handler,
data_provider=data_provider,
data_provider_detail=data_provider_detail,
geometry=geometry,
created=action,
)
intervention.save()
intervention.log.add(action)
return intervention
class EditInterventionForm(NewInterventionForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.instance is not None:
self.action_url = reverse("intervention:edit", args=(self.instance.id,))
self.cancel_redirect = reverse("intervention:index")
self.form_title = _("Edit intervention")
self.form_caption = ""
# Initialize form data
form_data = {
"identifier": self.instance.identifier,
"title": self.instance.title,
"type": self.instance.type,
"law": self.instance.law,
"handler": self.instance.handler,
"data_provider": self.instance.data_provider,
"data_provider_detail": self.instance.data_provider_detail,
"geometry": self.instance.geometry,
"documents": self.instance.documents.all(),
}
disabled_fields = [
"identifier",
]
self.load_initial_data(
form_data,
disabled_fields,
)
def save(self, user: User):
""" Overwrite instance with new form data
Args:
user ():
Returns:
"""
with transaction.atomic():
identifier = self.cleaned_data.get("identifier", None)
title = self.cleaned_data.get("title", None)
_type = self.cleaned_data.get("type", None)
law = self.cleaned_data.get("law", None)
handler = self.cleaned_data.get("handler", None)
data_provider = self.cleaned_data.get("data_provider", None)
data_provider_detail = self.cleaned_data.get("data_provider_detail", None)
geometry = self.cleaned_data.get("geometry", Polygon())
documents = self.cleaned_data.get("documents", []) or []
self.instance.identifier = identifier
self.instance.title = title
self.instance.type = _type
self.instance.law = law
self.instance.handler = handler
self.instance.data_provider = data_provider
self.instance.data_provider_detail = data_provider_detail
self.instance.geometry = geometry
self.instance.save()
user_action = UserActionLogEntry.objects.create(
user=self.user,
action=UserAction.EDITED
)
self.instance.log.add(user_action)
self.instance.modified = user_action
self.instance.save()
return self.instance
class OpenInterventionForm(EditInterventionForm):
"""
This form is not intended to be used as data-input form. It's used to simplify the rendering of intervention:open
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Resize map
self.fields["geometry"].widget.attrs["map_width"] = 500
self.fields["geometry"].widget.attrs["map_height"] = 300
# Disable all form fields
for field in self.fields:
self.disable_form_field(field)
class DummyFilterInput(forms.HiddenInput):
""" A dummy input widget
Does not render anything. Can be used to keep filter logic using django_filter without having a pre defined
filter widget being rendered to the template.
"""
template_name = "konova/custom_widgets/dummy-filter-input.html"
class TextToClipboardInput(forms.TextInput):
template_name = "konova/custom_widgets/text-to-clipboard-input.html"
class ShareInterventionForm(BaseModalForm):
url = forms.CharField( url = forms.CharField(
label=_("Share link"), label=_("Share link"),
label_suffix="", label_suffix="",
@ -247,7 +31,8 @@ class ShareInterventionForm(BaseModalForm):
required=False, required=False,
widget=TextToClipboardInput( widget=TextToClipboardInput(
attrs={ attrs={
"readonly": True "readonly": True,
"class": "form-control",
} }
) )
) )
@ -316,7 +101,7 @@ class ShareInterventionForm(BaseModalForm):
self.instance.users.set(accessing_users) self.instance.users.set(accessing_users)
class NewRevocationForm(BaseModalForm): class NewRevocationModalForm(BaseModalForm):
date = forms.DateField( date = forms.DateField(
label=_("Date"), label=_("Date"),
label_suffix=_(""), label_suffix=_(""),
@ -325,6 +110,7 @@ class NewRevocationForm(BaseModalForm):
attrs={ attrs={
"type": "date", "type": "date",
"data-provide": "datepicker", "data-provide": "datepicker",
"class": "form-control",
}, },
format="%d.%m.%Y" format="%d.%m.%Y"
) )
@ -336,7 +122,7 @@ class NewRevocationForm(BaseModalForm):
help_text=_("Must be smaller than 15 Mb"), help_text=_("Must be smaller than 15 Mb"),
widget=forms.FileInput( widget=forms.FileInput(
attrs={ attrs={
"class": "w-75" "class": "form-control-file"
} }
) )
) )
@ -350,6 +136,7 @@ class NewRevocationForm(BaseModalForm):
attrs={ attrs={
"cols": 30, "cols": 30,
"rows": 5, "rows": 5,
"class": "form-control",
} }
) )
) )
@ -394,7 +181,7 @@ class NewRevocationForm(BaseModalForm):
return revocation return revocation
class RunCheckForm(BaseModalForm): class RunCheckModalForm(BaseModalForm):
checked_intervention = forms.BooleanField( checked_intervention = forms.BooleanField(
label=_("Checked intervention data"), label=_("Checked intervention data"),
label_suffix="", label_suffix="",
@ -458,7 +245,7 @@ class RunCheckForm(BaseModalForm):
) )
class NewDeductionForm(BaseModalForm): class NewDeductionModalForm(BaseModalForm):
""" Form for creating new deduction """ Form for creating new deduction
Can be used for Intervention view as well as for EcoAccount views. Can be used for Intervention view as well as for EcoAccount views.
@ -487,6 +274,12 @@ class NewDeductionForm(BaseModalForm):
label=_("Surface"), label=_("Surface"),
label_suffix="", label_suffix="",
help_text=_("in m²"), help_text=_("in m²"),
widget=forms.NumberInput(
attrs={
"class": "form-control",
"placeholder": "0,00",
}
)
) )
intervention = forms.ModelChoiceField( intervention = forms.ModelChoiceField(
label=_("Intervention"), label=_("Intervention"),
@ -508,9 +301,6 @@ class NewDeductionForm(BaseModalForm):
self.form_caption = _("Enter the information for a new deduction from a chosen eco-account") self.form_caption = _("Enter the information for a new deduction from a chosen eco-account")
self.is_intervention_initially = False self.is_intervention_initially = False
# Add a placeholder for field 'surface' without having to define the whole widget above
self.add_placeholder_for_field("surface", "0,00")
# Check for Intervention or EcoAccount # Check for Intervention or EcoAccount
if isinstance(self.instance, Intervention): if isinstance(self.instance, Intervention):
# Form has been called with a given intervention # Form has been called with a given intervention

@ -0,0 +1,32 @@
from django import forms
class DummyFilterInput(forms.HiddenInput):
""" A dummy input widget
Does not render anything. Can be used to keep filter logic using django_filter without having a pre defined
filter widget being rendered to the template.
"""
template_name = "konova/widgets/empty-dummy-input.html"
class TextToClipboardInput(forms.TextInput):
template_name = "konova/widgets/text-to-clipboard-input.html"
class GenerateInput(forms.TextInput):
"""
Provides a form group with a button at the end, which generates new content for the input.
The url used to fetch new content can be added using the attrs like
widget=GenerateInput(
attrs={
"url": reverse_lazy("app_name:view_name")
...
}
)
"""
template_name = "konova/widgets/generate-content-input.html"

@ -260,14 +260,41 @@ class Intervention(BaseObject):
self.save() self.save()
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
""" Custom save functionality
Performs some pre-save checks:
1. Checking for existing identifiers
Args:
*args ():
**kwargs ():
Returns:
"""
if self.identifier is None or len(self.identifier) == 0: if self.identifier is None or len(self.identifier) == 0:
# Create new identifier # No identifier given
new_id = self._generate_new_identifier() self.identifier = self.generate_new_identifier()
while Intervention.objects.filter(identifier=new_id).exists():
new_id = self._generate_new_identifier() # Before saving, make sure the set identifier is not used, yet
self.identifier = new_id while Intervention.objects.filter(identifier=self.identifier).exists():
self.identifier = self.generate_new_identifier()
super().save(*args, **kwargs) super().save(*args, **kwargs)
def delete(self, using=None, keep_parents=False):
to_delete = [
self.legal,
self.responsible,
self.geometry,
self.log.all()
]
for entry in to_delete:
try:
entry.delete()
except AttributeError:
pass
super().delete(using, keep_parents)
def quality_check(self) -> list: def quality_check(self) -> list:
""" Quality check """ Quality check
@ -298,7 +325,7 @@ class Intervention(BaseObject):
ret_msgs.append(_("Registration office file number missing")) ret_msgs.append(_("Registration office file number missing"))
if not self.responsible.conservation_file_number or len(self.responsible.conservation_file_number) == 0: if not self.responsible.conservation_file_number or len(self.responsible.conservation_file_number) == 0:
ret_msgs.append(_("Conversation office file number missing")) ret_msgs.append(_("Conservation office file number missing"))
except AttributeError: except AttributeError:
# responsible data not found # responsible data not found
ret_msgs.append(_("Responsible data missing")) ret_msgs.append(_("Responsible data missing"))

@ -5,5 +5,5 @@ Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 30.11.20 Created on: 30.11.20
""" """
INTERVENTION_IDENTIFIER_LENGTH = 10 INTERVENTION_IDENTIFIER_LENGTH = 6
INTERVENTION_IDENTIFIER_TEMPLATE = "EIV-{}" INTERVENTION_IDENTIFIER_TEMPLATE = "EIV-{}"

@ -0,0 +1,23 @@
{% load i18n fontawesome_5 %}
{% if intervention.comment %}
<div class="w-100">
<div class="card mt-3">
<div class="card-header rlp-gd">
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-12">
<h5 class="card-title">
{% fa5_icon 'info-circle' %}
{% trans 'Comment' %}
</h5>
</div>
</div>
</div>
<div class="card-body">
<div class="card-text font-italic">
{{intervention.comment}}
</div>
</div>
</div>
</div>
{% endif %}

@ -11,7 +11,7 @@
<div class="col-sm-6"> <div class="col-sm-6">
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
{% if is_default_member and has_access %} {% if is_default_member and has_access %}
<a href="{% url 'compensation:new' %}" title="{% trans 'Add new compensation' %}"> <a href="{% url 'compensation:new' intervention.id %}" title="{% trans 'Add new compensation' %}">
<button class="btn btn-outline-default"> <button class="btn btn-outline-default">
{% fa5_icon 'plus' %} {% fa5_icon 'plus' %}
{% fa5_icon 'leaf' %} {% fa5_icon 'leaf' %}

@ -32,7 +32,7 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
{% if is_default_member %} {% if is_default_member %}
<a href="{% url 'home' %}" class="mr-2"> <a href="{% url 'intervention:edit' intervention.id %}" class="mr-2">
<button class="btn btn-default" title="{% trans 'Edit' %}"> <button class="btn btn-default" title="{% trans 'Edit' %}">
{% fa5_icon 'edit' %} {% fa5_icon 'edit' %}
</button> </button>

@ -56,7 +56,7 @@
<td class="align-middle">{{intervention.responsible.conservation_office.str_as_office|default_if_none:""}}</td> <td class="align-middle">{{intervention.responsible.conservation_office.str_as_office|default_if_none:""}}</td>
</tr> </tr>
<tr {% if not intervention.responsible.conservation_file_number %}class="alert alert-danger" title="{% trans 'Missing' %}" {% endif %}> <tr {% if not intervention.responsible.conservation_file_number %}class="alert alert-danger" title="{% trans 'Missing' %}" {% endif %}>
<th scope="row">{% trans 'Conversation office file number' %}</th> <th scope="row">{% trans 'Conservation office file number' %}</th>
<td class="align-middle">{{intervention.responsible.conservation_file_number|default_if_none:""}}</td> <td class="align-middle">{{intervention.responsible.conservation_file_number|default_if_none:""}}</td>
</tr> </tr>
<tr {% if not intervention.responsible.handler %}class="alert alert-danger" title="{% trans 'Missing' %}" {% endif %}> <tr {% if not intervention.responsible.handler %}class="alert alert-danger" title="{% trans 'Missing' %}" {% endif %}>
@ -123,8 +123,13 @@
</div> </div>
</div> </div>
<div class="col-sm-12 col-md-12 col-lg-6"> <div class="col-sm-12 col-md-12 col-lg-6">
<div class="row">
{% include 'map/geom_form.html' %} {% include 'map/geom_form.html' %}
</div> </div>
<div class="row">
{% include 'intervention/detail/includes/comment.html' %}
</div>
</div>
</div> </div>
<hr> <hr>

@ -0,0 +1,6 @@
{% extends 'base.html' %}
{% load i18n l10n %}
{% block body %}
{% include 'form/collapsable/form.html' %}
{% endblock %}

@ -9,12 +9,13 @@ from django.urls import path
from intervention.views import index_view, new_view, open_view, edit_view, remove_view, new_document_view, share_view, \ from intervention.views import index_view, new_view, open_view, edit_view, remove_view, new_document_view, share_view, \
create_share_view, remove_revocation_view, new_revocation_view, run_check_view, log_view, new_deduction_view, \ create_share_view, remove_revocation_view, new_revocation_view, run_check_view, log_view, new_deduction_view, \
record_view, remove_document_view, get_document_view, get_revocation_view record_view, remove_document_view, get_document_view, get_revocation_view, new_id_view
app_name = "intervention" app_name = "intervention"
urlpatterns = [ urlpatterns = [
path("", index_view, name="index"), path("", index_view, name="index"),
path('new/', new_view, name='new'), path('new/', new_view, name='new'),
path('new/id', new_id_view, name='new-id'),
path('<id>', open_view, name='open'), path('<id>', open_view, name='open'),
path('<id>/log', log_view, name='log'), path('<id>/log', log_view, name='log'),
path('<id>/edit', edit_view, name='edit'), path('<id>/edit', edit_view, name='edit'),

@ -1,11 +1,11 @@
from django.contrib import messages
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.http import HttpRequest from django.http import HttpRequest, JsonResponse
from django.shortcuts import render, get_object_or_404 from django.shortcuts import render, get_object_or_404
from intervention.forms import NewInterventionForm, EditInterventionForm, ShareInterventionForm, NewRevocationForm, \ from intervention.forms.forms import NewInterventionForm, EditInterventionForm
RunCheckForm, NewDeductionForm from intervention.forms.modalForms import ShareInterventionModalForm, NewRevocationModalForm, \
RunCheckModalForm, NewDeductionModalForm
from intervention.models import Intervention, Revocation, InterventionDocument, RevocationDocument from intervention.models import Intervention, Revocation, InterventionDocument, RevocationDocument
from intervention.tables import InterventionTable from intervention.tables import InterventionTable
from konova.contexts import BaseContext from konova.contexts import BaseContext
@ -13,7 +13,7 @@ from konova.decorators import *
from konova.forms import SimpleGeomForm, NewDocumentForm, RemoveModalForm, RecordModalForm from konova.forms import SimpleGeomForm, NewDocumentForm, RemoveModalForm, RecordModalForm
from konova.sub_settings.django_settings import DEFAULT_DATE_FORMAT from konova.sub_settings.django_settings import DEFAULT_DATE_FORMAT
from konova.utils.documents import remove_document, get_document from konova.utils.documents import remove_document, get_document
from konova.utils.message_templates import FORM_INVALID, INTERVENTION_INVALID from konova.utils.message_templates import INTERVENTION_INVALID, FORM_INVALID, IDENTIFIER_REPLACED
from konova.utils.user_checks import in_group from konova.utils.user_checks import in_group
@ -58,25 +58,54 @@ def new_view(request: HttpRequest):
Returns: Returns:
""" """
template = "konova/form.html" template = "intervention/form/view.html"
form = NewInterventionForm(request.POST or None) data_form = NewInterventionForm(request.POST or None)
geom_form = SimpleGeomForm(request.POST or None, read_only=False)
if request.method == "POST": if request.method == "POST":
if form.is_valid(): if data_form.is_valid() and geom_form.is_valid():
intervention = form.save(request.user) generated_identifier = data_form.cleaned_data.get("identifier", None)
messages.success(request, _("Intervention {} added").format(intervention.title)) intervention = data_form.save(request.user, geom_form)
return redirect("intervention:index") if generated_identifier != intervention.identifier:
messages.info(
request,
IDENTIFIER_REPLACED.format(
generated_identifier,
intervention.identifier
)
)
messages.success(request, _("Intervention {} added").format(intervention.identifier))
return redirect("intervention:open", id=intervention.id)
else: else:
messages.error(request, _("Invalid input")) messages.error(request, FORM_INVALID)
else: else:
# For clarification: nothing in this case # For clarification: nothing in this case
pass pass
context = { context = {
"form": form, "form": data_form,
"geom_form": geom_form,
} }
context = BaseContext(request, context).context context = BaseContext(request, context).context
return render(request, template, context) return render(request, template, context)
@login_required
def new_id_view(request: HttpRequest):
""" JSON endpoint
Provides fetching of free identifiers for e.g. AJAX calls
"""
tmp_intervention = Intervention()
identifier = tmp_intervention.generate_new_identifier()
while Intervention.objects.filter(identifier=identifier).exists():
identifier = tmp_intervention.generate_new_identifier()
return JsonResponse(
data={
"identifier": identifier
}
)
@login_required @login_required
def new_document_view(request: HttpRequest, id: str): def new_document_view(request: HttpRequest, id: str):
""" Renders a form for uploading new documents """ Renders a form for uploading new documents
@ -212,19 +241,26 @@ def edit_view(request: HttpRequest, id: str):
Returns: Returns:
""" """
template = "konova/form.html" template = "intervention/form/view.html"
# Get object from db
intervention = get_object_or_404(Intervention, id=id) intervention = get_object_or_404(Intervention, id=id)
# Create forms, initialize with values from db/from POST request
data_form = EditInterventionForm(request.POST or None, instance=intervention)
geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=intervention)
if request.method == "POST": if request.method == "POST":
form = EditInterventionForm(request.POST or None, instance=intervention) if data_form.is_valid() and geom_form.is_valid():
if form.is_valid(): # The data form takes the geom form for processing, as well as the performing user
intervention = form.save(request.user) intervention = data_form.save(request.user, geom_form)
messages.success(request, _("{} edited").format(intervention)) messages.success(request, _("Intervention {} edited").format(intervention.identifier))
return redirect("intervention:index") return redirect("intervention:open", id=intervention.id)
else: else:
messages.error(request, _("Invalid input")) messages.error(request, FORM_INVALID)
form = EditInterventionForm(instance=intervention) else:
# For clarification: nothing in this case
pass
context = { context = {
"form": form, "form": data_form,
"geom_form": geom_form,
} }
context = BaseContext(request, context).context context = BaseContext(request, context).context
return render(request, template, context) return render(request, template, context)
@ -324,7 +360,7 @@ def create_share_view(request: HttpRequest, id: str):
""" """
intervention = get_object_or_404(Intervention, id=id) intervention = get_object_or_404(Intervention, id=id)
form = ShareInterventionForm(request.POST or None, instance=intervention, request=request) form = ShareInterventionModalForm(request.POST or None, instance=intervention, request=request)
return form.process_request( return form.process_request(
request, request,
msg_success=_("Share settings updated") msg_success=_("Share settings updated")
@ -343,7 +379,7 @@ def run_check_view(request: HttpRequest, id: str):
""" """
intervention = get_object_or_404(Intervention, id=id) intervention = get_object_or_404(Intervention, id=id)
form = RunCheckForm(request.POST or None, instance=intervention, user=request.user) form = RunCheckModalForm(request.POST or None, instance=intervention, user=request.user)
return form.process_request( return form.process_request(
request, request,
msg_success=_("Check performed"), msg_success=_("Check performed"),
@ -363,7 +399,7 @@ def new_revocation_view(request: HttpRequest, id: str):
""" """
intervention = get_object_or_404(Intervention, id=id) intervention = get_object_or_404(Intervention, id=id)
form = NewRevocationForm(request.POST or None, request.FILES or None, instance=intervention, user=request.user) form = NewRevocationModalForm(request.POST or None, request.FILES or None, instance=intervention, user=request.user)
return form.process_request( return form.process_request(
request, request,
msg_success=_("Revocation added") msg_success=_("Revocation added")
@ -407,7 +443,7 @@ def new_deduction_view(request: HttpRequest, id: str):
""" """
intervention = get_object_or_404(Intervention, id=id) intervention = get_object_or_404(Intervention, id=id)
form = NewDeductionForm(request.POST or None, instance=intervention, user=request.user) form = NewDeductionModalForm(request.POST or None, instance=intervention, user=request.user)
return form.process_request( return form.process_request(
request, request,
msg_success=_("Deduction added") msg_success=_("Deduction added")

@ -10,7 +10,8 @@ from django.db.models import Q
from codelist.models import KonovaCode from codelist.models import KonovaCode
from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_BIOTOPES_ID, CODELIST_LAW_ID, \ from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_BIOTOPES_ID, CODELIST_LAW_ID, \
CODELIST_REGISTRATION_OFFICE_ID, CODELIST_CONSERVATION_OFFICE_ID, CODELIST_PROCESS_TYPE_ID CODELIST_REGISTRATION_OFFICE_ID, CODELIST_CONSERVATION_OFFICE_ID, CODELIST_PROCESS_TYPE_ID, \
CODELIST_COMPENSATION_FUNDING_ID
from compensation.models import EcoAccount from compensation.models import EcoAccount
from intervention.models import Intervention from intervention.models import Intervention
from organisation.models import Organisation from organisation.models import Organisation
@ -45,12 +46,18 @@ class NonOfficialOrganisationAutocomplete(Select2QuerySetView):
class EcoAccountAutocomplete(Select2QuerySetView): class EcoAccountAutocomplete(Select2QuerySetView):
""" Autocomplete for ecoAccount entries
Only returns entries that are accessible for the requesting user and already are recorded
"""
def get_queryset(self): def get_queryset(self):
if self.request.user.is_anonymous: if self.request.user.is_anonymous:
return EcoAccount.objects.none() return EcoAccount.objects.none()
qs = EcoAccount.objects.filter( qs = EcoAccount.objects.filter(
deleted=None, deleted=None,
recorded__isnull=False, recorded__isnull=False,
users__in=[self.request.user],
) )
if self.q: if self.q:
qs = qs.filter( qs = qs.filter(
@ -63,6 +70,11 @@ class EcoAccountAutocomplete(Select2QuerySetView):
class InterventionAutocomplete(Select2QuerySetView): class InterventionAutocomplete(Select2QuerySetView):
""" Autocomplete for intervention entries
Only returns entries that are accessible for the requesting user
"""
def get_queryset(self): def get_queryset(self):
if self.request.user.is_anonymous: if self.request.user.is_anonymous:
return Intervention.objects.none() return Intervention.objects.none()
@ -104,10 +116,18 @@ class KonovaCodeAutocomplete(Select2QuerySetView):
code_lists__in=[self.c] code_lists__in=[self.c]
) )
if self.q: if self.q:
# Remove whitespaces from self.q and split input in all keywords (if multiple given)
q = dict.fromkeys(self.q.strip().split(" "))
# Create one filter looking up for all keys where all keywords can be found in the same result
_filter = Q()
for keyword in q:
q_or = Q() q_or = Q()
q_or |= Q(long_name__icontains=self.q) q_or |= Q(long_name__icontains=keyword)
q_or |= Q(short_name__icontains=self.q) q_or |= Q(short_name__icontains=keyword)
qs = qs.filter(q_or) q_or |= Q(parent__long_name__icontains=keyword)
q_or |= Q(parent__short_name__icontains=keyword)
_filter.add(q_or, Q.AND)
qs = qs.filter(_filter).distinct()
return qs return qs
@ -120,6 +140,15 @@ class CompensationActionCodeAutocomplete(KonovaCodeAutocomplete):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
class CompensationFundingCodeAutocomplete(KonovaCodeAutocomplete):
"""
Due to limitations of the django dal package, we need to subclass for each code list
"""
def __init__(self, *args, **kwargs):
self.c = CODELIST_COMPENSATION_FUNDING_ID
super().__init__(*args, **kwargs)
class BiotopeCodeAutocomplete(KonovaCodeAutocomplete): class BiotopeCodeAutocomplete(KonovaCodeAutocomplete):
""" """
Due to limitations of the django dal package, we need to subclass for each code list Due to limitations of the django dal package, we need to subclass for each code list

@ -9,11 +9,12 @@ Created on: 16.11.20
from functools import wraps from functools import wraps
from django.contrib import messages from django.contrib import messages
from django.shortcuts import redirect from django.shortcuts import redirect, get_object_or_404
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from konova.settings import DEFAULT_GROUP, ETS_GROUP, ZB_GROUP from konova.settings import DEFAULT_GROUP, ETS_GROUP, ZB_GROUP
from konova.utils.message_templates import MISSING_GROUP_PERMISSION, DATA_UNSHARED
def staff_required(function): def staff_required(function):
@ -80,7 +81,7 @@ def default_group_required(function):
if has_group: if has_group:
return function(request, *args, **kwargs) return function(request, *args, **kwargs)
else: else:
messages.info(request, _("You need to be part of another user group.")) messages.info(request, MISSING_GROUP_PERMISSION)
return redirect(request.META.get("HTTP_REFERER", reverse("home"))) return redirect(request.META.get("HTTP_REFERER", reverse("home")))
return wrap return wrap
@ -100,7 +101,7 @@ def registration_office_group_required(function):
if has_group: if has_group:
return function(request, *args, **kwargs) return function(request, *args, **kwargs)
else: else:
messages.info(request, _("You need to be part of another user group.")) messages.info(request, MISSING_GROUP_PERMISSION)
return redirect(request.META.get("HTTP_REFERER", reverse("home"))) return redirect(request.META.get("HTTP_REFERER", reverse("home")))
return wrap return wrap
@ -120,6 +121,35 @@ def conservation_office_group_required(function):
if has_group: if has_group:
return function(request, *args, **kwargs) return function(request, *args, **kwargs)
else: else:
messages.info(request, _("You need to be part of another user group.")) messages.info(request, MISSING_GROUP_PERMISSION)
return redirect(request.META.get("HTTP_REFERER", reverse("home"))) return redirect(request.META.get("HTTP_REFERER", reverse("home")))
return wrap return wrap
def shared_access_required(obj_class, id_key):
""" Checks whether the data is shared with the requesting user
Args:
obj_class (Model): The object/model class
id_key (str): The name of the identifier attribute in **kwargs
Returns:
"""
def decorator(function):
@wraps(function)
def wrap(request, *args, **kwargs):
user = request.user
_id = kwargs.get(id_key, None)
if _id is not None:
obj = get_object_or_404(obj_class, id=_id)
is_shared = obj.is_shared_with(user)
if not is_shared:
messages.info(
request,
DATA_UNSHARED
)
return redirect("home")
return function(request, *args, **kwargs)
return wrap
return decorator

@ -13,8 +13,8 @@ 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.auth.models import User from django.contrib.auth.models import User
from django.contrib.gis.forms import GeometryField, OSMWidget from django.contrib.gis.forms import OSMWidget, MultiPolygonField
from django.contrib.gis.geos import Polygon from django.contrib.gis.geos import Polygon, MultiPolygon
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
@ -25,7 +25,8 @@ from compensation.models import EcoAccount, Compensation, EcoAccountDocument, Co
from ema.models import Ema, EmaDocument from ema.models import Ema, EmaDocument
from intervention.models import Intervention, Revocation, RevocationDocument, InterventionDocument from intervention.models import Intervention, Revocation, RevocationDocument, InterventionDocument
from konova.contexts import BaseContext from konova.contexts import BaseContext
from konova.models import BaseObject from konova.models import BaseObject, Geometry
from konova.settings import DEFAULT_SRID
from konova.utils.message_templates import FORM_INVALID from konova.utils.message_templates import FORM_INVALID
from user.models import UserActionLogEntry, UserAction from user.models import UserActionLogEntry, UserAction
@ -75,7 +76,7 @@ class BaseForm(forms.Form):
def add_placeholder_for_field(self, field: str, val): def add_placeholder_for_field(self, field: str, val):
""" """
Adds a placeholder to a field after initialization Adds a placeholder to a field after initialization without the need to redefine the form widget
Args: Args:
field (str): Field name field (str): Field name
@ -184,16 +185,10 @@ class BaseModalForm(BaseForm, BSModalForm):
""" """
is_modal_form = True is_modal_form = True
render_submit = True render_submit = True
full_width_fields = False
template = "modal/modal_form.html" template = "modal/modal_form.html"
def __init__(self, full_width_fields: bool = True, *args, **kwargs): def __init__(self, *args, **kwargs):
self.full_width_fields = full_width_fields
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
if self.full_width_fields:
# Automatically add bootstrap w-100 class for maximum width of form fields in modals
for key, val in self.fields.items():
self.add_widget_html_class(key, "w-100")
def process_request(self, request: HttpRequest, msg_success: str = _("Object removed"), msg_error: str = FORM_INVALID, redirect_url: str = None): def process_request(self, request: HttpRequest, msg_success: str = _("Object removed"), msg_error: str = FORM_INVALID, redirect_url: str = None):
""" Generic processing of request """ Generic processing of request
@ -243,32 +238,65 @@ 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
""" """
geom = GeometryField( geom = MultiPolygonField(
srid=DEFAULT_SRID,
label=_("Geometry"),
help_text=_(""),
label_suffix="",
required=False, required=False,
disabled=True, disabled=False,
widget=OSMWidget( widget=OSMWidget(
attrs={ attrs={
"map_width": 600, "map_width": 600,
"map_height": 400, "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)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
# Initialize geometry # Initialize geometry
try: try:
geom = self.instance.geometry.geom geom = self.instance.geometry.geom
if geom is None: self.empty = geom.empty
raise AttributeError
except AttributeError: except AttributeError:
# catches if no geometry has been added, yet. Replace with empty placeholder polygon. # If no geometry exists for this form, we simply set the value to None and zoom to the maximum level
geom = Polygon.from_bbox([0, 0, 0, 0]) geom = None
# Zoom out to a very high level, so the user can see directly that there is no geometry for this entry self.empty = True
self.fields["geom"].widget.attrs["default_zoom"] = 1 self.fields["geom"].widget.attrs["default_zoom"] = 1
self.initialize_form_field("geom", geom) self.initialize_form_field("geom", geom)
self.area = geom.area if read_only:
self.fields["geom"].disabled = True
def save(self, action: UserActionLogEntry):
""" Saves the form's geometry
Creates a new geometry entry if none is set, yet
Args:
action ():
Returns:
"""
try:
geometry = self.instance.geometry
geometry.geom = self.cleaned_data.get("geom", MultiPolygon(srid=DEFAULT_SRID))
geometry.modified = action
geometry.save()
except (AttributeError) as e:
# No geometry or linked instance holding a geometry exist --> create a new one!
geometry = Geometry.objects.create(
geom=self.cleaned_data.get("geom", MultiPolygon(srid=DEFAULT_SRID)),
created=action,
)
return geometry
class RemoveModalForm(BaseModalForm): class RemoveModalForm(BaseModalForm):
@ -294,15 +322,7 @@ class RemoveModalForm(BaseModalForm):
def save(self): def save(self):
if isinstance(self.instance, BaseObject): if isinstance(self.instance, BaseObject):
with transaction.atomic(): self.instance.mark_as_deleted(self.user)
action = UserActionLogEntry.objects.create(
user=self.user,
timestamp=timezone.now(),
action=UserAction.DELETED,
)
self.instance.deleted = action
self.instance.log.add(action)
self.instance.save()
else: else:
# If the class does not provide restorable delete functionality, we must delete the entry finally # If the class does not provide restorable delete functionality, we must delete the entry finally
self.instance.delete() self.instance.delete()
@ -316,6 +336,11 @@ class NewDocumentForm(BaseModalForm):
label=_("Title"), label=_("Title"),
label_suffix=_(""), label_suffix=_(""),
max_length=500, max_length=500,
widget=forms.TextInput(
attrs={
"class": "form-control",
}
)
) )
creation_date = forms.DateField( creation_date = forms.DateField(
label=_("Created on"), label=_("Created on"),
@ -325,6 +350,7 @@ class NewDocumentForm(BaseModalForm):
attrs={ attrs={
"type": "date", "type": "date",
"data-provide": "datepicker", "data-provide": "datepicker",
"class": "form-control",
}, },
format="%d.%m.%Y" format="%d.%m.%Y"
) )
@ -335,7 +361,7 @@ class NewDocumentForm(BaseModalForm):
help_text=_("Must be smaller than 15 Mb"), help_text=_("Must be smaller than 15 Mb"),
widget=forms.FileInput( widget=forms.FileInput(
attrs={ attrs={
"class": "w-75" "class": "form-control-file",
} }
), ),
) )
@ -349,6 +375,7 @@ class NewDocumentForm(BaseModalForm):
attrs={ attrs={
"cols": 30, "cols": 30,
"rows": 5, "rows": 5,
"class": "form-control",
} }
) )
) )

@ -21,7 +21,7 @@ class Command(BaseCommand):
len_ids = len(identifiers) len_ids = len(identifiers)
while len_ids < max_iterations: while len_ids < max_iterations:
tmp_intervention = Intervention() tmp_intervention = Intervention()
_id = tmp_intervention._generate_new_identifier() _id = tmp_intervention.generate_new_identifier()
len_ids = len(identifiers) len_ids = len(identifiers)
if _id not in identifiers: if _id not in identifiers:
if len_ids % (max_iterations/5) == 0: if len_ids % (max_iterations/5) == 0:

@ -9,6 +9,8 @@ import os
import uuid import uuid
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.exceptions import ObjectDoesNotExist
from django.utils import timezone
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.contrib.gis.db.models import MultiPolygonField from django.contrib.gis.db.models import MultiPolygonField
@ -61,8 +63,20 @@ class BaseResource(UuidModel):
abstract = True abstract = True
def delete(self, using=None, keep_parents=False): def delete(self, using=None, keep_parents=False):
if self.created: """ Base deleting of a resource
Args:
using ():
keep_parents ():
Returns:
"""
try:
self.created.delete() self.created.delete()
except (ObjectDoesNotExist, AttributeError) as e:
# Object does not exist anymore - we can skip this
pass
super().delete() super().delete()
@ -81,14 +95,13 @@ class BaseObject(BaseResource):
class Meta: class Meta:
abstract = True abstract = True
def delete(self, *args, **kwargs): def mark_as_deleted(self, user: User):
""" Custom delete functionality """ Mark an entry as deleted
Does not delete from database but sets a timestamp for being deleted on and which user deleted the object Does not delete from database but sets a timestamp for being deleted on and which user deleted the object
Args: Args:
*args (): user (User): The performing user
**kwargs ():
Returns: Returns:
@ -97,13 +110,14 @@ class BaseObject(BaseResource):
# Nothing to do here # Nothing to do here
return return
_user = kwargs.get("user", None)
with transaction.atomic(): with transaction.atomic():
action = UserActionLogEntry.objects.create( action = UserActionLogEntry.objects.create(
user=_user, user=user,
action=UserAction.DELETED action=UserAction.DELETED,
timestamp=timezone.now()
) )
self.deleted = action self.deleted = action
self.log.add(action)
self.save() self.save()
def add_log_entry(self, action: UserAction, user: User, comment: str): def add_log_entry(self, action: UserAction, user: User, comment: str):
@ -139,7 +153,7 @@ class BaseObject(BaseResource):
else: else:
return User.objects.none() return User.objects.none()
def _generate_new_identifier(self) -> str: def generate_new_identifier(self) -> str:
""" Generates a new identifier for the intervention object """ Generates a new identifier for the intervention object
Returns: Returns:
@ -177,7 +191,9 @@ class BaseObject(BaseResource):
curr_year = str(_now.year) curr_year = str(_now.year)
rand_str = generate_random_string( rand_str = generate_random_string(
length=definitions[self.__class__]["length"], length=definitions[self.__class__]["length"],
only_numbers=True, use_numbers=True,
use_letters_lc=False,
use_letters_uc=True,
) )
_str = "{}{}-{}".format(curr_month, curr_year, rand_str) _str = "{}{}-{}".format(curr_month, curr_year, rand_str)
return definitions[self.__class__]["template"].format(_str) return definitions[self.__class__]["template"].format(_str)

@ -60,6 +60,14 @@ a {
color: var(--rlp-red); color: var(--rlp-red);
} }
.form-control:focus{
outline: none;
border-color: var(--rlp-red);
box-shadow: 0 0 3px var(--rlp-red);
-moz-box-shadow: 0 0 3px var(--rlp-red);
-webkit-box-shadow: 0 0 3px var(--rlp-red);
}
.body-content{ .body-content{
margin: 1rem 0rem 0 0rem; margin: 1rem 0rem 0 0rem;
} }
@ -133,6 +141,13 @@ a {
height: 8rem; height: 8rem;
} }
/**
Overwrites bootstrap .btn:focus box shadow color
*/
.btn:focus{
box-shadow: 0 0 5px .2rem var(--rlp-gray-light);
}
.btn-default{ .btn-default{
color: white; color: white;
background-color: var(--rlp-red); background-color: var(--rlp-red);
@ -171,13 +186,6 @@ a {
background-color: var(--rlp-gray-light); background-color: var(--rlp-gray-light);
} }
input:focus, textarea:focus, select:focus{
border-color: var(--rlp-red) !important;
box-shadow: 0 0 3px var(--rlp-red) !important;
-moz-box-shadow: 0 0 3px var(--rlp-red) !important;
-webkit-box-shadow: 0 0 3px var(--rlp-red) !important;
}
.check-star{ .check-star{
color: goldenrod; color: goldenrod;
} }
@ -216,3 +224,6 @@ No other approach worked to get the autocomplete fields to full width of parent
.select2-container{ .select2-container{
width: 100% !important; width: 100% !important;
} }
.select2-results__option--highlighted{
background-color: var(--rlp-red) !important;
}

@ -1,5 +0,0 @@
<form action="{{ form.action_url }}" method="post">
{% csrf_token %}
{{ form.as_p }}
</form>

@ -3,6 +3,6 @@
{% block body %} {% block body %}
<div class="column"> <div class="column">
{% include 'form/generic_table_form.html' %} {% include 'form/table/generic_table_form.html' %}
</div> </div>
{% endblock %} {% endblock %}

@ -0,0 +1,22 @@
{% load i18n fontawesome_5 %}
<div class="input-group w-100" title="{{ widget.value|stringformat:'s' }}">
<input id="gen-id-input" aria-describedby="gen-id-btn" type="{{ widget.type }}" name="{{ widget.name }}"{% if widget.value != None %} value="{{ widget.value|stringformat:'s' }}"{% endif %}{% include "django/forms/widgets/attrs.html" %}>
<div class="input-group-append" onclick="fetchNewIdentifier()">
<span id="gen-id-btn" class="btn btn-default" value="{% trans 'Generate new' %}" title="{% trans 'Generate new' %}">{% fa5_icon 'dice' %}</span>
</div>
</div>
<script>
function fetchNewIdentifier() {
fetch("{{ widget.attrs.url }}")
.then(function(response){
return response.json();
})
.then(function(data){
document.getElementById("gen-id-input").value = data["identifier"];
})
.catch(function(error){
console.log(error);
});
}
</script>

@ -19,7 +19,8 @@ from django.urls import path, include
from konova.autocompletes import OrganisationAutocomplete, NonOfficialOrganisationAutocomplete, EcoAccountAutocomplete, \ from konova.autocompletes import OrganisationAutocomplete, NonOfficialOrganisationAutocomplete, EcoAccountAutocomplete, \
InterventionAutocomplete, CompensationActionCodeAutocomplete, BiotopeCodeAutocomplete, LawCodeAutocomplete, \ InterventionAutocomplete, CompensationActionCodeAutocomplete, BiotopeCodeAutocomplete, LawCodeAutocomplete, \
RegistrationOfficeCodeAutocomplete, ConservationOfficeCodeAutocomplete RegistrationOfficeCodeAutocomplete, ConservationOfficeCodeAutocomplete, ProcessTypeCodeAutocomplete, \
CompensationFundingCodeAutocomplete
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, remove_deadline_view from konova.views import logout_view, home_view, remove_deadline_view
@ -46,9 +47,11 @@ urlpatterns = [
path("atcmplt/orgs/other", NonOfficialOrganisationAutocomplete.as_view(), name="other-orgs-autocomplete"), path("atcmplt/orgs/other", NonOfficialOrganisationAutocomplete.as_view(), name="other-orgs-autocomplete"),
path("atcmplt/eco-accounts", EcoAccountAutocomplete.as_view(), name="accounts-autocomplete"), path("atcmplt/eco-accounts", EcoAccountAutocomplete.as_view(), name="accounts-autocomplete"),
path("atcmplt/interventions", InterventionAutocomplete.as_view(), name="interventions-autocomplete"), path("atcmplt/interventions", InterventionAutocomplete.as_view(), name="interventions-autocomplete"),
path("atcmplt/codes/compensation-action", CompensationActionCodeAutocomplete.as_view(), name="codes-compensation-action-autocomplete"), path("atcmplt/codes/comp/action", CompensationActionCodeAutocomplete.as_view(), name="codes-compensation-action-autocomplete"),
path("atcmplt/codes/comp/funding", CompensationFundingCodeAutocomplete.as_view(), name="codes-compensation-funding-autocomplete"),
path("atcmplt/codes/biotope", BiotopeCodeAutocomplete.as_view(), name="codes-biotope-autocomplete"), path("atcmplt/codes/biotope", BiotopeCodeAutocomplete.as_view(), name="codes-biotope-autocomplete"),
path("atcmplt/codes/law", LawCodeAutocomplete.as_view(), name="codes-law-autocomplete"), path("atcmplt/codes/law", LawCodeAutocomplete.as_view(), name="codes-law-autocomplete"),
path("atcmplt/codes/prc-type", ProcessTypeCodeAutocomplete.as_view(), name="codes-process-type-autocomplete"),
path("atcmplt/codes/reg-off", RegistrationOfficeCodeAutocomplete.as_view(), name="codes-registration-office-autocomplete"), path("atcmplt/codes/reg-off", RegistrationOfficeCodeAutocomplete.as_view(), name="codes-registration-office-autocomplete"),
path("atcmplt/codes/cons-off", ConservationOfficeCodeAutocomplete.as_view(), name="codes-conservation-office-autocomplete"), path("atcmplt/codes/cons-off", ConservationOfficeCodeAutocomplete.as_view(), name="codes-conservation-office-autocomplete"),
] ]

@ -9,14 +9,18 @@ import random
import string import string
def generate_random_string(length: int, only_numbers: bool = False) -> str: def generate_random_string(length: int, use_numbers: bool = False, use_letters_lc: bool = False, use_letters_uc: bool = False) -> str:
""" """
Generates a random string of variable length Generates a random string of variable length
""" """
if only_numbers: elements = []
elements = string.digits if use_numbers:
else: elements.append(string.digits)
elements = string.ascii_letters if use_letters_lc:
elements.append(string.ascii_lowercase)
if use_letters_uc:
elements.append(string.ascii_uppercase)
elements = "".join(elements)
ret_val = "".join(random.choice(elements) for i in range(length)) ret_val = "".join(random.choice(elements) for i in range(length))
return ret_val return ret_val

@ -10,3 +10,6 @@ from django.utils.translation import gettext_lazy as _
FORM_INVALID = _("There was an error on this form.") FORM_INVALID = _("There was an error on this form.")
INTERVENTION_INVALID = _("There are errors in this intervention.") INTERVENTION_INVALID = _("There are errors in this intervention.")
IDENTIFIER_REPLACED = _("The identifier '{}' had to be changed to '{}' since another entry has been added in the meanwhile, which uses this identifier")
DATA_UNSHARED = _("This data is not shared with you")
MISSING_GROUP_PERMISSION = _("You need to be part of another user group.")

Binary file not shown.

File diff suppressed because it is too large Load Diff

@ -13,8 +13,8 @@ django-simple-sso==1.1.0
django-tables2==2.3.4 django-tables2==2.3.4
idna==2.10 idna==2.10
importlib-metadata==2.1.1 importlib-metadata==2.1.1
pkg-resources==0.0.0 itsdangerous
psycopg2==2.8.6 psycopg2-binary
pytz==2020.4 pytz==2020.4
requests==2.25.0 requests==2.25.0
six==1.15.0 six==1.15.0

@ -0,0 +1,65 @@
{% load i18n l10n fontawesome_5 %}
<form method="post" action="{{ form.action_url }}" {% for attr_key, attr_val in form.form_attrs.items %} {{attr_key}}="{{attr_val}}"{% endfor %}>
{% csrf_token %}
<h2>{{form.form_title}}</h2>
<div id="help" class="col">
<div class="row rlp-gd-outline p-2">
<div class="col-lg-1 rlp-r-inv">
<span class="d-flex justify-content-center align-items-center h-100">
{% fa5_icon 'question-circle' 'far' %}
</span>
</div>
<div class="col-lg-11">
<small>
{% blocktrans %}
First enter the most basic data. Of course you can change everything later.
All further data, like documents or further details, can be added in the detail view after saving
your new entry.
{% endblocktrans %}
<br>
{% trans 'Open the input topic with a simple click.' %}
</small>
</div>
</div>
</div>
<div class="mt-3">
<div class="card">
<div id="dataCardHeader" class="card-header cursor-pointer rlp-r" data-toggle="collapse" data-target="#dataCard" aria-expanded="true" aria-controls="dataCard">
<h5>
{% fa5_icon 'list' %}
{% trans 'General data' %}
</h5>
</div>
<div id="dataCard" class="collapse" aria-labelledby="dataCardHeader">
<div class="card-body">
{% include 'form/table/generic_table_form_body.html' %}
</div>
</div>
</div>
</div>
<div class="">
<div class="card">
<div id="geometryCardHeader" class="card-header cursor-pointer rlp-r" data-toggle="collapse" data-target="#geometryCard" aria-expanded="true" aria-controls="geometryCard">
<h5>
{% fa5_icon 'map-marked-alt' %}
{% trans 'Geometry' %}
</h5>
</div>
<div id="geometryCard" class="collapse show" aria-labelledby="geometryCardHeader">
<div class="card-body">
{% include 'map/geom_form.html' %}
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-6">
<a href="{{ form.cancel_redirect }}">
<button class="btn btn-default" type="button" title="{% trans 'Cancel' %}">{% trans 'Cancel' %}</button>
</a>
</div>
<div class="col-6 d-flex justify-content-end">
<button class="btn btn-default" type="submit" title="{% trans 'Save' %}">{% trans 'Save' %}</button>
</div>
</div>
</form>

@ -16,14 +16,14 @@
{% 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 %}>
{% csrf_token %} {% csrf_token %}
{% include 'form/generic_table_form_body.html' %} {% include 'form/table/generic_table_form_body.html' %}
<div class="row"> <div class="row">
<div class="col-md"> <div class="col-6">
<a href="{{ form.cancel_redirect }}"> <a href="{{ form.cancel_redirect }}">
<button class="btn btn-default" type="button" title="{% trans 'Cancel' %}">{% trans 'Cancel' %}</button> <button class="btn btn-default" type="button" title="{% trans 'Cancel' %}">{% trans 'Cancel' %}</button>
</a> </a>
</div> </div>
<div class="col-md d-flex justify-content-end"> <div class="col-6 d-flex justify-content-end">
<button class="btn btn-default" type="submit" title="{% trans 'Save' %}">{% trans 'Save' %}</button> <button class="btn btn-default" type="submit" title="{% trans 'Save' %}">{% trans 'Save' %}</button>
</div> </div>
</div> </div>

@ -6,6 +6,7 @@
<tr title="{{ field.help_text }}" class="{% if field.errors %}alert-danger{% endif %}"> <tr title="{{ field.help_text }}" class="{% if field.errors %}alert-danger{% endif %}">
<th scope="row" class="col-sm-3"> <th scope="row" class="col-sm-3">
<label for="id_{{ field.name }}">{{ field.label }}<span class="label-required">{% if field.field.required %}*{% endif %}</span></label> <label for="id_{{ field.name }}">{{ field.label }}<span class="label-required">{% if field.field.required %}*{% endif %}</span></label>
<br>
<small>{{ field.help_text }}</small> <small>{{ field.help_text }}</small>
</th> </th>
<td class="col-sm-9"> <td class="col-sm-9">

@ -4,9 +4,10 @@
Encapsules the rendering and initializing of a geometry view component, e.g. used in the detail views. Encapsules the rendering and initializing of a geometry view component, e.g. used in the detail views.
{% endcomment %} {% endcomment %}
{% if geom_form.empty %}
{% if geom_form.area == 0 %} <div class="w-100">
<div class="alert alert-info">{% trans 'No geometry added, yet.' %}</div> <div class="alert alert-info">{% trans 'No geometry added, yet.' %}</div>
</div>
{% endif %} {% endif %}
{{geom_form.media}} {{geom_form.media}}
{{geom_form.geom}} {{geom_form.geom}}

@ -18,7 +18,7 @@
<article> <article>
{{ form.form_caption }} {{ form.form_caption }}
</article> </article>
{% include 'form/generic_table_form_body.html' %} {% include 'form/table/generic_table_form_body.html' %}
</div> </div>
{% if form.render_submit %} {% if form.render_submit %}
<div class="modal-footer"> <div class="modal-footer">

@ -1,5 +1,5 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block body %} {% block body %}
{% include 'form/generic_table_form.html' %} {% include 'form/table/generic_table_form.html' %}
{% endblock %} {% endblock %}
Loading…
Cancel
Save