Merge pull request 'Refactoring' (#43) from Refactoring into master

Reviewed-on: SGD-Nord/konova#43
pull/47/head
Michel Peltriaux 3 years ago
commit 3db2a156bb

@ -16,9 +16,9 @@ from codelist.models import KonovaCode
from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID
from compensation.models import Compensation, EcoAccount
from intervention.inputs import GenerateInput
from intervention.models import Intervention, ResponsibilityData, LegalData
from intervention.models import Intervention, Responsibility, Legal
from konova.forms import BaseForm, SimpleGeomForm
from user.models import UserActionLogEntry, UserAction
from user.models import UserActionLogEntry
class AbstractCompensationForm(BaseForm):
@ -210,10 +210,7 @@ class NewCompensationForm(AbstractCompensationForm, CEFCompensationFormMixin, Co
comment = self.cleaned_data.get("comment", None)
# Create log entry
action = UserActionLogEntry.objects.create(
user=user,
action=UserAction.CREATED,
)
action = UserActionLogEntry.get_created_action(user)
# Process the geometry form
geometry = geom_form.save(action)
@ -270,10 +267,7 @@ class EditCompensationForm(NewCompensationForm):
comment = self.cleaned_data.get("comment", None)
# Create log entry
action = UserActionLogEntry.objects.create(
user=user,
action=UserAction.EDITED,
)
action = UserActionLogEntry.get_edited_action(user)
# Process the geometry form
geometry = geom_form.save(action)
@ -364,20 +358,17 @@ class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMix
comment = self.cleaned_data.get("comment", None)
# Create log entry
action = UserActionLogEntry.objects.create(
user=user,
action=UserAction.CREATED,
)
action = UserActionLogEntry.get_created_action(user)
# Process the geometry form
geometry = geom_form.save(action)
responsible = ResponsibilityData.objects.create(
responsible = Responsibility.objects.create(
handler=handler,
conservation_file_number=conservation_file_number,
conservation_office=conservation_office,
)
legal = LegalData.objects.create(
legal = Legal.objects.create(
registration_date=registration_date
)
@ -444,10 +435,8 @@ class EditEcoAccountForm(NewEcoAccountForm):
comment = self.cleaned_data.get("comment", None)
# Create log entry
action = UserActionLogEntry.objects.create(
user=user,
action=UserAction.EDITED,
)
action = UserActionLogEntry.get_edited_action(user)
# Process the geometry form
geometry = geom_form.save(action)

@ -9,19 +9,17 @@ from bootstrap_modal_forms.utils import is_ajax
from dal import autocomplete
from django import forms
from django.contrib import messages
from django.db import transaction
from django.http import HttpRequest, HttpResponseRedirect
from django.shortcuts import render
from django.utils.translation import pgettext_lazy as _con, gettext_lazy as _
from codelist.models import KonovaCode
from codelist.settings import CODELIST_BIOTOPES_ID, CODELIST_COMPENSATION_ACTION_ID
from compensation.models import Payment, CompensationState, UnitChoices, CompensationAction
from compensation.models import CompensationDocument, EcoAccountDocument
from konova.contexts import BaseContext
from konova.forms import BaseModalForm
from konova.models import DeadlineType, Deadline
from konova.forms import BaseModalForm, NewDocumentForm
from konova.models import DeadlineType
from konova.utils.message_templates import FORM_INVALID
from user.models import UserActionLogEntry, UserAction
class NewPaymentForm(BaseModalForm):
@ -99,26 +97,7 @@ class NewPaymentForm(BaseModalForm):
return super_valid
def save(self):
with transaction.atomic():
created_action = UserActionLogEntry.objects.create(
user=self.user,
action=UserAction.CREATED,
)
edited_action = UserActionLogEntry.objects.create(
user=self.user,
action=UserAction.EDITED,
comment=_("Added payment"),
)
pay = Payment.objects.create(
created=created_action,
amount=self.cleaned_data.get("amount", -1),
due_on=self.cleaned_data.get("due", None),
comment=self.cleaned_data.get("comment", None),
intervention=self.intervention,
)
self.intervention.log.add(edited_action)
self.intervention.modified = edited_action
self.intervention.save()
pay = self.instance.add_payment(self)
return pay
@ -167,24 +146,7 @@ class NewStateModalForm(BaseModalForm):
self.form_caption = _("Insert data for the new state")
def save(self, is_before_state: bool = False):
with transaction.atomic():
user_action = UserActionLogEntry.objects.create(
user=self.user,
action=UserAction.EDITED,
comment=_("Added state")
)
self.instance.log.add(user_action)
self.instance.modified = user_action
self.instance.save()
state = CompensationState.objects.create(
biotope_type=self.cleaned_data["biotope_type"],
surface=self.cleaned_data["surface"],
)
if is_before_state:
self.instance.before_states.add(state)
else:
self.instance.after_states.add(state)
state = self.instance.add_state(self, is_before_state)
return state
def process_request(self, request: HttpRequest, msg_success: str = _("Object removed"), msg_error: str = FORM_INVALID, redirect_url: str = None):
@ -287,26 +249,7 @@ class NewDeadlineModalForm(BaseModalForm):
self.form_caption = _("Insert data for the new deadline")
def save(self):
with transaction.atomic():
created_action = UserActionLogEntry.objects.create(
user=self.user,
action=UserAction.CREATED
)
deadline = Deadline.objects.create(
type=self.cleaned_data["type"],
date=self.cleaned_data["date"],
comment=self.cleaned_data["comment"],
created=created_action,
)
edited_action = UserActionLogEntry.objects.create(
user=self.user,
action=UserAction.EDITED,
comment=_("Added deadline")
)
self.instance.modified = edited_action
self.instance.save()
self.instance.log.add(edited_action)
self.instance.deadlines.add(deadline)
deadline = self.instance.add_new_deadline(self)
return deadline
@ -318,6 +261,7 @@ class NewActionModalForm(BaseModalForm):
(not in the process logic in Konova, but in the real world).
"""
from compensation.models import UnitChoices
action_type = forms.ModelChoiceField(
label=_("Action Type"),
label_suffix="",
@ -381,25 +325,13 @@ class NewActionModalForm(BaseModalForm):
self.form_caption = _("Insert data for the new action")
def save(self):
with transaction.atomic():
user_action = UserActionLogEntry.objects.create(
user=self.user,
action=UserAction.CREATED,
)
comp_action = CompensationAction.objects.create(
action_type=self.cleaned_data["action_type"],
amount=self.cleaned_data["amount"],
unit=self.cleaned_data["unit"],
comment=self.cleaned_data["comment"],
created=user_action,
)
edited_action = UserActionLogEntry.objects.create(
user=self.user,
action=UserAction.EDITED,
comment=_("Added action"),
)
self.instance.modified = edited_action
self.instance.save()
self.instance.log.add(edited_action)
self.instance.actions.add(comp_action)
return comp_action
action = self.instance.add_new_action(self)
return action
class NewCompensationDocumentForm(NewDocumentForm):
document_model = CompensationDocument
class NewEcoAccountDocumentForm(NewDocumentForm):
document_model = EcoAccountDocument

@ -1,568 +0,0 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 17.11.20
"""
import shutil
from django.contrib.auth.models import User
from django.contrib.gis.db import models
from django.core.exceptions import ValidationError
from django.core.validators import MinValueValidator
from django.db.models import Sum, QuerySet
from django.utils.translation import gettext_lazy as _
from codelist.models import KonovaCode
from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_BIOTOPES_ID
from compensation.managers import CompensationStateManager, EcoAccountDeductionManager, CompensationActionManager, \
EcoAccountManager, CompensationManager
from compensation.utils.quality import CompensationQualityChecker, EcoAccountQualityChecker
from intervention.models import Intervention, ResponsibilityData, LegalData
from konova.models import BaseObject, BaseResource, Geometry, UuidModel, AbstractDocument, \
generate_document_file_upload_path, RecordableObject, ShareableObject
from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE
from user.models import UserActionLogEntry
class Payment(BaseResource):
"""
Holds data on a payment for an intervention (alternative to a classic compensation)
"""
amount = models.FloatField(validators=[MinValueValidator(limit_value=0.00)])
due_on = models.DateField(null=True, blank=True)
comment = models.TextField(
null=True,
blank=True,
help_text="Refers to german money transfer 'Verwendungszweck'",
)
intervention = models.ForeignKey(
Intervention,
null=True,
blank=True,
on_delete=models.CASCADE,
related_name='payments'
)
class Meta:
ordering = [
"-amount",
]
class CompensationState(UuidModel):
"""
Compensations must define the state of an area before and after the compensation.
"""
biotope_type = models.ForeignKey(
KonovaCode,
on_delete=models.SET_NULL,
null=True,
blank=True,
limit_choices_to={
"code_lists__in": [CODELIST_BIOTOPES_ID],
"is_selectable": True,
"is_archived": False,
}
)
surface = models.FloatField()
objects = CompensationStateManager()
def __str__(self):
return "{} | {}".format(self.biotope_type, self.surface)
class UnitChoices(models.TextChoices):
"""
Predefines units for selection
"""
cm = "cm", _("cm")
m = "m", _("m")
km = "km", _("km")
qm = "qm", _("")
ha = "ha", _("ha")
st = "pcs", _("Pieces") # pieces
class CompensationAction(BaseResource):
"""
Compensations include actions like planting trees, refreshing rivers and so on.
"""
action_type = models.ForeignKey(
KonovaCode,
on_delete=models.SET_NULL,
null=True,
blank=True,
limit_choices_to={
"code_lists__in": [CODELIST_COMPENSATION_ACTION_ID],
"is_selectable": True,
"is_archived": False,
}
)
amount = models.FloatField()
unit = models.CharField(max_length=100, null=True, blank=True, choices=UnitChoices.choices)
comment = models.TextField(blank=True, null=True, help_text="Additional comment")
objects = CompensationActionManager()
def __str__(self):
return "{} | {} {}".format(self.action_type, self.amount, self.unit)
@property
def unit_humanize(self):
""" Returns humanized version of enum
Used for template rendering
Returns:
"""
choices = UnitChoices.choices
for choice in choices:
if choice[0] == self.unit:
return choice[1]
return None
class AbstractCompensation(BaseObject):
"""
Abstract compensation model which holds basic attributes, shared by subclasses like the regular Compensation,
EMA or EcoAccount.
"""
responsible = models.OneToOneField(
ResponsibilityData,
on_delete=models.SET_NULL,
null=True,
blank=True,
help_text="Holds data on responsible organizations ('Zulassungsbehörde', 'Eintragungsstelle') and handler",
)
before_states = models.ManyToManyField(CompensationState, blank=True, related_name='+', help_text="Refers to 'Ausgangszustand Biotop'")
after_states = models.ManyToManyField(CompensationState, blank=True, related_name='+', help_text="Refers to 'Zielzustand Biotop'")
actions = models.ManyToManyField(CompensationAction, blank=True, help_text="Refers to 'Maßnahmen'")
deadlines = models.ManyToManyField("konova.Deadline", blank=True, related_name="+")
geometry = models.ForeignKey(Geometry, null=True, blank=True, on_delete=models.SET_NULL)
class Meta:
abstract = True
def get_surface_after_states(self) -> float:
""" Calculates the compensation's/account's surface
Returns:
sum_surface (float)
"""
return self._calc_surface(self.after_states.all())
def get_surface_before_states(self) -> float:
""" Calculates the compensation's/account's surface
Returns:
sum_surface (float)
"""
return self._calc_surface(self.before_states.all())
def _calc_surface(self, qs: QuerySet):
""" Calculates the surface sum of a given queryset
Args:
qs (QuerySet): The queryset containing CompensationState entries
Returns:
"""
return qs.aggregate(Sum("surface"))["surface__sum"] or 0
def quality_check(self) -> CompensationQualityChecker:
""" Performs data quality check
Returns:
checker (CompensationQualityChecker): Holds validity data and error messages
"""
checker = CompensationQualityChecker(self)
checker.run_check()
return checker
class CEFMixin(models.Model):
""" Provides CEF flag as Mixin
"""
is_cef = models.BooleanField(
blank=True,
null=True,
default=False,
help_text="Flag if compensation is a 'CEF-Maßnahme'"
)
class Meta:
abstract = True
class CoherenceMixin(models.Model):
""" Provides coherence keeping flag as Mixin
"""
is_coherence_keeping = models.BooleanField(
blank=True,
null=True,
default=False,
help_text="Flag if compensation is a 'Kohärenzsicherung'"
)
class Meta:
abstract = True
class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin):
"""
Regular compensation, linked to an intervention
"""
intervention = models.ForeignKey(
Intervention,
on_delete=models.CASCADE,
null=True,
blank=True,
related_name='compensations'
)
objects = CompensationManager()
def __str__(self):
return "{}".format(self.identifier)
def save(self, *args, **kwargs):
if self.identifier is None or len(self.identifier) == 0:
# Create new identifier is none was given
self.identifier = self.generate_new_identifier()
# Before saving, make sure a given identifier has not been taken already in the meanwhile
while Compensation.objects.filter(identifier=self.identifier).exclude(id=self.id).exists():
self.identifier = self.generate_new_identifier()
super().save(*args, **kwargs)
def is_shared_with(self, user: User):
""" Access check
Checks whether a given user has access to this object
Args:
user (User): The user to be checked
Returns:
"""
# Compensations inherit their shared state from the interventions
return self.intervention.is_shared_with(user)
def get_LANIS_link(self) -> str:
""" Generates a link for LANIS depending on the geometry
Returns:
"""
try:
geom = self.geometry.geom.transform(DEFAULT_SRID_RLP, clone=True)
x = geom.centroid.x
y = geom.centroid.y
zoom_lvl = 16
except AttributeError:
# If no geometry has been added, yet.
x = 1
y = 1
zoom_lvl = 6
return LANIS_LINK_TEMPLATE.format(
zoom_lvl,
x,
y,
)
def get_documents(self) -> QuerySet:
""" Getter for all documents of a compensation
Returns:
docs (QuerySet): The queryset of all documents
"""
docs = CompensationDocument.objects.filter(
instance=self
)
return docs
class CompensationDocument(AbstractDocument):
"""
Specializes document upload for revocations with certain path
"""
instance = models.ForeignKey(
Compensation,
on_delete=models.CASCADE,
related_name="documents",
)
file = models.FileField(
upload_to=generate_document_file_upload_path,
max_length=1000,
)
def delete(self, *args, **kwargs):
"""
Custom delete functionality for CompensationDocuments.
Removes the folder from the file system if there are no further documents for this entry.
Args:
*args ():
**kwargs ():
Returns:
"""
comp_docs = self.instance.get_documents()
folder_path = None
if comp_docs.count() == 1:
# The only file left for this compensation is the one which is currently processed and will be deleted
# Make sure that the compensation folder itself is deleted as well, not only the file
# Therefore take the folder path from the file path
folder_path = self.file.path.split("/")[:-1]
folder_path = "/".join(folder_path)
# Remove the file itself
super().delete(*args, **kwargs)
# If a folder path has been set, we need to delete the whole folder!
if folder_path is not None:
try:
shutil.rmtree(folder_path)
except FileNotFoundError:
# Folder seems to be missing already...
pass
class EcoAccount(AbstractCompensation, ShareableObject, RecordableObject):
"""
An eco account is a kind of 'prepaid' compensation. It can be compared to an account that already has been filled
with some kind of currency. From this account one is able to deduct currency for current projects.
"""
deductable_surface = models.FloatField(
blank=True,
null=True,
help_text="Amount of deductable surface - can be lower than the total surface due to deduction limitations",
default=0,
)
legal = models.OneToOneField(
LegalData,
on_delete=models.SET_NULL,
null=True,
blank=True,
help_text="Holds data on legal dates or law"
)
objects = EcoAccountManager()
def __str__(self):
return "{}".format(self.identifier)
def clean(self):
# Deductable surface can not be larger than added states after surface
after_state_sum = self.get_state_after_surface_sum()
if self.deductable_surface > after_state_sum:
raise ValidationError(_("Deductable surface can not be larger than existing surfaces in after states"))
# Deductable surface can not be lower than amount of already deducted surfaces
# User needs to contact deducting user in case of further problems
deducted_sum = self.get_deductions_surface()
if self.deductable_surface < deducted_sum:
raise ValidationError(
_("Deductable surface can not be smaller than the sum of already existing deductions. Please contact the responsible users for the deductions!")
)
def save(self, *args, **kwargs):
if self.identifier is None or len(self.identifier) == 0:
# Create new identifier if none was given
self.identifier = self.generate_new_identifier()
# Before saving, make sure the given identifier is not used, yet
while EcoAccount.objects.filter(identifier=self.identifier).exclude(id=self.id).exists():
self.identifier = self.generate_new_identifier()
super().save(*args, **kwargs)
@property
def deductions_surface_sum(self) -> float:
""" Shortcut for get_deductions_surface.
Can be used in templates
Returns:
sum_surface (float)
"""
return self.get_deductions_surface()
def get_deductions_surface(self) -> float:
""" Calculates the account's deductions surface sum
Returns:
sum_surface (float)
"""
return self.deductions.all().aggregate(Sum("surface"))["surface__sum"] or 0
def get_state_after_surface_sum(self) -> float:
""" Calculates the account's after state surface sum
Returns:
sum_surface (float)
"""
return self.after_states.all().aggregate(Sum("surface"))["surface__sum"] or 0
def get_available_rest(self) -> (float, float):
""" Calculates available rest surface of the eco account
Args:
Returns:
ret_val_total (float): Total amount
ret_val_relative (float): Amount as percentage (0-100)
"""
deductions = self.deductions.filter(
intervention__deleted=None,
)
deductions_surfaces = deductions.aggregate(Sum("surface"))["surface__sum"] or 0
available_surfaces = self.deductable_surface or deductions_surfaces ## no division by zero
ret_val_total = available_surfaces - deductions_surfaces
if available_surfaces > 0:
ret_val_relative = int((ret_val_total / available_surfaces) * 100)
else:
ret_val_relative = 0
return ret_val_total, ret_val_relative
def get_LANIS_link(self) -> str:
""" Generates a link for LANIS depending on the geometry
Returns:
"""
try:
geom = self.geometry.geom.transform(DEFAULT_SRID_RLP, clone=True)
x = geom.centroid.x
y = geom.centroid.y
zoom_lvl = 16
except AttributeError:
# If no geometry has been added, yet.
x = 1
y = 1
zoom_lvl = 6
return LANIS_LINK_TEMPLATE.format(
zoom_lvl,
x,
y,
)
def quality_check(self) -> EcoAccountQualityChecker:
""" Quality check
Returns:
ret_msgs (EcoAccountQualityChecker): Holds validity and error messages
"""
checker = EcoAccountQualityChecker(self)
checker.run_check()
return checker
def get_documents(self) -> QuerySet:
""" Getter for all documents of an EcoAccount
Returns:
docs (QuerySet): The queryset of all documents
"""
docs = EcoAccountDocument.objects.filter(
instance=self
)
return docs
class EcoAccountDocument(AbstractDocument):
"""
Specializes document upload for revocations with certain path
"""
instance = models.ForeignKey(
EcoAccount,
on_delete=models.CASCADE,
related_name="documents",
)
file = models.FileField(
upload_to=generate_document_file_upload_path,
max_length=1000,
)
def delete(self, *args, **kwargs):
"""
Custom delete functionality for EcoAccountDocuments.
Removes the folder from the file system if there are no further documents for this entry.
Args:
*args ():
**kwargs ():
Returns:
"""
acc_docs = self.instance.get_documents()
folder_path = None
if acc_docs.count() == 1:
# The only file left for this eco account is the one which is currently processed and will be deleted
# Make sure that the compensation folder itself is deleted as well, not only the file
# Therefore take the folder path from the file path
folder_path = self.file.path.split("/")[:-1]
folder_path = "/".join(folder_path)
# Remove the file itself
super().delete(*args, **kwargs)
# If a folder path has been set, we need to delete the whole folder!
if folder_path is not None:
try:
shutil.rmtree(folder_path)
except FileNotFoundError:
# Folder seems to be missing already...
pass
class EcoAccountDeduction(BaseResource):
"""
A deduction object for eco accounts
"""
account = models.ForeignKey(
EcoAccount,
on_delete=models.SET_NULL,
null=True,
blank=True,
help_text="Deducted from",
related_name="deductions",
)
surface = models.FloatField(
null=True,
blank=True,
help_text="Amount deducted (m²)",
validators=[
MinValueValidator(limit_value=0.00),
]
)
intervention = models.ForeignKey(
Intervention,
on_delete=models.CASCADE,
null=True,
blank=True,
help_text="Deducted for",
related_name="deductions",
)
objects = EcoAccountDeductionManager()
def __str__(self):
return "{} of {}".format(self.surface, self.account)

@ -0,0 +1,12 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 16.11.21
"""
from .action import *
from .state import *
from .compensation import *
from .eco_account import *
from .payment import *

@ -0,0 +1,66 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 16.11.21
"""
from django.db import models
from django.utils.translation import gettext_lazy as _
from codelist.models import KonovaCode
from codelist.settings import CODELIST_COMPENSATION_ACTION_ID
from compensation.managers import CompensationActionManager
from konova.models import BaseResource
class UnitChoices(models.TextChoices):
"""
Predefines units for selection
"""
cm = "cm", _("cm")
m = "m", _("m")
km = "km", _("km")
qm = "qm", _("")
ha = "ha", _("ha")
st = "pcs", _("Pieces") # pieces
class CompensationAction(BaseResource):
"""
Compensations include actions like planting trees, refreshing rivers and so on.
"""
action_type = models.ForeignKey(
KonovaCode,
on_delete=models.SET_NULL,
null=True,
blank=True,
limit_choices_to={
"code_lists__in": [CODELIST_COMPENSATION_ACTION_ID],
"is_selectable": True,
"is_archived": False,
}
)
amount = models.FloatField()
unit = models.CharField(max_length=100, null=True, blank=True, choices=UnitChoices.choices)
comment = models.TextField(blank=True, null=True, help_text="Additional comment")
objects = CompensationActionManager()
def __str__(self):
return "{} | {} {}".format(self.action_type, self.amount, self.unit)
@property
def unit_humanize(self):
""" Returns humanized version of enum
Used for template rendering
Returns:
"""
choices = UnitChoices.choices
for choice in choices:
if choice[0] == self.unit:
return choice[1]
return None

@ -0,0 +1,319 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 16.11.21
"""
import shutil
from django.contrib.auth.models import User
from django.db import models, transaction
from django.db.models import QuerySet, Sum
from django.utils.translation import gettext_lazy as _
from compensation.managers import CompensationManager
from compensation.models import CompensationState, CompensationAction
from compensation.utils.quality import CompensationQualityChecker
from konova.models import BaseObject, AbstractDocument, Geometry, Deadline, generate_document_file_upload_path
from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE
from user.models import UserActionLogEntry
class AbstractCompensation(BaseObject):
"""
Abstract compensation model which holds basic attributes, shared by subclasses like the regular Compensation,
EMA or EcoAccount.
"""
responsible = models.OneToOneField(
"intervention.Responsibility",
on_delete=models.SET_NULL,
null=True,
blank=True,
help_text="Holds data on responsible organizations ('Zulassungsbehörde', 'Eintragungsstelle') and handler",
)
before_states = models.ManyToManyField(CompensationState, blank=True, related_name='+', help_text="Refers to 'Ausgangszustand Biotop'")
after_states = models.ManyToManyField(CompensationState, blank=True, related_name='+', help_text="Refers to 'Zielzustand Biotop'")
actions = models.ManyToManyField(CompensationAction, blank=True, help_text="Refers to 'Maßnahmen'")
deadlines = models.ManyToManyField("konova.Deadline", blank=True, related_name="+")
geometry = models.ForeignKey(Geometry, null=True, blank=True, on_delete=models.SET_NULL)
class Meta:
abstract = True
def add_new_deadline(self, form) -> Deadline:
""" Adds a new deadline to the abstract compensation
Args:
form (NewDeadlineModalForm): The form holding all relevant data
Returns:
"""
form_data = form.cleaned_data
user = form.user
with transaction.atomic():
created_action = UserActionLogEntry.get_created_action(user)
edited_action = UserActionLogEntry.get_edited_action(user, _("Added deadline"))
deadline = Deadline.objects.create(
type=form_data["type"],
date=form_data["date"],
comment=form_data["comment"],
created=created_action,
)
self.modified = edited_action
self.save()
self.log.add(edited_action)
self.deadlines.add(deadline)
return deadline
def add_new_action(self, form) -> CompensationAction:
""" Adds a new action to the compensation
Args:
form (NewActionModalForm): The form holding all relevant data
Returns:
"""
form_data = form.cleaned_data
user = form.user
with transaction.atomic():
user_action = UserActionLogEntry.get_created_action(user)
edited_action = UserActionLogEntry.get_edited_action(user, _("Added action"))
comp_action = CompensationAction.objects.create(
action_type=form_data["action_type"],
amount=form_data["amount"],
unit=form_data["unit"],
comment=form_data["comment"],
created=user_action,
)
self.modified = edited_action
self.save()
self.log.add(edited_action)
self.actions.add(comp_action)
return comp_action
def add_state(self, form, is_before_state: bool) -> CompensationState:
""" Adds a new compensation state to the compensation
Args:
form (NewStateModalForm): The form, holding all relevant data
is_before_state (bool): Whether this is a new before_state or after_state
Returns:
"""
form_data = form.cleaned_data
user = form.user
with transaction.atomic():
user_action = UserActionLogEntry.get_edited_action(user, _("Added state"))
self.log.add(user_action)
self.modified = user_action
self.save()
state = CompensationState.objects.create(
biotope_type=form_data["biotope_type"],
surface=form_data["surface"],
)
if is_before_state:
self.before_states.add(state)
else:
self.after_states.add(state)
return state
def get_surface_after_states(self) -> float:
""" Calculates the compensation's/account's surface
Returns:
sum_surface (float)
"""
return self._calc_surface(self.after_states.all())
def get_surface_before_states(self) -> float:
""" Calculates the compensation's/account's surface
Returns:
sum_surface (float)
"""
return self._calc_surface(self.before_states.all())
def _calc_surface(self, qs: QuerySet):
""" Calculates the surface sum of a given queryset
Args:
qs (QuerySet): The queryset containing CompensationState entries
Returns:
"""
return qs.aggregate(Sum("surface"))["surface__sum"] or 0
def quality_check(self) -> CompensationQualityChecker:
""" Performs data quality check
Returns:
checker (CompensationQualityChecker): Holds validity data and error messages
"""
checker = CompensationQualityChecker(self)
checker.run_check()
return checker
class CEFMixin(models.Model):
""" Provides CEF flag as Mixin
"""
is_cef = models.BooleanField(
blank=True,
null=True,
default=False,
help_text="Flag if compensation is a 'CEF-Maßnahme'"
)
class Meta:
abstract = True
class CoherenceMixin(models.Model):
""" Provides coherence keeping flag as Mixin
"""
is_coherence_keeping = models.BooleanField(
blank=True,
null=True,
default=False,
help_text="Flag if compensation is a 'Kohärenzsicherung'"
)
class Meta:
abstract = True
class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin):
"""
Regular compensation, linked to an intervention
"""
intervention = models.ForeignKey(
"intervention.Intervention",
on_delete=models.CASCADE,
null=True,
blank=True,
related_name='compensations'
)
objects = CompensationManager()
def __str__(self):
return "{}".format(self.identifier)
def save(self, *args, **kwargs):
if self.identifier is None or len(self.identifier) == 0:
# Create new identifier is none was given
self.identifier = self.generate_new_identifier()
# Before saving, make sure a given identifier has not been taken already in the meanwhile
while Compensation.objects.filter(identifier=self.identifier).exclude(id=self.id).exists():
self.identifier = self.generate_new_identifier()
super().save(*args, **kwargs)
def is_shared_with(self, user: User):
""" Access check
Checks whether a given user has access to this object
Args:
user (User): The user to be checked
Returns:
"""
# Compensations inherit their shared state from the interventions
return self.intervention.is_shared_with(user)
def get_LANIS_link(self) -> str:
""" Generates a link for LANIS depending on the geometry
Returns:
"""
try:
geom = self.geometry.geom.transform(DEFAULT_SRID_RLP, clone=True)
x = geom.centroid.x
y = geom.centroid.y
zoom_lvl = 16
except AttributeError:
# If no geometry has been added, yet.
x = 1
y = 1
zoom_lvl = 6
return LANIS_LINK_TEMPLATE.format(
zoom_lvl,
x,
y,
)
def get_documents(self) -> QuerySet:
""" Getter for all documents of a compensation
Returns:
docs (QuerySet): The queryset of all documents
"""
docs = CompensationDocument.objects.filter(
instance=self
)
return docs
class CompensationDocument(AbstractDocument):
"""
Specializes document upload for revocations with certain path
"""
instance = models.ForeignKey(
Compensation,
on_delete=models.CASCADE,
related_name="documents",
)
file = models.FileField(
upload_to=generate_document_file_upload_path,
max_length=1000,
)
def delete(self, *args, **kwargs):
"""
Custom delete functionality for CompensationDocuments.
Removes the folder from the file system if there are no further documents for this entry.
Args:
*args ():
**kwargs ():
Returns:
"""
comp_docs = self.instance.get_documents()
folder_path = None
if comp_docs.count() == 1:
# The only file left for this compensation is the one which is currently processed and will be deleted
# Make sure that the compensation folder itself is deleted as well, not only the file
# Therefore take the folder path from the file path
folder_path = self.file.path.split("/")[:-1]
folder_path = "/".join(folder_path)
# Remove the file itself
super().delete(*args, **kwargs)
# If a folder path has been set, we need to delete the whole folder!
if folder_path is not None:
try:
shutil.rmtree(folder_path)
except FileNotFoundError:
# Folder seems to be missing already...
pass

@ -0,0 +1,276 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 16.11.21
"""
import shutil
from django.core.exceptions import ValidationError
from django.core.validators import MinValueValidator
from django.db import models, transaction
from django.db.models import Sum, QuerySet
from django.utils.translation import gettext_lazy as _
from compensation.managers import EcoAccountManager, EcoAccountDeductionManager
from compensation.models.compensation import AbstractCompensation
from compensation.utils.quality import EcoAccountQualityChecker
from konova.models import ShareableObjectMixin, RecordableObjectMixin, AbstractDocument, BaseResource, \
generate_document_file_upload_path
from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE
from user.models import UserActionLogEntry
class EcoAccount(AbstractCompensation, ShareableObjectMixin, RecordableObjectMixin):
"""
An eco account is a kind of 'prepaid' compensation. It can be compared to an account that already has been filled
with some kind of currency. From this account one is able to deduct currency for current projects.
"""
deductable_surface = models.FloatField(
blank=True,
null=True,
help_text="Amount of deductable surface - can be lower than the total surface due to deduction limitations",
default=0,
)
legal = models.OneToOneField(
"intervention.Legal",
on_delete=models.SET_NULL,
null=True,
blank=True,
help_text="Holds data on legal dates or law"
)
objects = EcoAccountManager()
def __str__(self):
return "{}".format(self.identifier)
def clean(self):
# Deductable surface can not be larger than added states after surface
after_state_sum = self.get_state_after_surface_sum()
if self.deductable_surface > after_state_sum:
raise ValidationError(_("Deductable surface can not be larger than existing surfaces in after states"))
# Deductable surface can not be lower than amount of already deducted surfaces
# User needs to contact deducting user in case of further problems
deducted_sum = self.get_deductions_surface()
if self.deductable_surface < deducted_sum:
raise ValidationError(
_("Deductable surface can not be smaller than the sum of already existing deductions. Please contact the responsible users for the deductions!")
)
def save(self, *args, **kwargs):
if self.identifier is None or len(self.identifier) == 0:
# Create new identifier if none was given
self.identifier = self.generate_new_identifier()
# Before saving, make sure the given identifier is not used, yet
while EcoAccount.objects.filter(identifier=self.identifier).exclude(id=self.id).exists():
self.identifier = self.generate_new_identifier()
super().save(*args, **kwargs)
@property
def deductions_surface_sum(self) -> float:
""" Shortcut for get_deductions_surface.
Can be used in templates
Returns:
sum_surface (float)
"""
return self.get_deductions_surface()
def get_deductions_surface(self) -> float:
""" Calculates the account's deductions surface sum
Returns:
sum_surface (float)
"""
return self.deductions.all().aggregate(Sum("surface"))["surface__sum"] or 0
def get_state_after_surface_sum(self) -> float:
""" Calculates the account's after state surface sum
Returns:
sum_surface (float)
"""
return self.after_states.all().aggregate(Sum("surface"))["surface__sum"] or 0
def get_available_rest(self) -> (float, float):
""" Calculates available rest surface of the eco account
Args:
Returns:
ret_val_total (float): Total amount
ret_val_relative (float): Amount as percentage (0-100)
"""
deductions = self.deductions.filter(
intervention__deleted=None,
)
deductions_surfaces = deductions.aggregate(Sum("surface"))["surface__sum"] or 0
available_surfaces = self.deductable_surface or deductions_surfaces ## no division by zero
ret_val_total = available_surfaces - deductions_surfaces
if available_surfaces > 0:
ret_val_relative = int((ret_val_total / available_surfaces) * 100)
else:
ret_val_relative = 0
return ret_val_total, ret_val_relative
def get_LANIS_link(self) -> str:
""" Generates a link for LANIS depending on the geometry
Returns:
"""
try:
geom = self.geometry.geom.transform(DEFAULT_SRID_RLP, clone=True)
x = geom.centroid.x
y = geom.centroid.y
zoom_lvl = 16
except AttributeError:
# If no geometry has been added, yet.
x = 1
y = 1
zoom_lvl = 6
return LANIS_LINK_TEMPLATE.format(
zoom_lvl,
x,
y,
)
def quality_check(self) -> EcoAccountQualityChecker:
""" Quality check
Returns:
ret_msgs (EcoAccountQualityChecker): Holds validity and error messages
"""
checker = EcoAccountQualityChecker(self)
checker.run_check()
return checker
def get_documents(self) -> QuerySet:
""" Getter for all documents of an EcoAccount
Returns:
docs (QuerySet): The queryset of all documents
"""
docs = EcoAccountDocument.objects.filter(
instance=self
)
return docs
def add_deduction(self, form):
""" Adds a new deduction to the intervention
Args:
form (NewDeductionModalForm): The form holding the data
Returns:
"""
form_data = form.cleaned_data
user = form.user
with transaction.atomic():
# Create log entry
user_action_create = UserActionLogEntry.get_created_action(user)
user_action_edit = UserActionLogEntry.get_edited_action(user)
self.log.add(user_action_edit)
self.modified = user_action_edit
self.save()
deduction = EcoAccountDeduction.objects.create(
intervention=form_data["intervention"],
account=self,
surface=form_data["surface"],
created=user_action_create,
)
return deduction
class EcoAccountDocument(AbstractDocument):
"""
Specializes document upload for revocations with certain path
"""
instance = models.ForeignKey(
EcoAccount,
on_delete=models.CASCADE,
related_name="documents",
)
file = models.FileField(
upload_to=generate_document_file_upload_path,
max_length=1000,
)
def delete(self, *args, **kwargs):
"""
Custom delete functionality for EcoAccountDocuments.
Removes the folder from the file system if there are no further documents for this entry.
Args:
*args ():
**kwargs ():
Returns:
"""
acc_docs = self.instance.get_documents()
folder_path = None
if acc_docs.count() == 1:
# The only file left for this eco account is the one which is currently processed and will be deleted
# Make sure that the compensation folder itself is deleted as well, not only the file
# Therefore take the folder path from the file path
folder_path = self.file.path.split("/")[:-1]
folder_path = "/".join(folder_path)
# Remove the file itself
super().delete(*args, **kwargs)
# If a folder path has been set, we need to delete the whole folder!
if folder_path is not None:
try:
shutil.rmtree(folder_path)
except FileNotFoundError:
# Folder seems to be missing already...
pass
class EcoAccountDeduction(BaseResource):
"""
A deduction object for eco accounts
"""
account = models.ForeignKey(
EcoAccount,
on_delete=models.SET_NULL,
null=True,
blank=True,
help_text="Deducted from",
related_name="deductions",
)
surface = models.FloatField(
null=True,
blank=True,
help_text="Amount deducted (m²)",
validators=[
MinValueValidator(limit_value=0.00),
]
)
intervention = models.ForeignKey(
"intervention.Intervention",
on_delete=models.CASCADE,
null=True,
blank=True,
help_text="Deducted for",
related_name="deductions",
)
objects = EcoAccountDeductionManager()
def __str__(self):
return "{} of {}".format(self.surface, self.account)

@ -0,0 +1,37 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 16.11.21
"""
from django.core.validators import MinValueValidator
from django.db import models
from intervention.models import Intervention
from konova.models import BaseResource
class Payment(BaseResource):
"""
Holds data on a payment for an intervention (alternative to a classic compensation)
"""
amount = models.FloatField(validators=[MinValueValidator(limit_value=0.00)])
due_on = models.DateField(null=True, blank=True)
comment = models.TextField(
null=True,
blank=True,
help_text="Refers to german money transfer 'Verwendungszweck'",
)
intervention = models.ForeignKey(
Intervention,
null=True,
blank=True,
on_delete=models.CASCADE,
related_name='payments'
)
class Meta:
ordering = [
"-amount",
]

@ -0,0 +1,36 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 16.11.21
"""
from django.db import models
from codelist.models import KonovaCode
from codelist.settings import CODELIST_BIOTOPES_ID
from compensation.managers import CompensationStateManager
from konova.models import UuidModel
class CompensationState(UuidModel):
"""
Compensations must define the state of an area before and after the compensation.
"""
biotope_type = models.ForeignKey(
KonovaCode,
on_delete=models.SET_NULL,
null=True,
blank=True,
limit_choices_to={
"code_lists__in": [CODELIST_BIOTOPES_ID],
"is_selectable": True,
"is_archived": False,
}
)
surface = models.FloatField()
objects = CompensationStateManager()
def __str__(self):
return "{} | {}".format(self.biotope_type, self.surface)

@ -0,0 +1,8 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 16.11.21
"""
from .urls import *

@ -6,7 +6,7 @@ Created on: 24.08.21
"""
from django.urls import path
from compensation.views.compensation_views import *
from compensation.views.compensation import *
urlpatterns = [
# Main compensation

@ -6,7 +6,7 @@ Created on: 24.08.21
"""
from django.urls import path
from compensation.views.eco_account_views import *
from compensation.views.eco_account import *
urlpatterns = [
path("", index_view, name="acc-index"),

@ -6,7 +6,7 @@ Created on: 24.08.21
"""
from django.urls import path
from compensation.views.payment_views import *
from compensation.views.payment import *
urlpatterns = [
path('<intervention_id>/new', new_payment_view, name='pay-new'),

@ -9,7 +9,7 @@ from django.urls import path, include
app_name = "compensation"
urlpatterns = [
path("", include("compensation.comp_urls")),
path("acc/", include("compensation.account_urls")),
path("pay/", include("compensation.payment_urls")),
path("", include("compensation.urls.compensation")),
path("acc/", include("compensation.urls.eco_account")),
path("pay/", include("compensation.urls.payment")),
]

@ -61,7 +61,7 @@ class EcoAccountQualityChecker(CompensationQualityChecker):
super().run_check()
def _check_legal_data(self):
""" Checks the data quality for LegalData
""" Checks the data quality for Legal
Returns:

@ -0,0 +1,10 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 16.11.21
"""
from .compensation import *
from .eco_account import *
from .payment import *

@ -5,13 +5,14 @@ from django.shortcuts import render
from django.utils.translation import gettext_lazy as _
from compensation.forms.forms import NewCompensationForm, EditCompensationForm
from compensation.forms.modalForms import NewStateModalForm, NewDeadlineModalForm, NewActionModalForm
from compensation.forms.modalForms import NewStateModalForm, NewDeadlineModalForm, NewActionModalForm, \
NewCompensationDocumentForm
from compensation.models import Compensation, CompensationState, CompensationAction, CompensationDocument
from compensation.tables import CompensationTable
from intervention.models import Intervention
from konova.contexts import BaseContext
from konova.decorators import *
from konova.forms import RemoveModalForm, SimpleGeomForm, NewDocumentForm
from konova.forms import RemoveModalForm, SimpleGeomForm
from konova.utils.documents import get_document, remove_document
from konova.utils.generators import generate_qr_code
from konova.utils.message_templates import FORM_INVALID, IDENTIFIER_REPLACED, DATA_UNSHARED_EXPLANATION
@ -258,7 +259,7 @@ def new_document_view(request: HttpRequest, id: str):
"""
comp = get_object_or_404(Compensation, id=id)
form = NewDocumentForm(request.POST or None, request.FILES or None, instance=comp, user=request.user)
form = NewCompensationDocumentForm(request.POST or None, request.FILES or None, instance=comp, user=request.user)
return form.process_request(
request,
msg_success=_("Document added")

@ -15,10 +15,11 @@ from django.http import HttpRequest, Http404, JsonResponse
from django.shortcuts import render, get_object_or_404, redirect
from compensation.forms.forms import NewEcoAccountForm, EditEcoAccountForm
from compensation.forms.modalForms import NewStateModalForm, NewActionModalForm, NewDeadlineModalForm
from compensation.forms.modalForms import NewStateModalForm, NewActionModalForm, NewDeadlineModalForm, \
NewEcoAccountDocumentForm
from compensation.models import EcoAccount, EcoAccountDocument, CompensationState, CompensationAction
from compensation.tables import EcoAccountTable
from intervention.forms.modalForms import NewDeductionModalForm, ShareInterventionModalForm
from intervention.forms.modalForms import NewDeductionModalForm, ShareModalForm
from konova.contexts import BaseContext
from konova.decorators import any_group_check, default_group_required, conservation_office_group_required, \
shared_access_required
@ -453,7 +454,7 @@ def new_document_view(request: HttpRequest, id: str):
"""
acc = get_object_or_404(EcoAccount, id=id)
form = NewDocumentForm(request.POST or None, request.FILES or None, instance=acc, user=request.user)
form = NewEcoAccountDocumentForm(request.POST or None, request.FILES or None, instance=acc, user=request.user)
return form.process_request(
request,
msg_success=_("Document added")
@ -641,7 +642,7 @@ def create_share_view(request: HttpRequest, id: str):
"""
obj = get_object_or_404(EcoAccount, id=id)
form = ShareInterventionModalForm(request.POST or None, instance=obj, request=request)
form = ShareModalForm(request.POST or None, instance=obj, request=request)
return form.process_request(
request,
msg_success=_("Share settings updated")

@ -13,10 +13,10 @@ 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
from ema.models import Ema, EmaDocument
from intervention.models import Responsibility
from konova.forms import SimpleGeomForm, NewDocumentForm
from user.models import UserActionLogEntry
class NewEmaForm(AbstractCompensationForm, CompensationResponsibleFormMixin):
@ -59,14 +59,11 @@ class NewEmaForm(AbstractCompensationForm, CompensationResponsibleFormMixin):
comment = self.cleaned_data.get("comment", None)
# Create log entry
action = UserActionLogEntry.objects.create(
user=user,
action=UserAction.CREATED,
)
action = UserActionLogEntry.get_created_action(user)
# Process the geometry form
geometry = geom_form.save(action)
responsible = ResponsibilityData.objects.create(
responsible = Responsibility.objects.create(
handler=handler,
conservation_file_number=conservation_file_number,
conservation_office=conservation_office,
@ -130,10 +127,7 @@ class EditEmaForm(NewEmaForm):
comment = self.cleaned_data.get("comment", None)
# Create log entry
action = UserActionLogEntry.objects.create(
user=user,
action=UserAction.EDITED,
)
action = UserActionLogEntry.get_edited_action(user)
# Process the geometry form
geometry = geom_form.save(action)
@ -154,3 +148,7 @@ class EditEmaForm(NewEmaForm):
# Add the log entry to the main objects log list
self.instance.log.add(action)
return self.instance
class NewEmaDocumentForm(NewDocumentForm):
document_model = EmaDocument

@ -0,0 +1,9 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 15.11.21
"""
from .ema import *

@ -1,3 +1,10 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 15.11.21
"""
import shutil
from django.db import models
@ -6,11 +13,11 @@ from django.db.models import QuerySet
from compensation.models import AbstractCompensation
from ema.managers import EmaManager
from ema.utils.quality import EmaQualityChecker
from konova.models import AbstractDocument, generate_document_file_upload_path, RecordableObject, ShareableObject
from konova.models import AbstractDocument, generate_document_file_upload_path, RecordableObjectMixin, ShareableObjectMixin
from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE
class Ema(AbstractCompensation, ShareableObject, RecordableObject):
class Ema(AbstractCompensation, ShareableObjectMixin, RecordableObjectMixin):
"""
EMA = Ersatzzahlungsmaßnahme
(compensation actions from payments)

@ -11,10 +11,10 @@ from django.test.client import Client
from compensation.tests.test_views import CompensationViewTestCase
from ema.models import Ema
from intervention.models import ResponsibilityData
from intervention.models import Responsibility
from konova.models import Geometry
from konova.settings import DEFAULT_GROUP, ETS_GROUP
from user.models import UserActionLogEntry, UserAction
from user.models import UserActionLogEntry
class EmaViewTestCase(CompensationViewTestCase):
@ -61,12 +61,9 @@ class EmaViewTestCase(CompensationViewTestCase):
def create_dummy_data(cls):
# Create dummy data
# Create log entry
action = UserActionLogEntry.objects.create(
user=cls.superuser,
action=UserAction.CREATED,
)
action = UserActionLogEntry.get_created_action(cls.superuser)
# Create responsible data object
responsibility_data = ResponsibilityData.objects.create()
responsibility_data = Responsibility.objects.create()
geometry = Geometry.objects.create()
cls.ema = Ema.objects.create(
identifier="TEST",

@ -8,13 +8,13 @@ from django.utils.translation import gettext_lazy as _
from compensation.forms.modalForms import NewStateModalForm, NewActionModalForm, NewDeadlineModalForm
from compensation.models import CompensationAction, CompensationState
from ema.forms import NewEmaForm, EditEmaForm
from ema.forms import NewEmaForm, EditEmaForm, NewEmaDocumentForm
from ema.tables import EmaTable
from intervention.forms.modalForms import ShareInterventionModalForm
from intervention.forms.modalForms import ShareModalForm
from konova.contexts import BaseContext
from konova.decorators import conservation_office_group_required, shared_access_required
from ema.models import Ema, EmaDocument
from konova.forms import RemoveModalForm, NewDocumentForm, SimpleGeomForm, RecordModalForm
from konova.forms import RemoveModalForm, SimpleGeomForm, RecordModalForm
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
from konova.utils.documents import get_document, remove_document
from konova.utils.generators import generate_qr_code
@ -343,7 +343,7 @@ def document_new_view(request: HttpRequest, id: str):
"""
ema = get_object_or_404(Ema, id=id)
form = NewDocumentForm(request.POST or None, request.FILES or None, instance=ema, user=request.user)
form = NewEmaDocumentForm(request.POST or None, request.FILES or None, instance=ema, user=request.user)
return form.process_request(
request,
msg_success=_("Document added")
@ -546,7 +546,7 @@ def create_share_view(request: HttpRequest, id: str):
"""
obj = get_object_or_404(Ema, id=id)
form = ShareInterventionModalForm(request.POST or None, instance=obj, request=request)
form = ShareModalForm(request.POST or None, instance=obj, request=request)
return form.process_request(
request,
msg_success=_("Share settings updated")

@ -1,6 +1,6 @@
from django.contrib import admin
from intervention.models import Intervention, ResponsibilityData, LegalData, Revocation, InterventionDocument
from intervention.models import Intervention, Responsibility, Legal, Revocation, InterventionDocument
from konova.admin import AbstractDocumentAdmin
@ -46,7 +46,7 @@ class RevocationAdmin(admin.ModelAdmin):
admin.site.register(Intervention, InterventionAdmin)
admin.site.register(ResponsibilityData, ResponsibilityAdmin)
admin.site.register(LegalData, LegalAdmin)
admin.site.register(Responsibility, ResponsibilityAdmin)
admin.site.register(Legal, LegalAdmin)
admin.site.register(Revocation, RevocationAdmin)
admin.site.register(InterventionDocument, InterventionDocumentAdmin)

@ -10,16 +10,15 @@ 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 intervention.models import Intervention, Legal, Responsibility
from konova.forms import BaseForm, SimpleGeomForm
from user.models import UserActionLogEntry, UserAction
from user.models import UserActionLogEntry
class NewInterventionForm(BaseForm):
@ -214,13 +213,10 @@ class NewInterventionForm(BaseForm):
comment = self.cleaned_data.get("comment", None)
# Create log entry
action = UserActionLogEntry.objects.create(
user=user,
action=UserAction.CREATED,
)
action = UserActionLogEntry.get_created_action(user)
# Create legal data object (without M2M laws first)
legal_data = LegalData.objects.create(
legal_data = Legal.objects.create(
registration_date=registration_date,
binding_date=binding_date,
process_type=_type,
@ -229,7 +225,7 @@ class NewInterventionForm(BaseForm):
legal_data.laws.set(laws)
# Create responsible data object
responsibility_data = ResponsibilityData.objects.create(
responsibility_data = Responsibility.objects.create(
registration_office=registration_office,
conservation_office=conservation_office,
registration_file_number=registration_file_number,
@ -337,11 +333,7 @@ class EditInterventionForm(NewInterventionForm):
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,
)
user_action = UserActionLogEntry.get_edited_action(user)
geometry = geom_form.save(user_action)
self.instance.geometry = geometry
@ -356,7 +348,9 @@ class EditInterventionForm(NewInterventionForm):
self.instance.save()
# Uncheck and unrecord intervention due to changed data
if self.instance.checked:
self.instance.set_unchecked()
if self.instance.recorded:
self.instance.set_unrecorded(user)
return self.instance

@ -12,17 +12,15 @@ from django import forms
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from compensation.models import EcoAccount, EcoAccountDeduction
from compensation.models import EcoAccount
from intervention.inputs import TextToClipboardInput
from intervention.models import Revocation, RevocationDocument, Intervention
from konova.forms import BaseModalForm
from intervention.models import Intervention, InterventionDocument
from konova.forms import BaseModalForm, NewDocumentForm
from konova.utils.general import format_german_float
from konova.utils.messenger import Messenger
from konova.utils.user_checks import is_default_group_only
from user.models import UserActionLogEntry, UserAction
class ShareInterventionModalForm(BaseModalForm):
class ShareModalForm(BaseModalForm):
url = forms.CharField(
label=_("Share link"),
label_suffix="",
@ -118,13 +116,7 @@ class ShareInterventionModalForm(BaseModalForm):
)
def save(self):
still_accessing_users = self.cleaned_data["users"]
new_accessing_users = list(self.cleaned_data["user_select"].values_list("id", flat=True))
accessing_users = still_accessing_users + new_accessing_users
users = User.objects.filter(
id__in=accessing_users
)
self.instance.share_with_list(users)
self.instance.update_sharing_user(self)
class NewRevocationModalForm(BaseModalForm):
@ -176,33 +168,7 @@ class NewRevocationModalForm(BaseModalForm):
}
def save(self):
with transaction.atomic():
created_action = UserActionLogEntry.objects.create(
user=self.user,
action=UserAction.CREATED
)
edited_action = UserActionLogEntry.objects.create(
user=self.user,
action=UserAction.EDITED
)
revocation = Revocation.objects.create(
date=self.cleaned_data["date"],
legal=self.instance.legal,
comment=self.cleaned_data["comment"],
created=created_action,
)
self.instance.modified = edited_action
self.instance.save()
self.instance.log.add(edited_action)
if self.cleaned_data["file"]:
RevocationDocument.objects.create(
title="revocation_of_{}".format(self.instance.identifier),
date_of_creation=self.cleaned_data["date"],
comment=self.cleaned_data["comment"],
file=self.cleaned_data["file"],
instance=revocation
)
revocation = self.instance.add_revocation(self)
return revocation
@ -261,16 +227,6 @@ class CheckModalForm(BaseModalForm):
with transaction.atomic():
self.instance.toggle_checked(self.user)
# Send message to the SSO server
messenger = Messenger(
self.instance.users.all(),
type="INFO",
)
messenger.send_object_checked(
self.instance.identifier,
self.user,
)
class NewDeductionModalForm(BaseModalForm):
""" Form for creating new deduction
@ -326,14 +282,12 @@ class NewDeductionModalForm(BaseModalForm):
super().__init__(*args, **kwargs)
self.form_title = _("New Deduction")
self.form_caption = _("Enter the information for a new deduction from a chosen eco-account")
self.is_intervention_initially = False
# Check for Intervention or EcoAccount
if isinstance(self.instance, Intervention):
# Form has been called with a given intervention
self.initialize_form_field("intervention", self.instance)
self.disable_form_field("intervention")
self.is_intervention_initially = True
elif isinstance(self.instance, EcoAccount):
# Form has been called with a given account --> make it initial in the form and read-only
self.initialize_form_field("account", self.instance)
@ -350,10 +304,7 @@ class NewDeductionModalForm(BaseModalForm):
is_valid (bool)
"""
super_result = super().is_valid()
if self.is_intervention_initially:
acc = self.cleaned_data["account"]
else:
acc = self.instance
if not acc.recorded:
self.add_error(
@ -367,7 +318,7 @@ class NewDeductionModalForm(BaseModalForm):
sum_surface_deductions = acc.get_deductions_surface()
rest_surface = deductable_surface - sum_surface_deductions
form_surface = float(self.cleaned_data["surface"])
is_valid_surface = form_surface < rest_surface
is_valid_surface = form_surface <= rest_surface
if not is_valid_surface:
self.add_error(
"surface",
@ -380,33 +331,9 @@ class NewDeductionModalForm(BaseModalForm):
return is_valid_surface and super_result
def save(self):
with transaction.atomic():
# Create log entry
user_action_edit = UserActionLogEntry.objects.create(
user=self.user,
action=UserAction.EDITED
)
user_action_create = UserActionLogEntry.objects.create(
user=self.user,
action=UserAction.CREATED
)
self.instance.log.add(user_action_edit)
self.instance.modified = user_action_edit
self.instance.save()
# Create deductions depending on Intervention or EcoAccount as the initial instance
if self.is_intervention_initially:
deduction = EcoAccountDeduction.objects.create(
intervention=self.instance,
account=self.cleaned_data["account"],
surface=self.cleaned_data["surface"],
created=user_action_create,
)
else:
deduction = EcoAccountDeduction.objects.create(
intervention=self.cleaned_data["intervention"],
account=self.instance,
surface=self.cleaned_data["surface"],
created=user_action_create,
)
deduction = self.instance.add_deduction(self)
return deduction
class NewInterventionDocumentForm(NewDocumentForm):
document_model = InterventionDocument

@ -0,0 +1,12 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 15.11.21
"""
from .intervention import *
from .legal import *
from .revocation import *
from .responsibility import *

@ -2,187 +2,41 @@
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 17.11.20
Created on: 15.11.21
"""
import shutil
from django.contrib.auth.models import User
from django.contrib.gis.db import models
from django.db import models, transaction
from django.db.models import QuerySet
from django.utils.translation import gettext_lazy as _
from codelist.models import KonovaCode
from codelist.settings import CODELIST_REGISTRATION_OFFICE_ID, CODELIST_CONSERVATION_OFFICE_ID, CODELIST_LAW_ID, \
CODELIST_PROCESS_TYPE_ID
from compensation.models import EcoAccountDeduction
from intervention.managers import InterventionManager
from intervention.models.legal import Legal
from intervention.models.responsibility import Responsibility
from intervention.models.revocation import RevocationDocument, Revocation
from intervention.utils.quality import InterventionQualityChecker
from konova.models import BaseObject, Geometry, UuidModel, BaseResource, AbstractDocument, \
generate_document_file_upload_path, RecordableObject, CheckableObject, ShareableObject
from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE, LANIS_ZOOM_LUT
from konova.models import generate_document_file_upload_path, AbstractDocument, Geometry, BaseObject, ShareableObjectMixin, \
RecordableObjectMixin, CheckableObjectMixin
from konova.settings import LANIS_LINK_TEMPLATE, LANIS_ZOOM_LUT, DEFAULT_SRID_RLP
from user.models import UserActionLogEntry
class ResponsibilityData(UuidModel):
"""
Holds intervention data about responsible organizations and their file numbers for this case
"""
registration_office = models.ForeignKey(
KonovaCode,
on_delete=models.SET_NULL,
null=True,
related_name="+",
blank=True,
limit_choices_to={
"code_lists__in": [CODELIST_REGISTRATION_OFFICE_ID],
"is_selectable": True,
"is_archived": False,
}
)
registration_file_number = models.CharField(max_length=1000, blank=True, null=True)
conservation_office = models.ForeignKey(
KonovaCode,
on_delete=models.SET_NULL,
null=True,
related_name="+",
blank=True,
limit_choices_to={
"code_lists__in": [CODELIST_CONSERVATION_OFFICE_ID],
"is_selectable": True,
"is_archived": False,
}
)
conservation_file_number = models.CharField(max_length=1000, blank=True, null=True)
handler = models.CharField(max_length=500, null=True, blank=True, help_text="Refers to 'Eingriffsverursacher' or 'Maßnahmenträger'")
def __str__(self):
return "ZB: {} | ETS: {} | Handler: {}".format(
self.registration_office,
self.conservation_office,
self.handler
)
class Revocation(BaseResource):
"""
Holds revocation data e.g. for intervention objects
"""
date = models.DateField(null=True, blank=True, help_text="Revocation from")
legal = models.ForeignKey("LegalData", null=False, blank=False, on_delete=models.CASCADE, help_text="Refers to 'Widerspruch am'", related_name="revocations")
comment = models.TextField(null=True, blank=True)
def delete(self, *args, **kwargs):
# Make sure related objects are being removed as well
if self.document:
self.document.delete(*args, **kwargs)
super().delete()
class RevocationDocument(AbstractDocument):
"""
Specializes document upload for revocations with certain path
"""
instance = models.OneToOneField(
Revocation,
on_delete=models.CASCADE,
related_name="document",
)
file = models.FileField(
upload_to=generate_document_file_upload_path,
max_length=1000,
)
@property
def intervention(self):
"""
Shortcut for opening the related intervention
Returns:
intervention (Intervention)
"""
return self.instance.legal.intervention
def delete(self, *args, **kwargs):
"""
Custom delete functionality for RevocationDocuments.
Removes the folder from the file system if there are no further documents for this entry.
Args:
*args ():
**kwargs ():
Returns:
"""
revoc_docs, other_intervention_docs = self.intervention.get_documents()
# Remove the file itself
super().delete(*args, **kwargs)
# Always remove 'revocation' folder if the one revocation we just processed is the only one left
folder_path = self.file.path.split("/")
if revoc_docs.count() == 0:
try:
shutil.rmtree("/".join(folder_path[:-1]))
except FileNotFoundError:
# Revocation subfolder seems to be missing already
pass
if other_intervention_docs.count() == 0:
# If there are no further documents for the intervention, we can simply remove the whole folder as well!
try:
shutil.rmtree("/".join(folder_path[:-2]))
except FileNotFoundError:
# Folder seems to be missing already
pass
class LegalData(UuidModel):
"""
Holds intervention legal data such as important dates, laws or responsible handler
"""
# Refers to "zugelassen am"
registration_date = models.DateField(null=True, blank=True, help_text="Refers to 'Zugelassen am'")
# Refers to "Bestandskraft am"
binding_date = models.DateField(null=True, blank=True, help_text="Refers to 'Bestandskraft am'")
process_type = models.ForeignKey(
KonovaCode,
on_delete=models.SET_NULL,
null=True,
related_name="+",
blank=True,
limit_choices_to={
"code_lists__in": [CODELIST_PROCESS_TYPE_ID],
"is_selectable": True,
"is_archived": False,
}
)
laws = models.ManyToManyField(
KonovaCode,
blank=True,
limit_choices_to={
"code_lists__in": [CODELIST_LAW_ID],
"is_selectable": True,
"is_archived": False,
}
)
class Intervention(BaseObject, ShareableObject, RecordableObject, CheckableObject):
class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, CheckableObjectMixin):
"""
Interventions are e.g. construction sites where nature used to be.
"""
responsible = models.OneToOneField(
ResponsibilityData,
Responsibility,
on_delete=models.SET_NULL,
null=True,
blank=True,
help_text="Holds data on responsible organizations ('Zulassungsbehörde', 'Eintragungsstelle')"
)
legal = models.OneToOneField(
LegalData,
Legal,
on_delete=models.SET_NULL,
null=True,
blank=True,
@ -322,6 +176,98 @@ class Intervention(BaseObject, ShareableObject, RecordableObject, CheckableObjec
for comp in comps:
comp.log.add(log_entry)
def add_payment(self, form):
""" Adds a new payment to the intervention
Args:
form (NewPaymentForm): The form holding the data
Returns:
"""
from compensation.models import Payment
form_data = form.cleaned_data
user = form.user
with transaction.atomic():
created_action = UserActionLogEntry.get_created_action(user)
edited_action = UserActionLogEntry.get_edited_action(user, _("Added payment"))
pay = Payment.objects.create(
created=created_action,
amount=form_data.get("amount", -1),
due_on=form_data.get("due", None),
comment=form_data.get("comment", None),
intervention=self,
)
self.log.add(edited_action)
self.modified = edited_action
self.save()
return pay
def add_revocation(self, form):
""" Adds a new revocation to the intervention
Args:
form (NewRevocationModalForm): The form holding the data
Returns:
"""
form_data = form.cleaned_data
user = form.user
with transaction.atomic():
created_action = UserActionLogEntry.get_created_action(user)
edited_action = UserActionLogEntry.get_edited_action(user)
revocation = Revocation.objects.create(
date=form_data["date"],
legal=self.legal,
comment=form_data["comment"],
created=created_action,
)
self.modified = edited_action
self.save()
self.log.add(edited_action)
if form_data["file"]:
RevocationDocument.objects.create(
title="revocation_of_{}".format(self.identifier),
date_of_creation=form_data["date"],
comment=form_data["comment"],
file=form_data["file"],
instance=revocation
)
return revocation
def add_deduction(self, form):
""" Adds a new deduction to the intervention
Args:
form (NewDeductionModalForm): The form holding the data
Returns:
"""
form_data = form.cleaned_data
user = form.user
with transaction.atomic():
# Create log entry
user_action_edit = UserActionLogEntry.get_edited_action(user)
user_action_create = UserActionLogEntry.get_created_action(user)
self.log.add(user_action_edit)
self.modified = user_action_edit
self.save()
deduction = EcoAccountDeduction.objects.create(
intervention=self,
account=form_data["account"],
surface=form_data["surface"],
created=user_action_create,
)
return deduction
class InterventionDocument(AbstractDocument):
"""
@ -366,7 +312,6 @@ class InterventionDocument(AbstractDocument):
if folder_path is not None:
try:
shutil.rmtree(folder_path)
pass
except FileNotFoundError:
# Folder seems to be missing already...
pass

@ -0,0 +1,46 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 15.11.21
"""
from django.db import models
from codelist.models import KonovaCode
from codelist.settings import CODELIST_LAW_ID, CODELIST_PROCESS_TYPE_ID
from konova.models import UuidModel
class Legal(UuidModel):
"""
Holds intervention legal data such as important dates, laws or responsible handler
"""
# Refers to "zugelassen am"
registration_date = models.DateField(null=True, blank=True, help_text="Refers to 'Zugelassen am'")
# Refers to "Bestandskraft am"
binding_date = models.DateField(null=True, blank=True, help_text="Refers to 'Bestandskraft am'")
process_type = models.ForeignKey(
KonovaCode,
on_delete=models.SET_NULL,
null=True,
related_name="+",
blank=True,
limit_choices_to={
"code_lists__in": [CODELIST_PROCESS_TYPE_ID],
"is_selectable": True,
"is_archived": False,
}
)
laws = models.ManyToManyField(
KonovaCode,
blank=True,
limit_choices_to={
"code_lists__in": [CODELIST_LAW_ID],
"is_selectable": True,
"is_archived": False,
}
)

@ -0,0 +1,53 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 15.11.21
"""
from django.db import models
from codelist.models import KonovaCode
from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID, CODELIST_REGISTRATION_OFFICE_ID
from konova.models import UuidModel
class Responsibility(UuidModel):
"""
Holds intervention data about responsible organizations and their file numbers for this case
"""
registration_office = models.ForeignKey(
KonovaCode,
on_delete=models.SET_NULL,
null=True,
related_name="+",
blank=True,
limit_choices_to={
"code_lists__in": [CODELIST_REGISTRATION_OFFICE_ID],
"is_selectable": True,
"is_archived": False,
}
)
registration_file_number = models.CharField(max_length=1000, blank=True, null=True)
conservation_office = models.ForeignKey(
KonovaCode,
on_delete=models.SET_NULL,
null=True,
related_name="+",
blank=True,
limit_choices_to={
"code_lists__in": [CODELIST_CONSERVATION_OFFICE_ID],
"is_selectable": True,
"is_archived": False,
}
)
conservation_file_number = models.CharField(max_length=1000, blank=True, null=True)
handler = models.CharField(max_length=500, null=True, blank=True, help_text="Refers to 'Eingriffsverursacher' or 'Maßnahmenträger'")
def __str__(self):
return "ZB: {} | ETS: {} | Handler: {}".format(
self.registration_office,
self.conservation_office,
self.handler
)

@ -0,0 +1,87 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 15.11.21
"""
import shutil
from django.contrib.gis.db import models
from konova.models import BaseResource, AbstractDocument, generate_document_file_upload_path
class Revocation(BaseResource):
"""
Holds revocation data e.g. for intervention objects
"""
date = models.DateField(null=True, blank=True, help_text="Revocation from")
legal = models.ForeignKey("Legal", null=False, blank=False, on_delete=models.CASCADE, help_text="Refers to 'Widerspruch am'", related_name="revocations")
comment = models.TextField(null=True, blank=True)
def delete(self, *args, **kwargs):
# Make sure related objects are being removed as well
if self.document:
self.document.delete(*args, **kwargs)
super().delete()
class RevocationDocument(AbstractDocument):
"""
Specializes document upload for revocations with certain path
"""
instance = models.OneToOneField(
Revocation,
on_delete=models.CASCADE,
related_name="document",
)
file = models.FileField(
upload_to=generate_document_file_upload_path,
max_length=1000,
)
@property
def intervention(self):
"""
Shortcut for opening the related intervention
Returns:
intervention (Intervention)
"""
return self.instance.legal.intervention
def delete(self, *args, **kwargs):
"""
Custom delete functionality for RevocationDocuments.
Removes the folder from the file system if there are no further documents for this entry.
Args:
*args ():
**kwargs ():
Returns:
"""
revoc_docs, other_intervention_docs = self.intervention.get_documents()
# Remove the file itself
super().delete(*args, **kwargs)
# Always remove 'revocation' folder if the one revocation we just processed is the only one left
folder_path = self.file.path.split("/")
if revoc_docs.count() == 0:
try:
shutil.rmtree("/".join(folder_path[:-1]))
except FileNotFoundError:
# Revocation subfolder seems to be missing already
pass
if other_intervention_docs.count() == 0:
# If there are no further documents for the intervention, we can simply remove the whole folder as well!
try:
shutil.rmtree("/".join(folder_path[:-2]))
except FileNotFoundError:
# Folder seems to be missing already
pass

@ -356,10 +356,7 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase):
# Prepare the account for a working situation (enough deductable surface, recorded and shared)
self.eco_account.deductable_surface = 10000.00
if self.eco_account.recorded is None:
rec_action = UserActionLogEntry.objects.create(
user=self.superuser,
action=UserAction.RECORDED
)
rec_action = UserActionLogEntry.get_recorded_action(self.superuser)
self.eco_account.recorded = rec_action
self.eco_account.share_with_list([self.superuser])
self.eco_account.save()

@ -24,7 +24,7 @@ class InterventionQualityChecker(AbstractQualityChecker):
self.valid = len(self.messages) == 0
def _check_responsible_data(self):
""" Checks data quality of related ResponsibilityData
""" Checks data quality of related Responsibility
Args:
self.messages (dict): Holds error messages
@ -55,7 +55,7 @@ class InterventionQualityChecker(AbstractQualityChecker):
self._add_missing_attr_name(_("Responsible data"))
def _check_legal_data(self):
""" Checks data quality of related LegalData
""" Checks data quality of related Legal
Args:
self.messages (dict): Holds error messages

@ -4,8 +4,8 @@ from django.http import HttpRequest, JsonResponse
from django.shortcuts import render
from intervention.forms.forms import NewInterventionForm, EditInterventionForm
from intervention.forms.modalForms import ShareInterventionModalForm, NewRevocationModalForm, \
CheckModalForm, NewDeductionModalForm
from intervention.forms.modalForms import ShareModalForm, NewRevocationModalForm, \
CheckModalForm, NewDeductionModalForm, NewInterventionDocumentForm
from intervention.models import Intervention, Revocation, InterventionDocument, RevocationDocument
from intervention.tables import InterventionTable
from konova.contexts import BaseContext
@ -123,7 +123,7 @@ def new_document_view(request: HttpRequest, id: str):
"""
intervention = get_object_or_404(Intervention, id=id)
form = NewDocumentForm(request.POST or None, request.FILES or None, instance=intervention, user=request.user)
form = NewInterventionDocumentForm(request.POST or None, request.FILES or None, instance=intervention, user=request.user)
return form.process_request(
request,
msg_success=_("Document added")
@ -402,7 +402,7 @@ def create_share_view(request: HttpRequest, id: str):
"""
intervention = get_object_or_404(Intervention, id=id)
form = ShareInterventionModalForm(request.POST or None, instance=intervention, request=request, user=request.user)
form = ShareModalForm(request.POST or None, instance=intervention, request=request, user=request.user)
return form.process_request(
request,
msg_success=_("Share settings updated")

@ -21,14 +21,11 @@ from django.shortcuts import render
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from compensation.models import EcoAccount, Compensation, EcoAccountDocument, CompensationDocument
from ema.models import Ema, EmaDocument
from intervention.models import Intervention, Revocation, RevocationDocument, InterventionDocument
from konova.contexts import BaseContext
from konova.models import BaseObject, Geometry
from konova.models import BaseObject, Geometry, RecordableObjectMixin
from konova.settings import DEFAULT_SRID
from konova.utils.message_templates import FORM_INVALID
from user.models import UserActionLogEntry, UserAction
from user.models import UserActionLogEntry
class BaseForm(forms.Form):
@ -171,11 +168,7 @@ class RemoveForm(BaseForm):
if self.object_to_remove is not None and self.is_checked():
with transaction.atomic():
self.object_to_remove.is_active = False
action = UserActionLogEntry.objects.create(
user=user,
timestamp=timezone.now(),
action=UserAction.DELETED
)
action = UserActionLogEntry.get_deleted_action(user)
self.object_to_remove.deleted = action
self.object_to_remove.save()
return self.object_to_remove
@ -293,7 +286,7 @@ class SimpleGeomForm(BaseForm):
geometry.geom = self.cleaned_data.get("geom", MultiPolygon(srid=DEFAULT_SRID))
geometry.modified = action
geometry.save()
except (AttributeError) as e:
except AttributeError:
# 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)),
@ -382,13 +375,10 @@ class NewDocumentForm(BaseModalForm):
}
)
)
document_instance_map = {
Intervention: InterventionDocument,
Compensation: CompensationDocument,
EcoAccount: EcoAccountDocument,
Revocation: RevocationDocument,
Ema: EmaDocument,
}
document_model = None
class Meta:
abstract = True
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@ -398,20 +388,15 @@ class NewDocumentForm(BaseModalForm):
self.form_attrs = {
"enctype": "multipart/form-data", # important for file upload
}
self.document_type = self.document_instance_map.get(
self.instance.__class__,
None
)
if not self.document_type:
if not self.document_model:
raise NotImplementedError("Unsupported document type for {}".format(self.instance.__class__))
def save(self):
with transaction.atomic():
action = UserActionLogEntry.objects.create(
user=self.user,
action=UserAction.CREATED,
)
doc = self.document_type.objects.create(
action = UserActionLogEntry.get_created_action(self.user)
edited_action = UserActionLogEntry.get_edited_action(self.user, _("Added document"))
doc = self.document_model.objects.create(
created=action,
title=self.cleaned_data["title"],
comment=self.cleaned_data["comment"],
@ -420,11 +405,6 @@ class NewDocumentForm(BaseModalForm):
instance=self.instance,
)
edited_action = UserActionLogEntry.objects.create(
user=self.user,
action=UserAction.EDITED,
comment=_("Added document"),
)
self.instance.log.add(edited_action)
self.instance.modified = edited_action
self.instance.save()
@ -456,13 +436,7 @@ class RecordModalForm(BaseModalForm):
self.form_title = _("Unrecord data")
self.form_caption = _("I, {} {}, confirm that this data must be unrecorded.").format(self.user.first_name, self.user.last_name)
implemented_cls_logic = {
Intervention,
EcoAccount,
Ema,
}
instance_name = self.instance.__class__
if instance_name not in implemented_cls_logic:
if not isinstance(self.instance, RecordableObjectMixin):
raise NotImplementedError
def is_valid(self):
@ -471,6 +445,7 @@ class RecordModalForm(BaseModalForm):
Returns:
"""
from intervention.models import Intervention
super_val = super().is_valid()
if self.instance.recorded:
# If user wants to unrecord an already recorded dataset, we do not need to perform custom checks

@ -0,0 +1,11 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 15.11.21
"""
from .object import *
from .deadline import *
from .document import *
from .geometry import *

@ -0,0 +1,49 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 15.11.21
"""
from django.db import models
from django.utils.translation import gettext_lazy as _
from konova.models import BaseResource
class DeadlineType(models.TextChoices):
"""
Django 3.x way of handling enums for models
"""
FINISHED = "finished", _("Finished")
MAINTAIN = "maintain", _("Maintain")
CONTROL = "control", _("Control")
OTHER = "other", _("Other")
class Deadline(BaseResource):
"""
Defines a deadline, which can be used to define dates with a semantic meaning
"""
type = models.CharField(max_length=255, null=True, blank=True, choices=DeadlineType.choices)
date = models.DateField(null=True, blank=True)
comment = models.TextField(null=True, blank=True)
def __str__(self):
return self.type
@property
def type_humanized(self):
""" Returns humanized version of enum
Used for template rendering
Returns:
"""
choices = DeadlineType.choices
for choice in choices:
if choice[0] == self.type:
return choice[1]
return None

@ -0,0 +1,84 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 15.11.21
"""
import os
from django.db import models
from konova.models import BaseResource
from konova.settings import INTERVENTION_REVOCATION_DOC_PATH
def generate_document_file_upload_path(instance, filename):
""" Generates the file upload path for certain document instances
Documents derived from AbstractDocument need specific upload paths for their related models.
Args:
instance (): The document instance
filename (): The filename
Returns:
"""
from compensation.models import CompensationDocument, EcoAccountDocument
from ema.models import EmaDocument
from intervention.models import InterventionDocument, RevocationDocument
from konova.settings import ECO_ACCOUNT_DOC_PATH, EMA_DOC_PATH, \
COMPENSATION_DOC_PATH, \
INTERVENTION_DOC_PATH
# Map document types to paths on the hard drive
path_map = {
InterventionDocument: INTERVENTION_DOC_PATH,
CompensationDocument: COMPENSATION_DOC_PATH,
EmaDocument: EMA_DOC_PATH,
RevocationDocument: INTERVENTION_REVOCATION_DOC_PATH,
EcoAccountDocument: ECO_ACCOUNT_DOC_PATH,
}
path = path_map.get(instance.__class__, None)
if path is None:
raise NotImplementedError("Unidentified document type: {}".format(instance.__class__))
# RevocationDocument needs special treatment, since these files need to be stored in a subfolder of the related
# instance's (Revocation) legaldata interventions folder
if instance.__class__ is RevocationDocument:
path = path.format(instance.intervention.id)
else:
path = path.format(instance.instance.id)
return path + filename
class AbstractDocument(BaseResource):
"""
Documents can be attached to compensation or intervention for uploading legal documents or pictures.
"""
title = models.CharField(max_length=500, null=True, blank=True)
date_of_creation = models.DateField()
file = models.FileField()
comment = models.TextField()
class Meta:
abstract = True
def delete(self, using=None, keep_parents=False):
""" Custom delete function to remove the real file from the hard drive
Args:
using ():
keep_parents ():
Returns:
"""
try:
os.remove(self.file.file.name)
except FileNotFoundError:
# File seems to missing anyway - continue!
pass
super().delete(using=using, keep_parents=keep_parents)

@ -0,0 +1,18 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 15.11.21
"""
from django.contrib.gis.db.models import MultiPolygonField
from konova.models import BaseResource
class Geometry(BaseResource):
"""
Outsourced geometry model so multiple versions of the same object can refer to the same geometry if it is not changed
"""
from konova.settings import DEFAULT_SRID
geom = MultiPolygonField(null=True, blank=True, srid=DEFAULT_SRID)

@ -2,25 +2,21 @@
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 17.11.20
Created on: 15.11.21
"""
import os
import uuid
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.translation import gettext_lazy as _
from django.contrib.gis.db.models import MultiPolygonField
from django.db import models, transaction
from compensation.settings import COMPENSATION_IDENTIFIER_TEMPLATE, COMPENSATION_IDENTIFIER_LENGTH, \
ECO_ACCOUNT_IDENTIFIER_TEMPLATE, ECO_ACCOUNT_IDENTIFIER_LENGTH
from ema.settings import EMA_ACCOUNT_IDENTIFIER_LENGTH, EMA_ACCOUNT_IDENTIFIER_TEMPLATE
from intervention.settings import INTERVENTION_IDENTIFIER_LENGTH, INTERVENTION_IDENTIFIER_TEMPLATE
from konova.settings import INTERVENTION_REVOCATION_DOC_PATH
from konova.utils import generators
from konova.utils.generators import generate_random_string
from user.models import UserActionLogEntry, UserAction
@ -75,7 +71,7 @@ class BaseResource(UuidModel):
"""
try:
self.created.delete()
except (ObjectDoesNotExist, AttributeError) as e:
except (ObjectDoesNotExist, AttributeError):
# Object does not exist anymore - we can skip this
pass
super().delete()
@ -112,11 +108,7 @@ class BaseObject(BaseResource):
return
with transaction.atomic():
action = UserActionLogEntry.objects.create(
user=user,
action=UserAction.DELETED,
timestamp=timezone.now()
)
action = UserActionLogEntry.get_deleted_action(user)
self.deleted = action
self.log.add(action)
self.save()
@ -139,45 +131,6 @@ class BaseObject(BaseResource):
)
self.log.add(user_action)
def is_shared_with(self, user: User):
""" Access check
Checks whether a given user has access to this object
Args:
user ():
Returns:
"""
if isinstance(self, ShareableObject):
return self.users.filter(id=user.id)
else:
return User.objects.none()
def share_with(self, user: User):
""" Adds user to list of shared access users
Args:
user (User): The user to be added to the object
Returns:
"""
if not self.is_shared_with(user):
self.users.add(user)
def share_with_list(self, user_list: list):
""" Sets the list of shared access users
Args:
user_list (list): The users to be added to the object
Returns:
"""
self.users.set(user_list)
def generate_new_identifier(self) -> str:
""" Generates a new identifier for the intervention object
@ -224,123 +177,7 @@ class BaseObject(BaseResource):
return definitions[self.__class__]["template"].format(_str)
class DeadlineType(models.TextChoices):
"""
Django 3.x way of handling enums for models
"""
FINISHED = "finished", _("Finished")
MAINTAIN = "maintain", _("Maintain")
CONTROL = "control", _("Control")
OTHER = "other", _("Other")
class Deadline(BaseResource):
"""
Defines a deadline, which can be used to define dates with a semantic meaning
"""
type = models.CharField(max_length=255, null=True, blank=True, choices=DeadlineType.choices)
date = models.DateField(null=True, blank=True)
comment = models.TextField(null=True, blank=True)
def __str__(self):
return self.type
@property
def type_humanized(self):
""" Returns humanized version of enum
Used for template rendering
Returns:
"""
choices = DeadlineType.choices
for choice in choices:
if choice[0] == self.type:
return choice[1]
return None
def generate_document_file_upload_path(instance, filename):
""" Generates the file upload path for certain document instances
Documents derived from AbstractDocument need specific upload paths for their related models.
Args:
instance (): The document instance
filename (): The filename
Returns:
"""
from compensation.models import CompensationDocument, EcoAccountDocument
from ema.models import EmaDocument
from intervention.models import InterventionDocument, RevocationDocument
from konova.settings import ECO_ACCOUNT_DOC_PATH, EMA_DOC_PATH, \
COMPENSATION_DOC_PATH, \
INTERVENTION_DOC_PATH
# Map document types to paths on the hard drive
path_map = {
InterventionDocument: INTERVENTION_DOC_PATH,
CompensationDocument: COMPENSATION_DOC_PATH,
EmaDocument: EMA_DOC_PATH,
RevocationDocument: INTERVENTION_REVOCATION_DOC_PATH,
EcoAccountDocument: ECO_ACCOUNT_DOC_PATH,
}
path = path_map.get(instance.__class__, None)
if path is None:
raise NotImplementedError("Unidentified document type: {}".format(instance.__class__))
# RevocationDocument needs special treatment, since these files need to be stored in a subfolder of the related
# instance's (Revocation) legaldata interventions folder
if instance.__class__ is RevocationDocument:
path = path.format(instance.intervention.id)
else:
path = path.format(instance.instance.id)
return path + filename
class AbstractDocument(BaseResource):
"""
Documents can be attached to compensation or intervention for uploading legal documents or pictures.
"""
title = models.CharField(max_length=500, null=True, blank=True)
date_of_creation = models.DateField()
file = models.FileField()
comment = models.TextField()
class Meta:
abstract = True
def delete(self, using=None, keep_parents=False):
""" Custom delete function to remove the real file from the hard drive
Args:
using ():
keep_parents ():
Returns:
"""
try:
os.remove(self.file.file.name)
except FileNotFoundError:
# File seems to missing anyway - continue!
pass
super().delete(using=using, keep_parents=keep_parents)
class Geometry(BaseResource):
"""
Outsourced geometry model so multiple versions of the same object can refer to the same geometry if it is not changed
"""
from konova.settings import DEFAULT_SRID
geom = MultiPolygonField(null=True, blank=True, srid=DEFAULT_SRID)
class RecordableObject(models.Model):
class RecordableObjectMixin(models.Model):
""" Wraps record related fields and functionality
"""
@ -366,10 +203,7 @@ class RecordableObject(models.Model):
Returns:
"""
action = UserActionLogEntry.objects.create(
user=user,
action=UserAction.UNRECORDED
)
action = UserActionLogEntry.get_unrecorded_action(user)
self.recorded = None
self.save()
self.log.add(action)
@ -384,10 +218,7 @@ class RecordableObject(models.Model):
Returns:
"""
action = UserActionLogEntry.objects.create(
user=user,
action=UserAction.RECORDED
)
action = UserActionLogEntry.get_recorded_action(user)
self.recorded = action
self.save()
self.log.add(action)
@ -409,7 +240,7 @@ class RecordableObject(models.Model):
return ret_log_entry
class CheckableObject(models.Model):
class CheckableObjectMixin(models.Model):
# Checks - Refers to "Genehmigen" but optional
checked = models.OneToOneField(
UserActionLogEntry,
@ -446,10 +277,7 @@ class CheckableObject(models.Model):
Returns:
"""
action = UserActionLogEntry.objects.create(
user=user,
action=UserAction.CHECKED
)
action = UserActionLogEntry.get_checked_action(user)
self.checked = action
self.save()
self.log.add(action)
@ -471,7 +299,7 @@ class CheckableObject(models.Model):
return ret_log_entry
class ShareableObject(models.Model):
class ShareableObjectMixin(models.Model):
# Users having access on this object
users = models.ManyToManyField(User, help_text="Users having access (data shared with)")
access_token = models.CharField(
@ -519,3 +347,58 @@ class ShareableObject(models.Model):
else:
self.access_token = token
self.save()
def is_shared_with(self, user: User):
""" Access check
Checks whether a given user has access to this object
Args:
user ():
Returns:
"""
return self.users.filter(id=user.id)
def share_with(self, user: User):
""" Adds user to list of shared access users
Args:
user (User): The user to be added to the object
Returns:
"""
if not self.is_shared_with(user):
self.users.add(user)
def share_with_list(self, user_list: list):
""" Sets the list of shared access users
Args:
user_list (list): The users to be added to the object
Returns:
"""
self.users.set(user_list)
def update_sharing_user(self, form):
""" Adds a new user with shared access to the object
Args:
form (ShareModalForm): The form holding the data
Returns:
"""
form_data = form.cleaned_data
keep_accessing_users = form_data["users"]
new_accessing_users = list(form_data["user_select"].values_list("id", flat=True))
accessing_users = keep_accessing_users + new_accessing_users
users = User.objects.filter(
id__in=accessing_users
)
self.share_with_list(users)

@ -15,12 +15,12 @@ from django.urls import reverse
from codelist.models import KonovaCode
from compensation.models import Compensation, CompensationState, CompensationAction, EcoAccount
from intervention.models import LegalData, ResponsibilityData, Intervention
from intervention.models import Legal, Responsibility, Intervention
from konova.management.commands.setup_data import GROUPS_DATA
from konova.models import Geometry
from konova.settings import DEFAULT_GROUP
from konova.utils.generators import generate_random_string
from user.models import UserActionLogEntry, UserAction
from user.models import UserActionLogEntry
class BaseTestCase(TestCase):
@ -98,14 +98,11 @@ class BaseTestCase(TestCase):
"""
# Create dummy data
# Create log entry
action = UserActionLogEntry.objects.create(
user=cls.superuser,
action=UserAction.CREATED,
)
action = UserActionLogEntry.get_created_action(cls.superuser)
# Create legal data object (without M2M laws first)
legal_data = LegalData.objects.create()
legal_data = Legal.objects.create()
# Create responsible data object
responsibility_data = ResponsibilityData.objects.create()
responsibility_data = Responsibility.objects.create()
geometry = Geometry.objects.create()
# Finally create main object, holding the other objects
intervention = Intervention.objects.create(
@ -131,10 +128,7 @@ class BaseTestCase(TestCase):
cls.intervention = cls.create_dummy_intervention()
# Create dummy data
# Create log entry
action = UserActionLogEntry.objects.create(
user=cls.superuser,
action=UserAction.CREATED,
)
action = UserActionLogEntry.get_created_action(cls.superuser)
geometry = Geometry.objects.create()
# Finally create main object, holding the other objects
compensation = Compensation.objects.create(
@ -156,14 +150,11 @@ class BaseTestCase(TestCase):
"""
# Create dummy data
# Create log entry
action = UserActionLogEntry.objects.create(
user=cls.superuser,
action=UserAction.CREATED,
)
action = UserActionLogEntry.get_created_action(cls.superuser)
geometry = Geometry.objects.create()
# Create responsible data object
lega_data = LegalData.objects.create()
responsible_data = ResponsibilityData.objects.create()
lega_data = Legal.objects.create()
responsible_data = Responsibility.objects.create()
# Finally create main object, holding the other objects
eco_account = EcoAccount.objects.create(
identifier="TEST",

@ -0,0 +1,10 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 15.11.21
"""
from .user_action import *
from .konova_user import *
from .notification import *

@ -0,0 +1,19 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 15.11.21
"""
from django.contrib.auth.models import User
from django.db import models
class KonovaUserExtension(models.Model):
""" Extension model for additional ksp features
Extends the default user model for some extras
"""
user = models.OneToOneField(User, on_delete=models.CASCADE)
notifications = models.ManyToManyField("user.UserNotification", related_name="+")

@ -0,0 +1,34 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 15.11.21
"""
from django.db import models
from user.enums import UserNotificationEnum
class UserNotification(models.Model):
""" Notifications for users
"""
id = models.CharField(
max_length=500,
null=False,
blank=False,
choices=UserNotificationEnum.as_choices(drop_empty_choice=True),
primary_key=True,
)
name = models.CharField(
max_length=500,
null=False,
blank=False,
unique=True,
help_text="Human readable name"
)
is_active = models.BooleanField(default=True, help_text="Can be toggle to enable/disable this notification for all users")
def __str__(self):
return self.name

@ -1,44 +1,15 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 15.11.21
"""
import uuid
from django.utils.translation import gettext_lazy as _
from django.contrib.auth.models import User
from django.db import models
from user.enums import UserNotificationEnum
class UserNotification(models.Model):
""" Notifications for users
"""
id = models.CharField(
max_length=500,
null=False,
blank=False,
choices=UserNotificationEnum.as_choices(drop_empty_choice=True),
primary_key=True,
)
name = models.CharField(
max_length=500,
null=False,
blank=False,
unique=True,
help_text="Human readable name"
)
is_active = models.BooleanField(default=True, help_text="Can be toggle to enable/disable this notification for all users")
def __str__(self):
return self.name
class KonovaUserExtension(models.Model):
""" Extension model for additional ksp features
Extends the default user model for some extras
"""
user = models.OneToOneField(User, on_delete=models.CASCADE)
notifications = models.ManyToManyField(UserNotification, related_name="+")
from django.utils.translation import gettext_lazy as _
class UserAction(models.TextChoices):
@ -96,3 +67,57 @@ class UserActionLogEntry(models.Model):
if choice[0] == self.action:
return choice[1]
return None
@classmethod
def get_created_action(cls, user: User, comment: str = None):
action = UserActionLogEntry.objects.create(
user=user,
action=UserAction.CREATED,
comment=comment,
)
return action
@classmethod
def get_edited_action(cls, user: User, comment: str = None):
action = UserActionLogEntry.objects.create(
user=user,
action=UserAction.EDITED,
comment=comment,
)
return action
@classmethod
def get_deleted_action(cls, user: User, comment: str = None):
action = UserActionLogEntry.objects.create(
user=user,
action=UserAction.DELETED,
comment=comment,
)
return action
@classmethod
def get_checked_action(cls, user: User, comment: str = None):
action = UserActionLogEntry.objects.create(
user=user,
action=UserAction.CHECKED,
comment=comment,
)
return action
@classmethod
def get_recorded_action(cls, user: User, comment: str = None):
action = UserActionLogEntry.objects.create(
user=user,
action=UserAction.RECORDED,
comment=comment,
)
return action
@classmethod
def get_unrecorded_action(cls, user: User, comment: str = None):
action = UserActionLogEntry.objects.create(
user=user,
action=UserAction.UNRECORDED,
comment=comment,
)
return action
Loading…
Cancel
Save