diff --git a/codelist/models.py b/codelist/models.py index 8e5de0c9..5d06172e 100644 --- a/codelist/models.py +++ b/codelist/models.py @@ -50,7 +50,7 @@ class KonovaCode(models.Model): def __str__(self, with_parent: bool = True): ret_val = "" - if self.parent and with_parent: + if self.parent and self.parent.long_name and with_parent: ret_val += self.parent.long_name + " > " ret_val += self.long_name if self.short_name and self.short_name != self.long_name: diff --git a/compensation/views/eco_account.py b/compensation/views/eco_account.py index d600c23c..07796d14 100644 --- a/compensation/views/eco_account.py +++ b/compensation/views/eco_account.py @@ -23,8 +23,9 @@ from compensation.forms.modals.state import NewCompensationStateModalForm, Remov EditCompensationStateModalForm from compensation.models import EcoAccount, EcoAccountDocument, CompensationState, CompensationAction from compensation.tables import EcoAccountTable -from intervention.forms.modalForms import NewDeductionModalForm, ShareModalForm, RemoveEcoAccountDeductionModalForm, \ +from intervention.forms.modals.deduction import RemoveEcoAccountDeductionModalForm, NewEcoAccountDeductionModalForm, \ EditEcoAccountDeductionModalForm +from intervention.forms.modals.share import ShareModalForm from konova.contexts import BaseContext from konova.decorators import any_group_check, default_group_required, conservation_office_group_required, \ shared_access_required @@ -707,7 +708,7 @@ def new_deduction_view(request: HttpRequest, id: str): acc = get_object_or_404(EcoAccount, id=id) if not acc.recorded: raise Http404() - form = NewDeductionModalForm(request.POST or None, instance=acc, request=request) + form = NewEcoAccountDeductionModalForm(request.POST or None, instance=acc, request=request) return form.process_request( request, msg_success=DEDUCTION_ADDED, diff --git a/ema/views.py b/ema/views.py index fea6d4bc..2dd6e51f 100644 --- a/ema/views.py +++ b/ema/views.py @@ -14,7 +14,7 @@ from compensation.forms.modals.state import NewCompensationStateModalForm, Remov from compensation.models import CompensationAction, CompensationState from ema.forms import NewEmaForm, EditEmaForm, NewEmaDocumentModalForm from ema.tables import EmaTable -from intervention.forms.modalForms import ShareModalForm +from intervention.forms.modals.share import ShareModalForm from konova.contexts import BaseContext from konova.decorators import conservation_office_group_required, shared_access_required from ema.models import Ema, EmaDocument diff --git a/intervention/forms/forms.py b/intervention/forms/intervention.py similarity index 100% rename from intervention/forms/forms.py rename to intervention/forms/intervention.py diff --git a/intervention/forms/modalForms.py b/intervention/forms/modalForms.py deleted file mode 100644 index a977c1ce..00000000 --- a/intervention/forms/modalForms.py +++ /dev/null @@ -1,574 +0,0 @@ -""" -Author: Michel Peltriaux -Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany -Contact: michel.peltriaux@sgdnord.rlp.de -Created on: 27.09.21 - -""" -from dal import autocomplete -from django.core.exceptions import ObjectDoesNotExist - -from konova.utils.message_templates import DEDUCTION_ADDED, REVOCATION_ADDED, DEDUCTION_REMOVED, DEDUCTION_EDITED, \ - REVOCATION_EDITED, ENTRY_REMOVE_MISSING_PERMISSION -from user.models import User, Team -from user.models import UserActionLogEntry -from django.db import transaction -from django import forms -from django.utils.translation import gettext_lazy as _ - -from compensation.models import EcoAccount, EcoAccountDeduction -from intervention.inputs import TextToClipboardInput -from intervention.models import Intervention, InterventionDocument, RevocationDocument -from konova.forms.modals import BaseModalForm -from konova.forms.modals import NewDocumentModalForm, RemoveModalForm -from konova.utils.general import format_german_float -from konova.utils.user_checks import is_default_group_only - - -class ShareModalForm(BaseModalForm): - url = forms.CharField( - label=_("Share link"), - label_suffix="", - help_text=_("Send this link to users who you want to have writing access on the data"), - required=False, - widget=TextToClipboardInput( - attrs={ - "readonly": True, - "class": "form-control", - } - ) - ) - teams = forms.ModelMultipleChoiceField( - label=_("Add team to share with"), - label_suffix="", - help_text=_("Multiple selection possible - You can only select teams which do not already have access."), - required=False, - queryset=Team.objects.all(), - widget=autocomplete.ModelSelect2Multiple( - url="share-team-autocomplete", - attrs={ - "data-placeholder": _("Click for selection"), - "data-minimum-input-length": 3, - }, - ), - ) - users = forms.ModelMultipleChoiceField( - label=_("Add user to share with"), - label_suffix="", - help_text=_("Multiple selection possible - You can only select users which do not already have access. Enter the full username."), - required=False, - queryset=User.objects.all(), - widget=autocomplete.ModelSelect2Multiple( - url="share-user-autocomplete", - attrs={ - "data-placeholder": _("Click for selection"), - "data-minimum-input-length": 3, - }, - ), - ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.form_title = _("Share") - self.form_caption = _("Share settings for {}").format(self.instance.identifier) - self.template = "modal/modal_form.html" - - # Make sure an access_token is set - if self.instance.access_token is None: - self.instance.generate_access_token() - - self._init_fields() - - def _user_team_valid(self): - """ Checks whether users and teams have been removed by the user and if the user is allowed to do so or not - - Returns: - - """ - users = self.cleaned_data.get("users", User.objects.none()) - teams = self.cleaned_data.get("teams", Team.objects.none()) - - _is_valid = True - if is_default_group_only(self.user): - shared_users = self.instance.shared_users - shared_teams = self.instance.shared_teams - - shared_users_are_removed = not set(shared_users).issubset(users) - shared_teams_are_removed = not set(shared_teams).issubset(teams) - - if shared_users_are_removed: - self.add_error( - "users", - ENTRY_REMOVE_MISSING_PERMISSION - ) - _is_valid = False - if shared_teams_are_removed: - self.add_error( - "teams", - ENTRY_REMOVE_MISSING_PERMISSION - ) - _is_valid = False - return _is_valid - - def is_valid(self): - """ Extended validity check - - Returns: - - """ - super_valid = super().is_valid() - user_team_valid = self._user_team_valid() - _is_valid = super_valid and user_team_valid - return _is_valid - - def _init_fields(self): - """ Wraps initializing of fields - - Returns: - - """ - # Initialize share_link field - share_link = self.instance.get_share_link() - self.share_link = self.request.build_absolute_uri(share_link) - self.initialize_form_field( - "url", - self.share_link - ) - - form_data = { - "teams": self.instance.teams.all(), - "users": self.instance.users.all(), - } - self.load_initial_data(form_data) - - def save(self): - self.instance.update_shared_access(self) - - -class NewRevocationModalForm(BaseModalForm): - date = forms.DateField( - label=_("Date"), - label_suffix=_(""), - help_text=_("Date of revocation"), - widget=forms.DateInput( - attrs={ - "type": "date", - "data-provide": "datepicker", - "class": "form-control", - }, - format="%d.%m.%Y" - ) - ) - file = forms.FileField( - label=_("Document"), - label_suffix=_(""), - required=False, - help_text=_("Must be smaller than 15 Mb"), - widget=forms.FileInput( - attrs={ - "class": "form-control-file" - } - ) - ) - comment = forms.CharField( - required=False, - max_length=200, - label=_("Comment"), - label_suffix=_(""), - help_text=_("Additional comment, maximum {} letters").format(200), - widget=forms.Textarea( - attrs={ - "cols": 30, - "rows": 5, - "class": "form-control", - } - ) - ) - document_model = RevocationDocument - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.form_title = _("Add revocation") - self.form_caption = "" - self.form_attrs = { - "enctype": "multipart/form-data", # important for file upload - } - - def save(self): - revocation = self.instance.add_revocation(self) - self.instance.mark_as_edited(self.user, self.request, edit_comment=REVOCATION_ADDED) - return revocation - - -class EditRevocationModalForm(NewRevocationModalForm): - revocation = None - - def __init__(self, *args, **kwargs): - self.revocation = kwargs.pop("revocation", None) - super().__init__(*args, **kwargs) - self.form_title = _("Edit revocation") - try: - doc = self.revocation.document.file - except ObjectDoesNotExist: - doc = None - form_data = { - "date": str(self.revocation.date), - "file": doc, - "comment": self.revocation.comment, - } - self.load_initial_data(form_data) - - def save(self): - revocation = self.instance.edit_revocation(self) - self.instance.mark_as_edited(self.user, self.request, edit_comment=REVOCATION_EDITED) - return revocation - - -class RemoveRevocationModalForm(RemoveModalForm): - """ Removing modal form for Revocation - - Can be used for anything, where removing shall be confirmed by the user a second time. - - """ - revocation = None - - def __init__(self, *args, **kwargs): - revocation = kwargs.pop("revocation", None) - self.revocation = revocation - super().__init__(*args, **kwargs) - - def save(self): - self.instance.remove_revocation(self) - - -class CheckModalForm(BaseModalForm): - """ The modal form for running a check on interventions and their compensations - - """ - checked_intervention = forms.BooleanField( - label=_("Checked intervention data"), - label_suffix="", - widget=forms.CheckboxInput(), - required=True, - ) - checked_comps = forms.BooleanField( - label=_("Checked compensations data and payments"), - label_suffix="", - widget=forms.CheckboxInput(), - required=True - ) - valid = None - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.form_title = _("Run check") - self.form_caption = _("I, {} {}, confirm that all necessary control steps have been performed by myself.").format(self.user.first_name, self.user.last_name) - self.valid = False - - def _are_deductions_valid(self): - """ Performs validity checks on deductions and their eco-account - - Returns: - - """ - deductions = self.instance.deductions.all() - for deduction in deductions: - checker = deduction.account.quality_check() - for msg in checker.messages: - self.add_error( - "checked_comps", - f"{deduction.account.identifier}: {msg}" - ) - return checker.valid - return True - - def _are_comps_valid(self): - """ Performs validity checks on all types of compensations - - Types of compensations are - * regular Compensations - * deductions from EcoAccounts - - Returns: - - """ - comps = self.instance.compensations.filter( - deleted=None, - ) - comps_valid = True - for comp in comps: - checker = comp.quality_check() - for msg in checker.messages: - self.add_error( - "checked_comps", - f"{comp.identifier}: {msg}" - ) - comps_valid = checker.valid - deductions_valid = self._are_deductions_valid() - return deductions_valid and comps_valid - - def is_valid(self) -> bool: - """ Perform a validity check based on quality_check() logic - - Returns: - result (bool) - """ - super_valid = super().is_valid() - # Perform check - checker = self.instance.quality_check() - for msg in checker.messages: - self.add_error( - "checked_intervention", - msg - ) - all_comps_valid = self._are_comps_valid() - intervention_valid = checker.valid - - return super_valid and intervention_valid and all_comps_valid - - def save(self): - """ Saving logic - - Returns: - - """ - with transaction.atomic(): - self.instance.set_checked(self.user) - - -class NewDeductionModalForm(BaseModalForm): - """ Form for creating new deduction - - Can be used for Intervention view as well as for EcoAccount views. - - Parameter 'instance' can be an intervention, as well as an ecoAccount. - An instance check handles both workflows properly. - - """ - account = forms.ModelChoiceField( - label=_("Eco-account"), - label_suffix="", - help_text=_("Only recorded accounts can be selected for deductions"), - queryset=EcoAccount.objects.filter(deleted=None), - widget=autocomplete.ModelSelect2( - url="accounts-autocomplete", - attrs={ - "data-placeholder": _("Eco-account"), - "data-minimum-input-length": 3, - "readonly": True, - } - ), - ) - surface = forms.DecimalField( - min_value=0.00, - decimal_places=2, - label=_("Surface"), - label_suffix="", - help_text=_("in m²"), - widget=forms.NumberInput( - attrs={ - "class": "form-control", - "placeholder": "0,00", - } - ) - ) - intervention = forms.ModelChoiceField( - label=_("Intervention"), - label_suffix="", - help_text=_("Only shared interventions can be selected"), - queryset=Intervention.objects.filter(deleted=None), - widget=autocomplete.ModelSelect2( - url="interventions-autocomplete", - attrs={ - "data-placeholder": _("Intervention"), - "data-minimum-input-length": 3, - } - ), - ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.form_title = _("New Deduction") - self.form_caption = _("Enter the information for a new deduction from a chosen eco-account") - - # Check for Intervention or EcoAccount - if isinstance(self.instance, Intervention): - # Form has been called with a given intervention - self.initialize_form_field("intervention", self.instance) - self.disable_form_field("intervention") - elif isinstance(self.instance, EcoAccount): - # Form has been called with a given account --> make it initial in the form and read-only - self.initialize_form_field("account", self.instance) - self.disable_form_field("account") - else: - raise NotImplementedError - - def _get_available_surface(self, acc): - """ Calculates how much available surface is left on the account - - Args: - acc (EcoAccount): - - Returns: - - """ - # Calculate valid surface - deductable_surface = acc.deductable_surface - sum_surface_deductions = acc.get_deductions_surface() - rest_surface = deductable_surface - sum_surface_deductions - return rest_surface - - def is_valid(self): - """ Custom validity check - - Makes sure the deduction can not contain more surface than the account still provides - - Returns: - is_valid (bool) - """ - super_result = super().is_valid() - acc = self.cleaned_data["account"] - intervention = self.cleaned_data["intervention"] - objects_valid = True - - if not acc.recorded: - self.add_error( - "account", - _("Eco-account {} is not recorded yet. You can only deduct from recorded accounts.").format(acc.identifier) - ) - objects_valid = False - - if intervention.is_recorded: - self.add_error( - "intervention", - _("Intervention {} is currently recorded. To change any data on it, the entry must be unrecorded.").format(intervention.identifier) - ) - objects_valid = False - - rest_surface = self._get_available_surface(acc) - form_surface = float(self.cleaned_data["surface"]) - is_valid_surface = form_surface <= rest_surface - if not is_valid_surface: - self.add_error( - "surface", - _("The account {} has not enough surface for a deduction of {} m². There are only {} m² left").format( - acc.identifier, - format_german_float(form_surface), - format_german_float(rest_surface), - ), - ) - return is_valid_surface and objects_valid and super_result - - def __create_deduction(self): - """ Creates the deduction - - Returns: - - """ - with transaction.atomic(): - user_action_create = UserActionLogEntry.get_created_action(self.user) - deduction = EcoAccountDeduction.objects.create( - intervention=self.cleaned_data["intervention"], - account=self.cleaned_data["account"], - surface=self.cleaned_data["surface"], - created=user_action_create, - ) - return deduction - - def save(self): - deduction = self.__create_deduction() - self.cleaned_data["intervention"].mark_as_edited(self.user, edit_comment=DEDUCTION_ADDED) - self.cleaned_data["account"].mark_as_edited(self.user, edit_comment=DEDUCTION_ADDED) - return deduction - - -class EditEcoAccountDeductionModalForm(NewDeductionModalForm): - deduction = None - - def __init__(self, *args, **kwargs): - self.deduction = kwargs.pop("deduction", None) - super().__init__(*args, **kwargs) - self.form_title = _("Edit Deduction") - form_data = { - "account": self.deduction.account, - "intervention": self.deduction.intervention, - "surface": self.deduction.surface, - } - self.load_initial_data(form_data) - - def _get_available_surface(self, acc): - rest_surface = super()._get_available_surface(acc) - # Increase available surface by the currently deducted surface, so we can 'deduct' the same amount again or - # increase the surface only a little, which will still be valid. - # Example: 200 m² left, 500 m² deducted. Entering 700 m² would fail if we would not add the 500 m² to the available - # surface again. - rest_surface += self.deduction.surface - return rest_surface - - def save(self): - deduction = self.deduction - form_account = self.cleaned_data.get("account", None) - form_intervention = self.cleaned_data.get("intervention", None) - old_account = deduction.account - old_intervention = deduction.intervention - old_surface = deduction.surface - - # If account or intervention has been changed, we put that change in the logs just as if the deduction has - # been removed for this entry. Act as if the deduction is newly created for the new entries - if old_account != form_account: - old_account.mark_as_edited(self.user, self.request, edit_comment=DEDUCTION_REMOVED) - form_account.mark_as_edited(self.user, self.request, edit_comment=DEDUCTION_ADDED) - else: - old_account.mark_as_edited(self.user, self.request, edit_comment=DEDUCTION_EDITED) - - if old_intervention != form_intervention: - old_intervention.mark_as_edited(self.user, self.request, edit_comment=DEDUCTION_REMOVED) - form_intervention.mark_as_edited(self.user, self.request, edit_comment=DEDUCTION_ADDED) - else: - old_intervention.mark_as_edited(self.user, self.request, edit_comment=DEDUCTION_EDITED) - - deduction.account = form_account - deduction.intervention = self.cleaned_data.get("intervention", None) - deduction.surface = self.cleaned_data.get("surface", None) - deduction.save() - - data_changes = { - "surface": { - "old": old_surface, - "new": deduction.surface, - }, - "intervention": { - "old": old_intervention.identifier, - "new": deduction.intervention.identifier, - }, - "account": { - "old": old_account.identifier, - "new": deduction.account.identifier, - } - } - old_account.send_notification_mail_on_deduction_change(data_changes) - return deduction - - -class RemoveEcoAccountDeductionModalForm(RemoveModalForm): - """ Removing modal form for EcoAccountDeduction - - Can be used for anything, where removing shall be confirmed by the user a second time. - - """ - deduction = None - - def __init__(self, *args, **kwargs): - deduction = kwargs.pop("deduction", None) - self.deduction = deduction - super().__init__(*args, **kwargs) - - def save(self): - with transaction.atomic(): - self.deduction.intervention.mark_as_edited(self.user, edit_comment=DEDUCTION_REMOVED) - self.deduction.account.mark_as_edited(self.user, edit_comment=DEDUCTION_REMOVED) - self.deduction.delete() - - -class NewInterventionDocumentModalForm(NewDocumentModalForm): - document_model = InterventionDocument diff --git a/intervention/forms/modals/__init__.py b/intervention/forms/modals/__init__.py new file mode 100644 index 00000000..ca978536 --- /dev/null +++ b/intervention/forms/modals/__init__.py @@ -0,0 +1,7 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 18.08.22 + +""" diff --git a/intervention/forms/modals/check.py b/intervention/forms/modals/check.py new file mode 100644 index 00000000..8e2a551e --- /dev/null +++ b/intervention/forms/modals/check.py @@ -0,0 +1,107 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 18.08.22 + +""" +from django import forms +from django.db import transaction +from django.utils.translation import gettext_lazy as _ + +from konova.forms.modals import BaseModalForm + + +class CheckModalForm(BaseModalForm): + """ The modal form for running a check on interventions and their compensations + + """ + checked_intervention = forms.BooleanField( + label=_("Checked intervention data"), + label_suffix="", + widget=forms.CheckboxInput(), + required=True, + ) + checked_comps = forms.BooleanField( + label=_("Checked compensations data and payments"), + label_suffix="", + widget=forms.CheckboxInput(), + required=True + ) + valid = None + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.form_title = _("Run check") + self.form_caption = _("I, {} {}, confirm that all necessary control steps have been performed by myself.").format(self.user.first_name, self.user.last_name) + self.valid = False + + def _are_deductions_valid(self): + """ Performs validity checks on deductions and their eco-account + + Returns: + + """ + deductions = self.instance.deductions.all() + for deduction in deductions: + checker = deduction.account.quality_check() + for msg in checker.messages: + self.add_error( + "checked_comps", + f"{deduction.account.identifier}: {msg}" + ) + return checker.valid + return True + + def _are_comps_valid(self): + """ Performs validity checks on all types of compensations + + Types of compensations are + * regular Compensations + * deductions from EcoAccounts + + Returns: + + """ + comps = self.instance.compensations.filter( + deleted=None, + ) + comps_valid = True + for comp in comps: + checker = comp.quality_check() + for msg in checker.messages: + self.add_error( + "checked_comps", + f"{comp.identifier}: {msg}" + ) + comps_valid = checker.valid + deductions_valid = self._are_deductions_valid() + return deductions_valid and comps_valid + + def is_valid(self) -> bool: + """ Perform a validity check based on quality_check() logic + + Returns: + result (bool) + """ + super_valid = super().is_valid() + # Perform check + checker = self.instance.quality_check() + for msg in checker.messages: + self.add_error( + "checked_intervention", + msg + ) + all_comps_valid = self._are_comps_valid() + intervention_valid = checker.valid + + return super_valid and intervention_valid and all_comps_valid + + def save(self): + """ Saving logic + + Returns: + + """ + with transaction.atomic(): + self.instance.set_checked(self.user) diff --git a/intervention/forms/modals/deduction.py b/intervention/forms/modals/deduction.py new file mode 100644 index 00000000..2e182146 --- /dev/null +++ b/intervention/forms/modals/deduction.py @@ -0,0 +1,252 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 18.08.22 + +""" +from dal import autocomplete +from django import forms +from django.db import transaction +from django.utils.translation import gettext_lazy as _ + +from compensation.models import EcoAccount, EcoAccountDeduction +from intervention.models import Intervention +from konova.forms.modals import BaseModalForm, RemoveModalForm +from konova.utils.general import format_german_float +from konova.utils.message_templates import DEDUCTION_ADDED, DEDUCTION_REMOVED, DEDUCTION_EDITED +from user.models import UserActionLogEntry + + +class NewEcoAccountDeductionModalForm(BaseModalForm): + """ Form for creating new deduction + + Can be used for Intervention view as well as for EcoAccount views. + + Parameter 'instance' can be an intervention, as well as an ecoAccount. + An instance check handles both workflows properly. + + """ + account = forms.ModelChoiceField( + label=_("Eco-account"), + label_suffix="", + help_text=_("Only recorded accounts can be selected for deductions"), + queryset=EcoAccount.objects.filter(deleted=None), + widget=autocomplete.ModelSelect2( + url="accounts-autocomplete", + attrs={ + "data-placeholder": _("Eco-account"), + "data-minimum-input-length": 3, + "readonly": True, + } + ), + ) + surface = forms.DecimalField( + min_value=0.00, + decimal_places=2, + label=_("Surface"), + label_suffix="", + help_text=_("in m²"), + widget=forms.NumberInput( + attrs={ + "class": "form-control", + "placeholder": "0,00", + } + ) + ) + intervention = forms.ModelChoiceField( + label=_("Intervention"), + label_suffix="", + help_text=_("Only shared interventions can be selected"), + queryset=Intervention.objects.filter(deleted=None), + widget=autocomplete.ModelSelect2( + url="interventions-autocomplete", + attrs={ + "data-placeholder": _("Intervention"), + "data-minimum-input-length": 3, + } + ), + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.form_title = _("New Deduction") + self.form_caption = _("Enter the information for a new deduction from a chosen eco-account") + + # Check for Intervention or EcoAccount + if isinstance(self.instance, Intervention): + # Form has been called with a given intervention + self.initialize_form_field("intervention", self.instance) + self.disable_form_field("intervention") + elif isinstance(self.instance, EcoAccount): + # Form has been called with a given account --> make it initial in the form and read-only + self.initialize_form_field("account", self.instance) + self.disable_form_field("account") + else: + raise NotImplementedError + + def _get_available_surface(self, acc): + """ Calculates how much available surface is left on the account + + Args: + acc (EcoAccount): + + Returns: + + """ + # Calculate valid surface + deductable_surface = acc.deductable_surface + sum_surface_deductions = acc.get_deductions_surface() + rest_surface = deductable_surface - sum_surface_deductions + return rest_surface + + def is_valid(self): + """ Custom validity check + + Makes sure the deduction can not contain more surface than the account still provides + + Returns: + is_valid (bool) + """ + super_result = super().is_valid() + acc = self.cleaned_data["account"] + intervention = self.cleaned_data["intervention"] + objects_valid = True + + if not acc.recorded: + self.add_error( + "account", + _("Eco-account {} is not recorded yet. You can only deduct from recorded accounts.").format(acc.identifier) + ) + objects_valid = False + + if intervention.is_recorded: + self.add_error( + "intervention", + _("Intervention {} is currently recorded. To change any data on it, the entry must be unrecorded.").format(intervention.identifier) + ) + objects_valid = False + + rest_surface = self._get_available_surface(acc) + form_surface = float(self.cleaned_data["surface"]) + is_valid_surface = form_surface <= rest_surface + if not is_valid_surface: + self.add_error( + "surface", + _("The account {} has not enough surface for a deduction of {} m². There are only {} m² left").format( + acc.identifier, + format_german_float(form_surface), + format_german_float(rest_surface), + ), + ) + return is_valid_surface and objects_valid and super_result + + def __create_deduction(self): + """ Creates the deduction + + Returns: + + """ + with transaction.atomic(): + user_action_create = UserActionLogEntry.get_created_action(self.user) + deduction = EcoAccountDeduction.objects.create( + intervention=self.cleaned_data["intervention"], + account=self.cleaned_data["account"], + surface=self.cleaned_data["surface"], + created=user_action_create, + ) + return deduction + + def save(self): + deduction = self.__create_deduction() + self.cleaned_data["intervention"].mark_as_edited(self.user, edit_comment=DEDUCTION_ADDED) + self.cleaned_data["account"].mark_as_edited(self.user, edit_comment=DEDUCTION_ADDED) + return deduction + + +class EditEcoAccountDeductionModalForm(NewEcoAccountDeductionModalForm): + deduction = None + + def __init__(self, *args, **kwargs): + self.deduction = kwargs.pop("deduction", None) + super().__init__(*args, **kwargs) + self.form_title = _("Edit Deduction") + form_data = { + "account": self.deduction.account, + "intervention": self.deduction.intervention, + "surface": self.deduction.surface, + } + self.load_initial_data(form_data) + + def _get_available_surface(self, acc): + rest_surface = super()._get_available_surface(acc) + # Increase available surface by the currently deducted surface, so we can 'deduct' the same amount again or + # increase the surface only a little, which will still be valid. + # Example: 200 m² left, 500 m² deducted. Entering 700 m² would fail if we would not add the 500 m² to the available + # surface again. + rest_surface += self.deduction.surface + return rest_surface + + def save(self): + deduction = self.deduction + form_account = self.cleaned_data.get("account", None) + form_intervention = self.cleaned_data.get("intervention", None) + old_account = deduction.account + old_intervention = deduction.intervention + old_surface = deduction.surface + + # If account or intervention has been changed, we put that change in the logs just as if the deduction has + # been removed for this entry. Act as if the deduction is newly created for the new entries + if old_account != form_account: + old_account.mark_as_edited(self.user, self.request, edit_comment=DEDUCTION_REMOVED) + form_account.mark_as_edited(self.user, self.request, edit_comment=DEDUCTION_ADDED) + else: + old_account.mark_as_edited(self.user, self.request, edit_comment=DEDUCTION_EDITED) + + if old_intervention != form_intervention: + old_intervention.mark_as_edited(self.user, self.request, edit_comment=DEDUCTION_REMOVED) + form_intervention.mark_as_edited(self.user, self.request, edit_comment=DEDUCTION_ADDED) + else: + old_intervention.mark_as_edited(self.user, self.request, edit_comment=DEDUCTION_EDITED) + + deduction.account = form_account + deduction.intervention = self.cleaned_data.get("intervention", None) + deduction.surface = self.cleaned_data.get("surface", None) + deduction.save() + + data_changes = { + "surface": { + "old": old_surface, + "new": deduction.surface, + }, + "intervention": { + "old": old_intervention.identifier, + "new": deduction.intervention.identifier, + }, + "account": { + "old": old_account.identifier, + "new": deduction.account.identifier, + } + } + old_account.send_notification_mail_on_deduction_change(data_changes) + return deduction + + +class RemoveEcoAccountDeductionModalForm(RemoveModalForm): + """ Removing modal form for EcoAccountDeduction + + Can be used for anything, where removing shall be confirmed by the user a second time. + + """ + deduction = None + + def __init__(self, *args, **kwargs): + deduction = kwargs.pop("deduction", None) + self.deduction = deduction + super().__init__(*args, **kwargs) + + def save(self): + with transaction.atomic(): + self.deduction.intervention.mark_as_edited(self.user, edit_comment=DEDUCTION_REMOVED) + self.deduction.account.mark_as_edited(self.user, edit_comment=DEDUCTION_REMOVED) + self.deduction.delete() \ No newline at end of file diff --git a/intervention/forms/modals/document.py b/intervention/forms/modals/document.py new file mode 100644 index 00000000..212f3f02 --- /dev/null +++ b/intervention/forms/modals/document.py @@ -0,0 +1,13 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 18.08.22 + +""" +from intervention.models import InterventionDocument +from konova.forms.modals import NewDocumentModalForm + + +class NewInterventionDocumentModalForm(NewDocumentModalForm): + document_model = InterventionDocument \ No newline at end of file diff --git a/intervention/forms/modals/revocation.py b/intervention/forms/modals/revocation.py new file mode 100644 index 00000000..b738efe6 --- /dev/null +++ b/intervention/forms/modals/revocation.py @@ -0,0 +1,110 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 18.08.22 + +""" +from django import forms +from django.core.exceptions import ObjectDoesNotExist +from django.utils.translation import gettext_lazy as _ + +from intervention.models import RevocationDocument +from konova.forms.modals import BaseModalForm, RemoveModalForm +from konova.utils.message_templates import REVOCATION_ADDED, REVOCATION_EDITED + + +class NewRevocationModalForm(BaseModalForm): + date = forms.DateField( + label=_("Date"), + label_suffix=_(""), + help_text=_("Date of revocation"), + widget=forms.DateInput( + attrs={ + "type": "date", + "data-provide": "datepicker", + "class": "form-control", + }, + format="%d.%m.%Y" + ) + ) + file = forms.FileField( + label=_("Document"), + label_suffix=_(""), + required=False, + help_text=_("Must be smaller than 15 Mb"), + widget=forms.FileInput( + attrs={ + "class": "form-control-file" + } + ) + ) + comment = forms.CharField( + required=False, + max_length=200, + label=_("Comment"), + label_suffix=_(""), + help_text=_("Additional comment, maximum {} letters").format(200), + widget=forms.Textarea( + attrs={ + "cols": 30, + "rows": 5, + "class": "form-control", + } + ) + ) + document_model = RevocationDocument + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.form_title = _("Add revocation") + self.form_caption = "" + self.form_attrs = { + "enctype": "multipart/form-data", # important for file upload + } + + def save(self): + revocation = self.instance.add_revocation(self) + self.instance.mark_as_edited(self.user, self.request, edit_comment=REVOCATION_ADDED) + return revocation + + +class EditRevocationModalForm(NewRevocationModalForm): + revocation = None + + def __init__(self, *args, **kwargs): + self.revocation = kwargs.pop("revocation", None) + super().__init__(*args, **kwargs) + self.form_title = _("Edit revocation") + try: + doc = self.revocation.document.file + except ObjectDoesNotExist: + doc = None + form_data = { + "date": str(self.revocation.date), + "file": doc, + "comment": self.revocation.comment, + } + self.load_initial_data(form_data) + + def save(self): + revocation = self.instance.edit_revocation(self) + self.instance.mark_as_edited(self.user, self.request, edit_comment=REVOCATION_EDITED) + return revocation + + +class RemoveRevocationModalForm(RemoveModalForm): + """ Removing modal form for Revocation + + Can be used for anything, where removing shall be confirmed by the user a second time. + + """ + revocation = None + + def __init__(self, *args, **kwargs): + revocation = kwargs.pop("revocation", None) + self.revocation = revocation + super().__init__(*args, **kwargs) + + def save(self): + self.instance.remove_revocation(self) diff --git a/intervention/forms/modals/share.py b/intervention/forms/modals/share.py new file mode 100644 index 00000000..39c9d142 --- /dev/null +++ b/intervention/forms/modals/share.py @@ -0,0 +1,136 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 18.08.22 + +""" +from dal import autocomplete +from django import forms +from django.utils.translation import gettext_lazy as _ + +from intervention.inputs import TextToClipboardInput +from konova.forms.modals import BaseModalForm +from konova.utils.message_templates import ENTRY_REMOVE_MISSING_PERMISSION +from konova.utils.user_checks import is_default_group_only +from user.models import Team, User + + +class ShareModalForm(BaseModalForm): + url = forms.CharField( + label=_("Share link"), + label_suffix="", + help_text=_("Send this link to users who you want to have writing access on the data"), + required=False, + widget=TextToClipboardInput( + attrs={ + "readonly": True, + "class": "form-control", + } + ) + ) + teams = forms.ModelMultipleChoiceField( + label=_("Add team to share with"), + label_suffix="", + help_text=_("Multiple selection possible - You can only select teams which do not already have access."), + required=False, + queryset=Team.objects.all(), + widget=autocomplete.ModelSelect2Multiple( + url="share-team-autocomplete", + attrs={ + "data-placeholder": _("Click for selection"), + "data-minimum-input-length": 3, + }, + ), + ) + users = forms.ModelMultipleChoiceField( + label=_("Add user to share with"), + label_suffix="", + help_text=_("Multiple selection possible - You can only select users which do not already have access. Enter the full username."), + required=False, + queryset=User.objects.all(), + widget=autocomplete.ModelSelect2Multiple( + url="share-user-autocomplete", + attrs={ + "data-placeholder": _("Click for selection"), + "data-minimum-input-length": 3, + }, + ), + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.form_title = _("Share") + self.form_caption = _("Share settings for {}").format(self.instance.identifier) + self.template = "modal/modal_form.html" + + # Make sure an access_token is set + if self.instance.access_token is None: + self.instance.generate_access_token() + + self._init_fields() + + def _user_team_valid(self): + """ Checks whether users and teams have been removed by the user and if the user is allowed to do so or not + + Returns: + + """ + users = self.cleaned_data.get("users", User.objects.none()) + teams = self.cleaned_data.get("teams", Team.objects.none()) + + _is_valid = True + if is_default_group_only(self.user): + shared_users = self.instance.shared_users + shared_teams = self.instance.shared_teams + + shared_users_are_removed = not set(shared_users).issubset(users) + shared_teams_are_removed = not set(shared_teams).issubset(teams) + + if shared_users_are_removed: + self.add_error( + "users", + ENTRY_REMOVE_MISSING_PERMISSION + ) + _is_valid = False + if shared_teams_are_removed: + self.add_error( + "teams", + ENTRY_REMOVE_MISSING_PERMISSION + ) + _is_valid = False + return _is_valid + + def is_valid(self): + """ Extended validity check + + Returns: + + """ + super_valid = super().is_valid() + user_team_valid = self._user_team_valid() + _is_valid = super_valid and user_team_valid + return _is_valid + + def _init_fields(self): + """ Wraps initializing of fields + + Returns: + + """ + # Initialize share_link field + share_link = self.instance.get_share_link() + self.share_link = self.request.build_absolute_uri(share_link) + self.initialize_form_field( + "url", + self.share_link + ) + + form_data = { + "teams": self.instance.teams.all(), + "users": self.instance.users.all(), + } + self.load_initial_data(form_data) + + def save(self): + self.instance.update_shared_access(self) diff --git a/intervention/views.py b/intervention/views.py index 6a9304e9..e622488f 100644 --- a/intervention/views.py +++ b/intervention/views.py @@ -4,10 +4,14 @@ from django.utils.translation import gettext_lazy as _ from django.http import HttpRequest, JsonResponse, Http404 from django.shortcuts import render -from intervention.forms.forms import NewInterventionForm, EditInterventionForm -from intervention.forms.modalForms import ShareModalForm, NewRevocationModalForm, \ - CheckModalForm, NewDeductionModalForm, NewInterventionDocumentModalForm, RemoveEcoAccountDeductionModalForm, \ - RemoveRevocationModalForm, EditEcoAccountDeductionModalForm, EditRevocationModalForm +from intervention.forms.intervention import NewInterventionForm, EditInterventionForm +from intervention.forms.modals.check import CheckModalForm +from intervention.forms.modals.deduction import NewEcoAccountDeductionModalForm, RemoveEcoAccountDeductionModalForm, \ + EditEcoAccountDeductionModalForm +from intervention.forms.modals.document import NewInterventionDocumentModalForm +from intervention.forms.modals.revocation import EditRevocationModalForm, RemoveRevocationModalForm, \ + NewRevocationModalForm +from intervention.forms.modals.share import ShareModalForm from intervention.models import Intervention, Revocation, InterventionDocument, RevocationDocument from intervention.tables import InterventionTable from konova.contexts import BaseContext @@ -583,7 +587,7 @@ def new_deduction_view(request: HttpRequest, id: str): """ intervention = get_object_or_404(Intervention, id=id) - form = NewDeductionModalForm(request.POST or None, instance=intervention, request=request) + form = NewEcoAccountDeductionModalForm(request.POST or None, instance=intervention, request=request) return form.process_request( request, msg_success=DEDUCTION_ADDED, diff --git a/konova/forms/base_form.py b/konova/forms/base_form.py index 065fba17..fb69999c 100644 --- a/konova/forms/base_form.py +++ b/konova/forms/base_form.py @@ -134,7 +134,7 @@ class BaseForm(forms.Form): Returns: """ - from intervention.forms.modalForms import NewDeductionModalForm, EditEcoAccountDeductionModalForm, \ + from intervention.forms.modals.deduction import NewEcoAccountDeductionModalForm, EditEcoAccountDeductionModalForm, \ RemoveEcoAccountDeductionModalForm from konova.forms.modals.resubmission_form import ResubmissionModalForm is_none = self.instance is None @@ -142,7 +142,7 @@ class BaseForm(forms.Form): is_deduction_form_from_account = isinstance( self, ( - NewDeductionModalForm, + NewEcoAccountDeductionModalForm, ResubmissionModalForm, EditEcoAccountDeductionModalForm, RemoveEcoAccountDeductionModalForm,