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

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

@ -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 django.urls import path
from compensation.views.compensation_views import * from compensation.views.compensation import *
urlpatterns = [ urlpatterns = [
# Main compensation # Main compensation

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

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

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

@ -61,7 +61,7 @@ class EcoAccountQualityChecker(CompensationQualityChecker):
super().run_check() super().run_check()
def _check_legal_data(self): def _check_legal_data(self):
""" Checks the data quality for LegalData """ Checks the data quality for Legal
Returns: 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 django.utils.translation import gettext_lazy as _
from compensation.forms.forms import NewCompensationForm, EditCompensationForm 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.models import Compensation, CompensationState, CompensationAction, CompensationDocument
from compensation.tables import CompensationTable from compensation.tables import CompensationTable
from intervention.models import Intervention from intervention.models import Intervention
from konova.contexts import BaseContext from konova.contexts import BaseContext
from konova.decorators import * from konova.decorators import *
from konova.forms import RemoveModalForm, SimpleGeomForm, NewDocumentForm from konova.forms import RemoveModalForm, SimpleGeomForm
from konova.utils.documents import get_document, remove_document from konova.utils.documents import get_document, remove_document
from konova.utils.generators import generate_qr_code from konova.utils.generators import generate_qr_code
from konova.utils.message_templates import FORM_INVALID, IDENTIFIER_REPLACED, DATA_UNSHARED_EXPLANATION 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) 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( return form.process_request(
request, request,
msg_success=_("Document added") 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 django.shortcuts import render, get_object_or_404, redirect
from compensation.forms.forms import NewEcoAccountForm, EditEcoAccountForm 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.models import EcoAccount, EcoAccountDocument, CompensationState, CompensationAction
from compensation.tables import EcoAccountTable 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.contexts import BaseContext
from konova.decorators import any_group_check, default_group_required, conservation_office_group_required, \ from konova.decorators import any_group_check, default_group_required, conservation_office_group_required, \
shared_access_required shared_access_required
@ -453,7 +454,7 @@ def new_document_view(request: HttpRequest, id: str):
""" """
acc = get_object_or_404(EcoAccount, id=id) 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( return form.process_request(
request, request,
msg_success=_("Document added") msg_success=_("Document added")
@ -641,7 +642,7 @@ def create_share_view(request: HttpRequest, id: str):
""" """
obj = get_object_or_404(EcoAccount, id=id) 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( return form.process_request(
request, request,
msg_success=_("Share settings updated") 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 django.utils.translation import gettext_lazy as _
from compensation.forms.forms import AbstractCompensationForm, CompensationResponsibleFormMixin from compensation.forms.forms import AbstractCompensationForm, CompensationResponsibleFormMixin
from ema.models import Ema from ema.models import Ema, EmaDocument
from intervention.models import ResponsibilityData from intervention.models import Responsibility
from konova.forms import SimpleGeomForm from konova.forms import SimpleGeomForm, NewDocumentForm
from user.models import UserActionLogEntry, UserAction from user.models import UserActionLogEntry
class NewEmaForm(AbstractCompensationForm, CompensationResponsibleFormMixin): class NewEmaForm(AbstractCompensationForm, CompensationResponsibleFormMixin):
@ -59,14 +59,11 @@ class NewEmaForm(AbstractCompensationForm, CompensationResponsibleFormMixin):
comment = self.cleaned_data.get("comment", None) comment = self.cleaned_data.get("comment", None)
# Create log entry # Create log entry
action = UserActionLogEntry.objects.create( action = UserActionLogEntry.get_created_action(user)
user=user,
action=UserAction.CREATED,
)
# Process the geometry form # Process the geometry form
geometry = geom_form.save(action) geometry = geom_form.save(action)
responsible = ResponsibilityData.objects.create( responsible = Responsibility.objects.create(
handler=handler, handler=handler,
conservation_file_number=conservation_file_number, conservation_file_number=conservation_file_number,
conservation_office=conservation_office, conservation_office=conservation_office,
@ -130,10 +127,7 @@ class EditEmaForm(NewEmaForm):
comment = self.cleaned_data.get("comment", None) comment = self.cleaned_data.get("comment", None)
# Create log entry # Create log entry
action = UserActionLogEntry.objects.create( action = UserActionLogEntry.get_edited_action(user)
user=user,
action=UserAction.EDITED,
)
# Process the geometry form # Process the geometry form
geometry = geom_form.save(action) geometry = geom_form.save(action)
@ -154,3 +148,7 @@ class EditEmaForm(NewEmaForm):
# Add the log entry to the main objects log list # Add the log entry to the main objects log list
self.instance.log.add(action) self.instance.log.add(action)
return self.instance 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 import shutil
from django.db import models from django.db import models
@ -6,11 +13,11 @@ from django.db.models import QuerySet
from compensation.models import AbstractCompensation from compensation.models import AbstractCompensation
from ema.managers import EmaManager from ema.managers import EmaManager
from ema.utils.quality import EmaQualityChecker from ema.utils.quality import EmaQualityChecker
from konova.models import AbstractDocument, generate_document_file_upload_path, RecordableObject, ShareableObject from konova.models import AbstractDocument, generate_document_file_upload_path, RecordableObjectMixin, ShareableObjectMixin
from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE
class Ema(AbstractCompensation, ShareableObject, RecordableObject): class Ema(AbstractCompensation, ShareableObjectMixin, RecordableObjectMixin):
""" """
EMA = Ersatzzahlungsmaßnahme EMA = Ersatzzahlungsmaßnahme
(compensation actions from payments) (compensation actions from payments)

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

@ -8,13 +8,13 @@ from django.utils.translation import gettext_lazy as _
from compensation.forms.modalForms import NewStateModalForm, NewActionModalForm, NewDeadlineModalForm from compensation.forms.modalForms import NewStateModalForm, NewActionModalForm, NewDeadlineModalForm
from compensation.models import CompensationAction, CompensationState 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 ema.tables import EmaTable
from intervention.forms.modalForms import ShareInterventionModalForm from intervention.forms.modalForms import ShareModalForm
from konova.contexts import BaseContext from konova.contexts import BaseContext
from konova.decorators import conservation_office_group_required, shared_access_required from konova.decorators import conservation_office_group_required, shared_access_required
from ema.models import Ema, EmaDocument 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.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
from konova.utils.documents import get_document, remove_document from konova.utils.documents import get_document, remove_document
from konova.utils.generators import generate_qr_code 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) 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( return form.process_request(
request, request,
msg_success=_("Document added") msg_success=_("Document added")
@ -546,7 +546,7 @@ def create_share_view(request: HttpRequest, id: str):
""" """
obj = get_object_or_404(Ema, id=id) 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( return form.process_request(
request, request,
msg_success=_("Share settings updated") msg_success=_("Share settings updated")

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

@ -10,16 +10,15 @@ from django import forms
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db import transaction from django.db import transaction
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from codelist.models import KonovaCode from codelist.models import KonovaCode
from codelist.settings import CODELIST_PROCESS_TYPE_ID, CODELIST_LAW_ID, \ from codelist.settings import CODELIST_PROCESS_TYPE_ID, CODELIST_LAW_ID, \
CODELIST_REGISTRATION_OFFICE_ID, CODELIST_CONSERVATION_OFFICE_ID CODELIST_REGISTRATION_OFFICE_ID, CODELIST_CONSERVATION_OFFICE_ID
from intervention.inputs import GenerateInput 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 konova.forms import BaseForm, SimpleGeomForm
from user.models import UserActionLogEntry, UserAction from user.models import UserActionLogEntry
class NewInterventionForm(BaseForm): class NewInterventionForm(BaseForm):
@ -214,13 +213,10 @@ class NewInterventionForm(BaseForm):
comment = self.cleaned_data.get("comment", None) comment = self.cleaned_data.get("comment", None)
# Create log entry # Create log entry
action = UserActionLogEntry.objects.create( action = UserActionLogEntry.get_created_action(user)
user=user,
action=UserAction.CREATED,
)
# Create legal data object (without M2M laws first) # Create legal data object (without M2M laws first)
legal_data = LegalData.objects.create( legal_data = Legal.objects.create(
registration_date=registration_date, registration_date=registration_date,
binding_date=binding_date, binding_date=binding_date,
process_type=_type, process_type=_type,
@ -229,7 +225,7 @@ class NewInterventionForm(BaseForm):
legal_data.laws.set(laws) legal_data.laws.set(laws)
# Create responsible data object # Create responsible data object
responsibility_data = ResponsibilityData.objects.create( responsibility_data = Responsibility.objects.create(
registration_office=registration_office, registration_office=registration_office,
conservation_office=conservation_office, conservation_office=conservation_office,
registration_file_number=registration_file_number, 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.conservation_file_number = conservation_file_number
self.instance.responsible.save() self.instance.responsible.save()
user_action = UserActionLogEntry.objects.create( user_action = UserActionLogEntry.get_edited_action(user)
user=user,
timestamp=timezone.now(),
action=UserAction.EDITED,
)
geometry = geom_form.save(user_action) geometry = geom_form.save(user_action)
self.instance.geometry = geometry self.instance.geometry = geometry
@ -356,8 +348,10 @@ class EditInterventionForm(NewInterventionForm):
self.instance.save() self.instance.save()
# Uncheck and unrecord intervention due to changed data # Uncheck and unrecord intervention due to changed data
self.instance.set_unchecked() if self.instance.checked:
self.instance.set_unrecorded(user) self.instance.set_unchecked()
if self.instance.recorded:
self.instance.set_unrecorded(user)
return self.instance return self.instance

@ -10,19 +10,17 @@ from django.contrib.auth.models import User
from django.db import transaction from django.db import transaction
from django import forms from django import forms
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from compensation.models import EcoAccount, EcoAccountDeduction from compensation.models import EcoAccount
from intervention.inputs import TextToClipboardInput from intervention.inputs import TextToClipboardInput
from intervention.models import Revocation, RevocationDocument, Intervention from intervention.models import Intervention, InterventionDocument
from konova.forms import BaseModalForm from konova.forms import BaseModalForm, NewDocumentForm
from konova.utils.general import format_german_float from konova.utils.general import format_german_float
from konova.utils.messenger import Messenger
from konova.utils.user_checks import is_default_group_only 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( url = forms.CharField(
label=_("Share link"), label=_("Share link"),
label_suffix="", label_suffix="",
@ -118,13 +116,7 @@ class ShareInterventionModalForm(BaseModalForm):
) )
def save(self): def save(self):
still_accessing_users = self.cleaned_data["users"] self.instance.update_sharing_user(self)
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)
class NewRevocationModalForm(BaseModalForm): class NewRevocationModalForm(BaseModalForm):
@ -176,33 +168,7 @@ class NewRevocationModalForm(BaseModalForm):
} }
def save(self): def save(self):
with transaction.atomic(): revocation = self.instance.add_revocation(self)
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
)
return revocation return revocation
@ -261,16 +227,6 @@ class CheckModalForm(BaseModalForm):
with transaction.atomic(): with transaction.atomic():
self.instance.toggle_checked(self.user) 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): class NewDeductionModalForm(BaseModalForm):
""" Form for creating new deduction """ Form for creating new deduction
@ -326,14 +282,12 @@ class NewDeductionModalForm(BaseModalForm):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.form_title = _("New Deduction") self.form_title = _("New Deduction")
self.form_caption = _("Enter the information for a new deduction from a chosen eco-account") self.form_caption = _("Enter the information for a new deduction from a chosen eco-account")
self.is_intervention_initially = False
# Check for Intervention or EcoAccount # Check for Intervention or EcoAccount
if isinstance(self.instance, Intervention): if isinstance(self.instance, Intervention):
# Form has been called with a given intervention # Form has been called with a given intervention
self.initialize_form_field("intervention", self.instance) self.initialize_form_field("intervention", self.instance)
self.disable_form_field("intervention") self.disable_form_field("intervention")
self.is_intervention_initially = True
elif isinstance(self.instance, EcoAccount): elif isinstance(self.instance, EcoAccount):
# Form has been called with a given account --> make it initial in the form and read-only # Form has been called with a given account --> make it initial in the form and read-only
self.initialize_form_field("account", self.instance) self.initialize_form_field("account", self.instance)
@ -350,10 +304,7 @@ class NewDeductionModalForm(BaseModalForm):
is_valid (bool) is_valid (bool)
""" """
super_result = super().is_valid() super_result = super().is_valid()
if self.is_intervention_initially: acc = self.cleaned_data["account"]
acc = self.cleaned_data["account"]
else:
acc = self.instance
if not acc.recorded: if not acc.recorded:
self.add_error( self.add_error(
@ -367,7 +318,7 @@ class NewDeductionModalForm(BaseModalForm):
sum_surface_deductions = acc.get_deductions_surface() sum_surface_deductions = acc.get_deductions_surface()
rest_surface = deductable_surface - sum_surface_deductions rest_surface = deductable_surface - sum_surface_deductions
form_surface = float(self.cleaned_data["surface"]) 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: if not is_valid_surface:
self.add_error( self.add_error(
"surface", "surface",
@ -380,33 +331,9 @@ class NewDeductionModalForm(BaseModalForm):
return is_valid_surface and super_result return is_valid_surface and super_result
def save(self): def save(self):
with transaction.atomic(): deduction = self.instance.add_deduction(self)
# 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,
)
return deduction 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 Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 17.11.20 Created on: 15.11.21
""" """
import shutil import shutil
from django.contrib.auth.models import User 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.db.models import QuerySet
from django.utils.translation import gettext_lazy as _
from codelist.models import KonovaCode from compensation.models import EcoAccountDeduction
from codelist.settings import CODELIST_REGISTRATION_OFFICE_ID, CODELIST_CONSERVATION_OFFICE_ID, CODELIST_LAW_ID, \
CODELIST_PROCESS_TYPE_ID
from intervention.managers import InterventionManager 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 intervention.utils.quality import InterventionQualityChecker
from konova.models import BaseObject, Geometry, UuidModel, BaseResource, AbstractDocument, \ from konova.models import generate_document_file_upload_path, AbstractDocument, Geometry, BaseObject, ShareableObjectMixin, \
generate_document_file_upload_path, RecordableObject, CheckableObject, ShareableObject RecordableObjectMixin, CheckableObjectMixin
from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE, LANIS_ZOOM_LUT from konova.settings import LANIS_LINK_TEMPLATE, LANIS_ZOOM_LUT, DEFAULT_SRID_RLP
from user.models import UserActionLogEntry from user.models import UserActionLogEntry
class ResponsibilityData(UuidModel): class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, CheckableObjectMixin):
"""
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):
""" """
Interventions are e.g. construction sites where nature used to be. Interventions are e.g. construction sites where nature used to be.
""" """
responsible = models.OneToOneField( responsible = models.OneToOneField(
ResponsibilityData, Responsibility,
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
null=True, null=True,
blank=True, blank=True,
help_text="Holds data on responsible organizations ('Zulassungsbehörde', 'Eintragungsstelle')" help_text="Holds data on responsible organizations ('Zulassungsbehörde', 'Eintragungsstelle')"
) )
legal = models.OneToOneField( legal = models.OneToOneField(
LegalData, Legal,
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
null=True, null=True,
blank=True, blank=True,
@ -322,6 +176,98 @@ class Intervention(BaseObject, ShareableObject, RecordableObject, CheckableObjec
for comp in comps: for comp in comps:
comp.log.add(log_entry) 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): class InterventionDocument(AbstractDocument):
""" """
@ -366,7 +312,6 @@ class InterventionDocument(AbstractDocument):
if folder_path is not None: if folder_path is not None:
try: try:
shutil.rmtree(folder_path) shutil.rmtree(folder_path)
pass
except FileNotFoundError: except FileNotFoundError:
# Folder seems to be missing already... # Folder seems to be missing already...
pass 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) # Prepare the account for a working situation (enough deductable surface, recorded and shared)
self.eco_account.deductable_surface = 10000.00 self.eco_account.deductable_surface = 10000.00
if self.eco_account.recorded is None: if self.eco_account.recorded is None:
rec_action = UserActionLogEntry.objects.create( rec_action = UserActionLogEntry.get_recorded_action(self.superuser)
user=self.superuser,
action=UserAction.RECORDED
)
self.eco_account.recorded = rec_action self.eco_account.recorded = rec_action
self.eco_account.share_with_list([self.superuser]) self.eco_account.share_with_list([self.superuser])
self.eco_account.save() self.eco_account.save()

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

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

@ -21,14 +21,11 @@ from django.shortcuts import render
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _ 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.contexts import BaseContext
from konova.models import BaseObject, Geometry from konova.models import BaseObject, Geometry, RecordableObjectMixin
from konova.settings import DEFAULT_SRID from konova.settings import DEFAULT_SRID
from konova.utils.message_templates import FORM_INVALID from konova.utils.message_templates import FORM_INVALID
from user.models import UserActionLogEntry, UserAction from user.models import UserActionLogEntry
class BaseForm(forms.Form): class BaseForm(forms.Form):
@ -171,11 +168,7 @@ class RemoveForm(BaseForm):
if self.object_to_remove is not None and self.is_checked(): if self.object_to_remove is not None and self.is_checked():
with transaction.atomic(): with transaction.atomic():
self.object_to_remove.is_active = False self.object_to_remove.is_active = False
action = UserActionLogEntry.objects.create( action = UserActionLogEntry.get_deleted_action(user)
user=user,
timestamp=timezone.now(),
action=UserAction.DELETED
)
self.object_to_remove.deleted = action self.object_to_remove.deleted = action
self.object_to_remove.save() self.object_to_remove.save()
return self.object_to_remove return self.object_to_remove
@ -293,7 +286,7 @@ class SimpleGeomForm(BaseForm):
geometry.geom = self.cleaned_data.get("geom", MultiPolygon(srid=DEFAULT_SRID)) geometry.geom = self.cleaned_data.get("geom", MultiPolygon(srid=DEFAULT_SRID))
geometry.modified = action geometry.modified = action
geometry.save() geometry.save()
except (AttributeError) as e: except AttributeError:
# No geometry or linked instance holding a geometry exist --> create a new one! # No geometry or linked instance holding a geometry exist --> create a new one!
geometry = Geometry.objects.create( geometry = Geometry.objects.create(
geom=self.cleaned_data.get("geom", MultiPolygon(srid=DEFAULT_SRID)), geom=self.cleaned_data.get("geom", MultiPolygon(srid=DEFAULT_SRID)),
@ -382,13 +375,10 @@ class NewDocumentForm(BaseModalForm):
} }
) )
) )
document_instance_map = { document_model = None
Intervention: InterventionDocument,
Compensation: CompensationDocument, class Meta:
EcoAccount: EcoAccountDocument, abstract = True
Revocation: RevocationDocument,
Ema: EmaDocument,
}
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@ -398,20 +388,15 @@ class NewDocumentForm(BaseModalForm):
self.form_attrs = { self.form_attrs = {
"enctype": "multipart/form-data", # important for file upload "enctype": "multipart/form-data", # important for file upload
} }
self.document_type = self.document_instance_map.get( if not self.document_model:
self.instance.__class__,
None
)
if not self.document_type:
raise NotImplementedError("Unsupported document type for {}".format(self.instance.__class__)) raise NotImplementedError("Unsupported document type for {}".format(self.instance.__class__))
def save(self): def save(self):
with transaction.atomic(): with transaction.atomic():
action = UserActionLogEntry.objects.create( action = UserActionLogEntry.get_created_action(self.user)
user=self.user, edited_action = UserActionLogEntry.get_edited_action(self.user, _("Added document"))
action=UserAction.CREATED,
) doc = self.document_model.objects.create(
doc = self.document_type.objects.create(
created=action, created=action,
title=self.cleaned_data["title"], title=self.cleaned_data["title"],
comment=self.cleaned_data["comment"], comment=self.cleaned_data["comment"],
@ -420,11 +405,6 @@ class NewDocumentForm(BaseModalForm):
instance=self.instance, 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.log.add(edited_action)
self.instance.modified = edited_action self.instance.modified = edited_action
self.instance.save() self.instance.save()
@ -456,13 +436,7 @@ class RecordModalForm(BaseModalForm):
self.form_title = _("Unrecord data") 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) self.form_caption = _("I, {} {}, confirm that this data must be unrecorded.").format(self.user.first_name, self.user.last_name)
implemented_cls_logic = { if not isinstance(self.instance, RecordableObjectMixin):
Intervention,
EcoAccount,
Ema,
}
instance_name = self.instance.__class__
if instance_name not in implemented_cls_logic:
raise NotImplementedError raise NotImplementedError
def is_valid(self): def is_valid(self):
@ -471,6 +445,7 @@ class RecordModalForm(BaseModalForm):
Returns: Returns:
""" """
from intervention.models import Intervention
super_val = super().is_valid() super_val = super().is_valid()
if self.instance.recorded: if self.instance.recorded:
# If user wants to unrecord an already recorded dataset, we do not need to perform custom checks # 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 Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 17.11.20 Created on: 15.11.21
""" """
import os
import uuid import uuid
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.utils import timezone from django.utils import timezone
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from django.contrib.gis.db.models import MultiPolygonField
from django.db import models, transaction from django.db import models, transaction
from compensation.settings import COMPENSATION_IDENTIFIER_TEMPLATE, COMPENSATION_IDENTIFIER_LENGTH, \ from compensation.settings import COMPENSATION_IDENTIFIER_TEMPLATE, COMPENSATION_IDENTIFIER_LENGTH, \
ECO_ACCOUNT_IDENTIFIER_TEMPLATE, ECO_ACCOUNT_IDENTIFIER_LENGTH ECO_ACCOUNT_IDENTIFIER_TEMPLATE, ECO_ACCOUNT_IDENTIFIER_LENGTH
from ema.settings import EMA_ACCOUNT_IDENTIFIER_LENGTH, EMA_ACCOUNT_IDENTIFIER_TEMPLATE from ema.settings import EMA_ACCOUNT_IDENTIFIER_LENGTH, EMA_ACCOUNT_IDENTIFIER_TEMPLATE
from intervention.settings import INTERVENTION_IDENTIFIER_LENGTH, INTERVENTION_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 import generators
from konova.utils.generators import generate_random_string from konova.utils.generators import generate_random_string
from user.models import UserActionLogEntry, UserAction from user.models import UserActionLogEntry, UserAction
@ -75,7 +71,7 @@ class BaseResource(UuidModel):
""" """
try: try:
self.created.delete() self.created.delete()
except (ObjectDoesNotExist, AttributeError) as e: except (ObjectDoesNotExist, AttributeError):
# Object does not exist anymore - we can skip this # Object does not exist anymore - we can skip this
pass pass
super().delete() super().delete()
@ -112,11 +108,7 @@ class BaseObject(BaseResource):
return return
with transaction.atomic(): with transaction.atomic():
action = UserActionLogEntry.objects.create( action = UserActionLogEntry.get_deleted_action(user)
user=user,
action=UserAction.DELETED,
timestamp=timezone.now()
)
self.deleted = action self.deleted = action
self.log.add(action) self.log.add(action)
self.save() self.save()
@ -139,45 +131,6 @@ class BaseObject(BaseResource):
) )
self.log.add(user_action) 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: def generate_new_identifier(self) -> str:
""" Generates a new identifier for the intervention object """ Generates a new identifier for the intervention object
@ -224,123 +177,7 @@ class BaseObject(BaseResource):
return definitions[self.__class__]["template"].format(_str) return definitions[self.__class__]["template"].format(_str)
class DeadlineType(models.TextChoices): class RecordableObjectMixin(models.Model):
"""
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):
""" Wraps record related fields and functionality """ Wraps record related fields and functionality
""" """
@ -366,10 +203,7 @@ class RecordableObject(models.Model):
Returns: Returns:
""" """
action = UserActionLogEntry.objects.create( action = UserActionLogEntry.get_unrecorded_action(user)
user=user,
action=UserAction.UNRECORDED
)
self.recorded = None self.recorded = None
self.save() self.save()
self.log.add(action) self.log.add(action)
@ -384,10 +218,7 @@ class RecordableObject(models.Model):
Returns: Returns:
""" """
action = UserActionLogEntry.objects.create( action = UserActionLogEntry.get_recorded_action(user)
user=user,
action=UserAction.RECORDED
)
self.recorded = action self.recorded = action
self.save() self.save()
self.log.add(action) self.log.add(action)
@ -409,7 +240,7 @@ class RecordableObject(models.Model):
return ret_log_entry return ret_log_entry
class CheckableObject(models.Model): class CheckableObjectMixin(models.Model):
# Checks - Refers to "Genehmigen" but optional # Checks - Refers to "Genehmigen" but optional
checked = models.OneToOneField( checked = models.OneToOneField(
UserActionLogEntry, UserActionLogEntry,
@ -446,10 +277,7 @@ class CheckableObject(models.Model):
Returns: Returns:
""" """
action = UserActionLogEntry.objects.create( action = UserActionLogEntry.get_checked_action(user)
user=user,
action=UserAction.CHECKED
)
self.checked = action self.checked = action
self.save() self.save()
self.log.add(action) self.log.add(action)
@ -471,7 +299,7 @@ class CheckableObject(models.Model):
return ret_log_entry return ret_log_entry
class ShareableObject(models.Model): class ShareableObjectMixin(models.Model):
# Users having access on this object # Users having access on this object
users = models.ManyToManyField(User, help_text="Users having access (data shared with)") users = models.ManyToManyField(User, help_text="Users having access (data shared with)")
access_token = models.CharField( access_token = models.CharField(
@ -519,3 +347,58 @@ class ShareableObject(models.Model):
else: else:
self.access_token = token self.access_token = token
self.save() 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 codelist.models import KonovaCode
from compensation.models import Compensation, CompensationState, CompensationAction, EcoAccount 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.management.commands.setup_data import GROUPS_DATA
from konova.models import Geometry from konova.models import Geometry
from konova.settings import DEFAULT_GROUP from konova.settings import DEFAULT_GROUP
from konova.utils.generators import generate_random_string from konova.utils.generators import generate_random_string
from user.models import UserActionLogEntry, UserAction from user.models import UserActionLogEntry
class BaseTestCase(TestCase): class BaseTestCase(TestCase):
@ -98,14 +98,11 @@ class BaseTestCase(TestCase):
""" """
# Create dummy data # Create dummy data
# Create log entry # Create log entry
action = UserActionLogEntry.objects.create( action = UserActionLogEntry.get_created_action(cls.superuser)
user=cls.superuser,
action=UserAction.CREATED,
)
# Create legal data object (without M2M laws first) # Create legal data object (without M2M laws first)
legal_data = LegalData.objects.create() legal_data = Legal.objects.create()
# Create responsible data object # Create responsible data object
responsibility_data = ResponsibilityData.objects.create() responsibility_data = Responsibility.objects.create()
geometry = Geometry.objects.create() geometry = Geometry.objects.create()
# Finally create main object, holding the other objects # Finally create main object, holding the other objects
intervention = Intervention.objects.create( intervention = Intervention.objects.create(
@ -131,10 +128,7 @@ class BaseTestCase(TestCase):
cls.intervention = cls.create_dummy_intervention() cls.intervention = cls.create_dummy_intervention()
# Create dummy data # Create dummy data
# Create log entry # Create log entry
action = UserActionLogEntry.objects.create( action = UserActionLogEntry.get_created_action(cls.superuser)
user=cls.superuser,
action=UserAction.CREATED,
)
geometry = Geometry.objects.create() geometry = Geometry.objects.create()
# Finally create main object, holding the other objects # Finally create main object, holding the other objects
compensation = Compensation.objects.create( compensation = Compensation.objects.create(
@ -156,14 +150,11 @@ class BaseTestCase(TestCase):
""" """
# Create dummy data # Create dummy data
# Create log entry # Create log entry
action = UserActionLogEntry.objects.create( action = UserActionLogEntry.get_created_action(cls.superuser)
user=cls.superuser,
action=UserAction.CREATED,
)
geometry = Geometry.objects.create() geometry = Geometry.objects.create()
# Create responsible data object # Create responsible data object
lega_data = LegalData.objects.create() lega_data = Legal.objects.create()
responsible_data = ResponsibilityData.objects.create() responsible_data = Responsibility.objects.create()
# Finally create main object, holding the other objects # Finally create main object, holding the other objects
eco_account = EcoAccount.objects.create( eco_account = EcoAccount.objects.create(
identifier="TEST", 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 import uuid
from django.utils.translation import gettext_lazy as _
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _
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="+")
class UserAction(models.TextChoices): class UserAction(models.TextChoices):
@ -96,3 +67,57 @@ class UserActionLogEntry(models.Model):
if choice[0] == self.action: if choice[0] == self.action:
return choice[1] return choice[1]
return None 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