mpeltriaux
edcf7b3c78
* adds tests for team sharing * extends the API for team sharing support * adds shared_teams property shortcut for ShareableObjectMixin * adds full support for team-based sharing to all views and functions * simplifies ShareModalForm * adds/updates translations
545 lines
18 KiB
Python
545 lines
18 KiB
Python
"""
|
|
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 import BaseModalForm, 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_sharing_user(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)
|
|
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.all()
|
|
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"]
|
|
|
|
if not acc.recorded:
|
|
self.add_error(
|
|
"account",
|
|
_("Eco-account {} is not recorded yet. You can only deduct from recorded accounts.").format(acc.identifier)
|
|
)
|
|
return 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 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)
|
|
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)
|
|
current_account = deduction.account
|
|
current_intervention = deduction.intervention
|
|
|
|
|
|
# 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 current_account != form_account:
|
|
current_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:
|
|
current_account.mark_as_edited(self.user, self.request, edit_comment=DEDUCTION_EDITED)
|
|
|
|
if current_intervention != form_intervention:
|
|
current_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:
|
|
current_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()
|
|
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
|