Merge pull request '86_User_suggestions_and_feedback' (#111) from 86_User_suggestions_and_feedback into master

Reviewed-on: SGD-Nord/konova#111
This commit is contained in:
mpeltriaux 2022-02-10 13:31:55 +01:00
commit a26ab9cc3a
85 changed files with 3657 additions and 1367 deletions

View File

@ -12,15 +12,17 @@ class BaseAPIV1TestCase(BaseTestCase):
def setUpTestData(cls): def setUpTestData(cls):
super().setUpTestData() super().setUpTestData()
cls.superuser.get_API_token() def setUp(self) -> None:
cls.superuser.api_token.is_active = True super().setUp()
cls.superuser.api_token.save() self.superuser.get_API_token()
default_group = cls.groups.get(name=DEFAULT_GROUP) self.superuser.api_token.is_active = True
cls.superuser.groups.add(default_group) self.superuser.api_token.save()
default_group = self.groups.get(name=DEFAULT_GROUP)
self.superuser.groups.add(default_group)
cls.header_data = { self.header_data = {
"HTTP_ksptoken": cls.superuser.api_token.token, "HTTP_ksptoken": self.superuser.api_token.token,
"HTTP_kspuser": cls.superuser.username, "HTTP_kspuser": self.superuser.username,
} }

View File

@ -18,10 +18,10 @@ from codelist.settings import CODELIST_BIOTOPES_ID, CODELIST_COMPENSATION_ACTION
CODELIST_COMPENSATION_ACTION_DETAIL_ID CODELIST_COMPENSATION_ACTION_DETAIL_ID
from compensation.models import CompensationDocument, EcoAccountDocument from compensation.models import CompensationDocument, EcoAccountDocument
from konova.contexts import BaseContext from konova.contexts import BaseContext
from konova.forms import BaseModalForm, NewDocumentForm from konova.forms import BaseModalForm, NewDocumentModalForm, RemoveModalForm
from konova.models import DeadlineType from konova.models import DeadlineType
from konova.utils.message_templates import FORM_INVALID, ADDED_COMPENSATION_STATE, ADDED_DEADLINE, \ from konova.utils.message_templates import FORM_INVALID, ADDED_COMPENSATION_STATE, ADDED_DEADLINE, \
ADDED_COMPENSATION_ACTION, PAYMENT_ADDED ADDED_COMPENSATION_ACTION, PAYMENT_EDITED, COMPENSATION_STATE_EDITED, COMPENSATION_ACTION_EDITED, DEADLINE_EDITED
class NewPaymentForm(BaseModalForm): class NewPaymentForm(BaseModalForm):
@ -100,10 +100,52 @@ class NewPaymentForm(BaseModalForm):
def save(self): def save(self):
pay = self.instance.add_payment(self) pay = self.instance.add_payment(self)
self.instance.mark_as_edited(self.user, self.request, edit_comment=PAYMENT_ADDED)
return pay return pay
class EditPaymentModalForm(NewPaymentForm):
""" Form handling edit for Payment
"""
payment = None
def __init__(self, *args, **kwargs):
self.payment = kwargs.pop("payment", None)
super().__init__(*args, **kwargs)
form_date = {
"amount": self.payment.amount,
"due": str(self.payment.due_on),
"comment": self.payment.comment,
}
self.load_initial_data(form_date, disabled_fields=[])
def save(self):
payment = self.payment
payment.amount = self.cleaned_data.get("amount", None)
payment.due_on = self.cleaned_data.get("due", None)
payment.comment = self.cleaned_data.get("comment", None)
payment.save()
self.instance.mark_as_edited(self.user, self.request, edit_comment=PAYMENT_EDITED)
return payment
class RemovePaymentModalForm(RemoveModalForm):
""" Removing modal form for Payment
Can be used for anything, where removing shall be confirmed by the user a second time.
"""
payment = None
def __init__(self, *args, **kwargs):
payment = kwargs.pop("payment", None)
self.payment = payment
super().__init__(*args, **kwargs)
def save(self):
self.instance.remove_payment(self)
class NewStateModalForm(BaseModalForm): class NewStateModalForm(BaseModalForm):
""" Form handling state related input """ Form handling state related input
@ -219,6 +261,63 @@ class NewStateModalForm(BaseModalForm):
raise NotImplementedError raise NotImplementedError
class EditCompensationStateModalForm(NewStateModalForm):
state = None
def __init__(self, *args, **kwargs):
self.state = kwargs.pop("state", None)
super().__init__(*args, **kwargs)
form_data = {
"biotope_type": self.state.biotope_type,
"biotope_extra": self.state.biotope_type_details.all(),
"surface": self.state.surface,
}
self.load_initial_data(form_data)
def save(self, is_before_state: bool = False):
state = self.state
state.biotope_type = self.cleaned_data.get("biotope_type", None)
state.biotope_type_details.set(self.cleaned_data.get("biotope_extra", []))
state.surface = self.cleaned_data.get("surface", None)
state.save()
self.instance.mark_as_edited(self.user, self.request, edit_comment=COMPENSATION_STATE_EDITED)
return state
class RemoveCompensationStateModalForm(RemoveModalForm):
""" Removing modal form for CompensationState
Can be used for anything, where removing shall be confirmed by the user a second time.
"""
state = None
def __init__(self, *args, **kwargs):
state = kwargs.pop("state", None)
self.state = state
super().__init__(*args, **kwargs)
def save(self):
self.instance.remove_state(self)
class RemoveCompensationActionModalForm(RemoveModalForm):
""" Removing modal form for CompensationAction
Can be used for anything, where removing shall be confirmed by the user a second time.
"""
action = None
def __init__(self, *args, **kwargs):
action = kwargs.pop("action", None)
self.action = action
super().__init__(*args, **kwargs)
def save(self):
self.instance.remove_action(self)
class NewDeadlineModalForm(BaseModalForm): class NewDeadlineModalForm(BaseModalForm):
""" Form handling deadline related input """ Form handling deadline related input
@ -271,7 +370,29 @@ class NewDeadlineModalForm(BaseModalForm):
def save(self): def save(self):
deadline = self.instance.add_deadline(self) deadline = self.instance.add_deadline(self)
self.instance.mark_as_edited(self.user, self.request, ADDED_DEADLINE) return deadline
class EditDeadlineModalForm(NewDeadlineModalForm):
deadline = None
def __init__(self, *args, **kwargs):
self.deadline = kwargs.pop("deadline", None)
super().__init__(*args, **kwargs)
form_data = {
"type": self.deadline.type,
"date": str(self.deadline.date),
"comment": self.deadline.comment,
}
self.load_initial_data(form_data)
def save(self):
deadline = self.deadline
deadline.type = self.cleaned_data.get("type", None)
deadline.date = self.cleaned_data.get("date", None)
deadline.comment = self.cleaned_data.get("comment", None)
deadline.save()
self.instance.mark_as_edited(self.user, self.request, edit_comment=DEADLINE_EDITED)
return deadline return deadline
@ -368,9 +489,36 @@ class NewActionModalForm(BaseModalForm):
return action return action
class NewCompensationDocumentForm(NewDocumentForm): class EditCompensationActionModalForm(NewActionModalForm):
action = None
def __init__(self, *args, **kwargs):
self.action = kwargs.pop("action", None)
super().__init__(*args, **kwargs)
form_data = {
"action_type": self.action.action_type,
"action_type_details": self.action.action_type_details.all(),
"amount": self.action.amount,
"unit": self.action.unit,
"comment": self.action.comment,
}
self.load_initial_data(form_data)
def save(self):
action = self.action
action.action_type = self.cleaned_data.get("action_type", None)
action.action_type_details.set(self.cleaned_data.get("action_type_details", []))
action.amount = self.cleaned_data.get("amount", None)
action.unit = self.cleaned_data.get("unit", None)
action.comment = self.cleaned_data.get("comment", None)
action.save()
self.instance.mark_as_edited(self.user, self.request, edit_comment=COMPENSATION_ACTION_EDITED)
return action
class NewCompensationDocumentModalForm(NewDocumentModalForm):
document_model = CompensationDocument document_model = CompensationDocument
class NewEcoAccountDocumentForm(NewDocumentForm): class NewEcoAccountDocumentModalForm(NewDocumentModalForm):
document_model = EcoAccountDocument document_model = EcoAccountDocument

View File

@ -12,6 +12,7 @@ from codelist.models import KonovaCode
from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_COMPENSATION_ACTION_DETAIL_ID from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_COMPENSATION_ACTION_DETAIL_ID
from compensation.managers import CompensationActionManager from compensation.managers import CompensationActionManager
from konova.models import BaseResource from konova.models import BaseResource
from konova.utils.message_templates import COMPENSATION_ACTION_REMOVED
class UnitChoices(models.TextChoices): class UnitChoices(models.TextChoices):

View File

@ -21,7 +21,8 @@ from konova.models import BaseObject, AbstractDocument, Deadline, generate_docum
GeoReferencedMixin GeoReferencedMixin
from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE
from konova.utils.message_templates import DATA_UNSHARED_EXPLANATION, COMPENSATION_REMOVED_TEMPLATE, \ from konova.utils.message_templates import DATA_UNSHARED_EXPLANATION, COMPENSATION_REMOVED_TEMPLATE, \
DOCUMENT_REMOVED_TEMPLATE DOCUMENT_REMOVED_TEMPLATE, COMPENSATION_EDITED_TEMPLATE, DEADLINE_REMOVED, ADDED_DEADLINE, \
COMPENSATION_ACTION_REMOVED, COMPENSATION_STATE_REMOVED, INTERVENTION_HAS_REVOCATIONS_TEMPLATE
from user.models import UserActionLogEntry from user.models import UserActionLogEntry
@ -61,7 +62,6 @@ class AbstractCompensation(BaseObject, GeoReferencedMixin):
user = form.user user = form.user
with transaction.atomic(): with transaction.atomic():
created_action = UserActionLogEntry.get_created_action(user) created_action = UserActionLogEntry.get_created_action(user)
edited_action = UserActionLogEntry.get_edited_action(user, _("Added deadline"))
deadline = Deadline.objects.create( deadline = Deadline.objects.create(
type=form_data["type"], type=form_data["type"],
@ -70,12 +70,26 @@ class AbstractCompensation(BaseObject, GeoReferencedMixin):
created=created_action, created=created_action,
) )
self.modified = edited_action
self.save() self.save()
self.log.add(edited_action)
self.deadlines.add(deadline) self.deadlines.add(deadline)
self.mark_as_edited(user, edit_comment=ADDED_DEADLINE)
return deadline return deadline
def remove_deadline(self, form):
""" Removes a deadline from the abstract compensation
Args:
form (RemoveDeadlineModalForm): The form holding all relevant data
Returns:
"""
deadline = form.deadline
user = form.user
with transaction.atomic():
deadline.delete()
self.mark_as_edited(user, edit_comment=DEADLINE_REMOVED)
def add_action(self, form) -> CompensationAction: def add_action(self, form) -> CompensationAction:
""" Adds a new action to the compensation """ Adds a new action to the compensation
@ -101,6 +115,21 @@ class AbstractCompensation(BaseObject, GeoReferencedMixin):
self.actions.add(comp_action) self.actions.add(comp_action)
return comp_action return comp_action
def remove_action(self, form):
""" Removes a CompensationAction from the abstract compensation
Args:
form (RemoveCompensationActionModalForm): The form holding all relevant data
Returns:
"""
action = form.action
user = form.user
with transaction.atomic():
action.delete()
self.mark_as_edited(user, edit_comment=COMPENSATION_ACTION_REMOVED)
def add_state(self, form, is_before_state: bool) -> CompensationState: def add_state(self, form, is_before_state: bool) -> CompensationState:
""" Adds a new compensation state to the compensation """ Adds a new compensation state to the compensation
@ -125,6 +154,21 @@ class AbstractCompensation(BaseObject, GeoReferencedMixin):
self.after_states.add(state) self.after_states.add(state)
return state return state
def remove_state(self, form):
""" Removes a CompensationState from the abstract compensation
Args:
form (RemoveCompensationStateModalForm): The form holding all relevant data
Returns:
"""
state = form.state
user = form.user
with transaction.atomic():
state.delete()
self.mark_as_edited(user, edit_comment=COMPENSATION_STATE_REMOVED)
def get_surface_after_states(self) -> float: def get_surface_after_states(self) -> float:
""" Calculates the compensation's/account's surface """ Calculates the compensation's/account's surface
@ -287,28 +331,6 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin):
""" """
return self.intervention.users.all() return self.intervention.users.all()
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: def get_documents(self) -> QuerySet:
""" Getter for all documents of a compensation """ Getter for all documents of a compensation
@ -332,7 +354,9 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin):
Returns: Returns:
""" """
return self.intervention.mark_as_edited(user, request, edit_comment, reset_recorded) self.intervention.unrecord(user, request)
action = super().mark_as_edited(user, edit_comment=edit_comment)
return action
def is_ready_for_publish(self) -> bool: def is_ready_for_publish(self) -> bool:
""" Not inherited by RecordableObjectMixin """ Not inherited by RecordableObjectMixin
@ -344,6 +368,26 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin):
""" """
return self.intervention.is_ready_for_publish() return self.intervention.is_ready_for_publish()
def set_status_messages(self, request: HttpRequest):
""" Setter for different information that need to be rendered
Adds messages to the given HttpRequest
Args:
request (HttpRequest): The incoming request
Returns:
request (HttpRequest): The modified request
"""
if self.intervention.legal.revocations.exists():
messages.error(
request,
INTERVENTION_HAS_REVOCATIONS_TEMPLATE.format(self.intervention.legal.revocations.count()),
extra_tags="danger",
)
super().set_status_messages(request)
return request
class CompensationDocument(AbstractDocument): class CompensationDocument(AbstractDocument):
""" """

View File

@ -123,28 +123,6 @@ class EcoAccount(AbstractCompensation, ShareableObjectMixin, RecordableObjectMix
return ret_val_total, ret_val_relative 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: def quality_check(self) -> EcoAccountQualityChecker:
""" Quality check """ Quality check
@ -273,5 +251,5 @@ class EcoAccountDeduction(BaseResource):
def delete(self, user=None, *args, **kwargs): def delete(self, user=None, *args, **kwargs):
if user is not None: if user is not None:
self.intervention.mark_as_edited(user, edit_comment=DEDUCTION_REMOVED) self.intervention.mark_as_edited(user, edit_comment=DEDUCTION_REMOVED)
self.account.mark_as_edited(user, edit_comment=DEDUCTION_REMOVED, reset_recorded=False) self.account.mark_as_edited(user, edit_comment=DEDUCTION_REMOVED)
super().delete(*args, **kwargs) super().delete(*args, **kwargs)

View File

@ -37,8 +37,3 @@ class Payment(BaseResource):
ordering = [ ordering = [
"-amount", "-amount",
] ]
def delete(self, user=None, *args, **kwargs):
if user is not None:
self.intervention.mark_as_edited(user, edit_comment=PAYMENT_REMOVED)
super().delete(*args, **kwargs)

View File

@ -6,11 +6,13 @@ Created on: 16.11.21
""" """
from django.db import models from django.db import models
from django.db.models import Q
from codelist.models import KonovaCode from codelist.models import KonovaCode
from codelist.settings import CODELIST_BIOTOPES_ID, CODELIST_BIOTOPES_EXTRA_CODES_ID from codelist.settings import CODELIST_BIOTOPES_ID, CODELIST_BIOTOPES_EXTRA_CODES_ID
from compensation.managers import CompensationStateManager from compensation.managers import CompensationStateManager
from konova.models import UuidModel from konova.models import UuidModel
from konova.utils.message_templates import COMPENSATION_STATE_REMOVED
class CompensationState(UuidModel): class CompensationState(UuidModel):

View File

@ -31,6 +31,11 @@ class CompensationTable(BaseTable, TableRenderMixin):
orderable=True, orderable=True,
accessor="title", accessor="title",
) )
d = tables.Column(
verbose_name=_("Parcel gmrkng"),
orderable=True,
accessor="geometry",
)
c = tables.Column( c = tables.Column(
verbose_name=_("Checked"), verbose_name=_("Checked"),
orderable=True, orderable=True,
@ -80,14 +85,17 @@ class CompensationTable(BaseTable, TableRenderMixin):
Returns: Returns:
""" """
html = "" context = {
html += self.render_link( "tooltip": _("Open {}").format(_("Intervention")),
tooltip=_("Open {}").format(_("Compensation")), "content": value,
href=reverse("compensation:detail", args=(record.id,)), "url": reverse("compensation:detail", args=(record.id,)),
txt=value, "has_revocations": record.intervention.legal.revocations.exists()
new_tab=False, }
html = render_to_string(
"table/revocation_warning_col.html",
context
) )
return format_html(html) return html
def render_c(self, value, record: Compensation): def render_c(self, value, record: Compensation):
""" Renders the checked column for a compensation """ Renders the checked column for a compensation
@ -115,6 +123,28 @@ class CompensationTable(BaseTable, TableRenderMixin):
) )
return format_html(html) return format_html(html)
def render_d(self, value, record: Compensation):
""" Renders the parcel district column for a compensation
Args:
value (str): The geometry
record (Compensation): The compensation record
Returns:
"""
parcels = value.get_underlying_parcels().values_list(
"gmrkng",
flat=True
).distinct()
html = render_to_string(
"table/gmrkng_col.html",
{
"entries": parcels
}
)
return html
def render_r(self, value, record: Compensation): def render_r(self, value, record: Compensation):
""" Renders the registered column for a compensation """ Renders the registered column for a compensation
@ -173,10 +203,20 @@ class EcoAccountTable(BaseTable, TableRenderMixin):
orderable=True, orderable=True,
accessor="title", accessor="title",
) )
d = tables.Column(
verbose_name=_("Parcel gmrkng"),
orderable=True,
accessor="geometry",
)
av = tables.Column( av = tables.Column(
verbose_name=_("Available"), verbose_name=_("Available"),
orderable=True, orderable=True,
empty_values=[], empty_values=[],
attrs={
"th": {
"class": "w-20",
}
}
) )
r = tables.Column( r = tables.Column(
verbose_name=_("Recorded"), verbose_name=_("Recorded"),
@ -244,6 +284,28 @@ class EcoAccountTable(BaseTable, TableRenderMixin):
html = render_to_string("konova/widgets/progressbar.html", {"value": value_relative}) html = render_to_string("konova/widgets/progressbar.html", {"value": value_relative})
return format_html(html) return format_html(html)
def render_d(self, value, record: Compensation):
""" Renders the parcel district column for a compensation
Args:
value (str): The geometry
record (Compensation): The compensation record
Returns:
"""
parcels = value.get_underlying_parcels().values_list(
"gmrkng",
flat=True
).distinct()
html = render_to_string(
"table/gmrkng_col.html",
{
"entries": parcels
}
)
return html
def render_r(self, value, record: EcoAccount): def render_r(self, value, record: EcoAccount):
""" Renders the recorded column for an eco account """ Renders the recorded column for an eco account

View File

@ -35,7 +35,7 @@
{% trans 'Comment' %} {% trans 'Comment' %}
</th> </th>
{% if is_default_member and has_access %} {% if is_default_member and has_access %}
<th scope="col"> <th class="w-10" scope="col">
<span class="float-right"> <span class="float-right">
{% trans 'Action' %} {% trans 'Action' %}
</span> </span>
@ -61,9 +61,12 @@
{{ action.comment }} {{ action.comment }}
</div> </div>
</td> </td>
<td class=""> <td class="align-middle float-right">
{% if is_default_member and has_access %} {% if is_default_member and has_access %}
<button data-form-url="{% url 'compensation:action-remove' obj.id action.id %}" class="btn btn-default btn-modal float-right" title="{% trans 'Remove action' %}"> <button data-form-url="{% url 'compensation:action-edit' obj.id action.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit action' %}">
{% fa5_icon 'edit' %}
</button>
<button data-form-url="{% url 'compensation:action-remove' obj.id action.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove action' %}">
{% fa5_icon 'trash' %} {% fa5_icon 'trash' %}
</button> </button>
{% endif %} {% endif %}

View File

@ -34,7 +34,7 @@
{% trans 'Comment' %} {% trans 'Comment' %}
</th> </th>
{% if is_default_member and has_access %} {% if is_default_member and has_access %}
<th scope="col"> <th class="w-10" scope="col">
<span class="float-right"> <span class="float-right">
{% trans 'Action' %} {% trans 'Action' %}
</span> </span>
@ -54,9 +54,12 @@
{{ deadline.comment }} {{ deadline.comment }}
</div> </div>
</td> </td>
<td> <td class="align-middle float-right">
{% if is_default_member and has_access %} {% if is_default_member and has_access %}
<button data-form-url="{% url 'compensation:deadline-remove' obj.id deadline.id %}" class="btn btn-default btn-modal float-right" title="{% trans 'Remove deadline' %}"> <button data-form-url="{% url 'compensation:deadline-edit' obj.id deadline.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit deadline' %}">
{% fa5_icon 'edit' %}
</button>
<button data-form-url="{% url 'compensation:deadline-remove' obj.id deadline.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove deadline' %}">
{% fa5_icon 'trash' %} {% fa5_icon 'trash' %}
</button> </button>
{% endif %} {% endif %}

View File

@ -27,11 +27,14 @@
<th scope="col"> <th scope="col">
{% trans 'Title' %} {% trans 'Title' %}
</th> </th>
<th scope="col">
{% trans 'Created on' %}
</th>
<th scope="col"> <th scope="col">
{% trans 'Comment' %} {% trans 'Comment' %}
</th> </th>
{% if is_default_member and has_access %} {% if is_default_member and has_access %}
<th scope="col"> <th class="w-10" scope="col">
<span class="float-right"> <span class="float-right">
{% trans 'Action' %} {% trans 'Action' %}
</span> </span>
@ -43,18 +46,24 @@
{% for doc in obj.documents.all %} {% for doc in obj.documents.all %}
<tr> <tr>
<td class="align-middle"> <td class="align-middle">
<a href="{% url 'compensation:get-doc' doc.id %}"> <a href="{% url 'compensation:get-doc' obj.id doc.id %}">
{{ doc.title }} {{ doc.title }}
</a> </a>
</td> </td>
<td class="align-middle">
{{ doc.date_of_creation }}
</td>
<td class="align-middle"> <td class="align-middle">
<div class="scroll-150"> <div class="scroll-150">
{{ doc.comment }} {{ doc.comment }}
</div> </div>
</td> </td>
<td class="align-middle"> <td class="align-middle float-right">
{% if is_default_member and has_access %} {% if is_default_member and has_access %}
<button data-form-url="{% url 'compensation:remove-doc' doc.id %}" class="btn btn-default btn-modal float-right" title="{% trans 'Remove document' %}"> <button data-form-url="{% url 'compensation:edit-doc' obj.id doc.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit document' %}">
{% fa5_icon 'edit' %}
</button>
<button data-form-url="{% url 'compensation:remove-doc' obj.id doc.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove document' %}">
{% fa5_icon 'trash' %} {% fa5_icon 'trash' %}
</button> </button>
{% endif %} {% endif %}

View File

@ -36,7 +36,7 @@
{% trans 'Surface' %} {% trans 'Surface' %}
</th> </th>
{% if is_default_member and has_access %} {% if is_default_member and has_access %}
<th scope="col"> <th class="w-10" scope="col">
<span class="float-right"> <span class="float-right">
{% trans 'Action' %} {% trans 'Action' %}
</span> </span>
@ -57,9 +57,12 @@
{% endif %} {% endif %}
</td> </td>
<td>{{ state.surface|floatformat:2 }} m²</td> <td>{{ state.surface|floatformat:2 }} m²</td>
<td> <td class="align-middle float-right">
{% if is_default_member and has_access %} {% if is_default_member and has_access %}
<button data-form-url="{% url 'compensation:state-remove' obj.id state.id %}" class="btn btn-default btn-modal float-right" title="{% trans 'Remove state' %}"> <button data-form-url="{% url 'compensation:state-edit' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit state' %}">
{% fa5_icon 'edit' %}
</button>
<button data-form-url="{% url 'compensation:state-remove' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove state' %}">
{% fa5_icon 'trash' %} {% fa5_icon 'trash' %}
</button> </button>
{% endif %} {% endif %}

View File

@ -36,7 +36,7 @@
{% trans 'Surface' %} {% trans 'Surface' %}
</th> </th>
{% if is_default_member and has_access %} {% if is_default_member and has_access %}
<th scope="col"> <th class="w-10" scope="col">
<span class="float-right"> <span class="float-right">
{% trans 'Action' %} {% trans 'Action' %}
</span> </span>
@ -57,9 +57,12 @@
{% endif %} {% endif %}
</td> </td>
<td>{{ state.surface|floatformat:2 }} m²</td> <td>{{ state.surface|floatformat:2 }} m²</td>
<td> <td class="align-middle float-right">
{% if is_default_member and has_access %} {% if is_default_member and has_access %}
<button data-form-url="{% url 'compensation:state-remove' obj.id state.id %}" class="btn btn-default btn-modal float-right" title="{% trans 'Remove state' %}"> <button data-form-url="{% url 'compensation:state-edit' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit state' %}">
{% fa5_icon 'edit' %}
</button>
<button data-form-url="{% url 'compensation:state-remove' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove state' %}">
{% fa5_icon 'trash' %} {% fa5_icon 'trash' %}
</button> </button>
{% endif %} {% endif %}

View File

@ -34,7 +34,7 @@
{% trans 'Comment' %} {% trans 'Comment' %}
</th> </th>
{% if is_default_member and has_access %} {% if is_default_member and has_access %}
<th scope="col"> <th class="w-10" scope="col">
<span class="float-right"> <span class="float-right">
{% trans 'Action' %} {% trans 'Action' %}
</span> </span>
@ -60,9 +60,12 @@
{{ action.comment }} {{ action.comment }}
</div> </div>
</td> </td>
<td class=""> <td class="align-middle float-right">
{% if is_default_member and has_access %} {% if is_default_member and has_access %}
<button data-form-url="{% url 'compensation:acc:action-remove' obj.id action.id %}" class="btn btn-default btn-modal float-right" title="{% trans 'Remove action' %}"> <button data-form-url="{% url 'compensation:acc:action-edit' obj.id action.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit action' %}">
{% fa5_icon 'edit' %}
</button>
<button data-form-url="{% url 'compensation:acc:action-remove' obj.id action.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove action' %}">
{% fa5_icon 'trash' %} {% fa5_icon 'trash' %}
</button> </button>
{% endif %} {% endif %}

View File

@ -33,7 +33,7 @@
<th scope="col"> <th scope="col">
{% trans 'Comment' %} {% trans 'Comment' %}
</th> </th>
<th scope="col"> <th class="w-10" scope="col">
<span class="float-right"> <span class="float-right">
{% trans 'Action' %} {% trans 'Action' %}
</span> </span>
@ -52,9 +52,12 @@
{{ deadline.comment }} {{ deadline.comment }}
</div> </div>
</td> </td>
<td class="align-middle"> <td class="align-middle float-right">
{% if is_default_member and has_access %} {% if is_default_member and has_access %}
<button data-form-url="{% url 'compensation:acc:deadline-remove' obj.id deadline.id %}" class="btn btn-default btn-modal float-right" title="{% trans 'Remove deadline' %}"> <button data-form-url="{% url 'compensation:acc:deadline-edit' obj.id deadline.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit deadline' %}">
{% fa5_icon 'edit' %}
</button>
<button data-form-url="{% url 'compensation:acc:deadline-remove' obj.id deadline.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove deadline' %}">
{% fa5_icon 'trash' %} {% fa5_icon 'trash' %}
</button> </button>
{% endif %} {% endif %}

View File

@ -36,7 +36,7 @@
<th scope="col"> <th scope="col">
{% trans 'Created' %} {% trans 'Created' %}
</th> </th>
<th scope="col"> <th class="w-10" scope="col">
<span class="float-right"> <span class="float-right">
{% trans 'Action' %} {% trans 'Action' %}
</span> </span>
@ -60,9 +60,12 @@
</td> </td>
<td class="align-middle">{{ deduction.surface|floatformat:2|intcomma }} m²</td> <td class="align-middle">{{ deduction.surface|floatformat:2|intcomma }} m²</td>
<td class="align-middle">{{ deduction.created.timestamp|default_if_none:""|naturalday}}</td> <td class="align-middle">{{ deduction.created.timestamp|default_if_none:""|naturalday}}</td>
<td> <td class="align-middle float-right">
{% if is_default_member and has_access %} {% if is_default_member and has_access %}
<button data-form-url="{% url 'compensation:acc:remove-deduction' deduction.account.id deduction.id %}" class="btn btn-default btn-modal float-right" title="{% trans 'Remove Deduction' %}"> <button data-form-url="{% url 'compensation:acc:edit-deduction' deduction.account.id deduction.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit Deduction' %}">
{% fa5_icon 'edit' %}
</button>
<button data-form-url="{% url 'compensation:acc:remove-deduction' deduction.account.id deduction.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove Deduction' %}">
{% fa5_icon 'trash' %} {% fa5_icon 'trash' %}
</button> </button>
{% endif %} {% endif %}

View File

@ -28,9 +28,12 @@
{% trans 'Title' %} {% trans 'Title' %}
</th> </th>
<th scope="col"> <th scope="col">
{% trans 'Comment' %} {% trans 'Created on' %}
</th> </th>
<th scope="col"> <th scope="col">
{% trans 'Comment' %}
</th>
<th class="w-10" scope="col">
<span class="float-right"> <span class="float-right">
{% trans 'Action' %} {% trans 'Action' %}
</span> </span>
@ -41,18 +44,24 @@
{% for doc in obj.documents.all %} {% for doc in obj.documents.all %}
<tr> <tr>
<td class="align-middle"> <td class="align-middle">
<a href="{% url 'compensation:acc:get-doc' doc.id %}"> <a href="{% url 'compensation:acc:get-doc' obj.id doc.id %}">
{{ doc.title }} {{ doc.title }}
</a> </a>
</td> </td>
<td class="align-middle">
{{ doc.date_of_creation }}
</td>
<td class="align-middle"> <td class="align-middle">
<div class="scroll-150"> <div class="scroll-150">
{{ doc.comment }} {{ doc.comment }}
</div> </div>
</td> </td>
<td class="align-middle"> <td class="align-middle float-right">
{% if is_default_member and has_access %} {% if is_default_member and has_access %}
<button data-form-url="{% url 'compensation:acc:remove-doc' doc.id %}" class="btn btn-default btn-modal float-right" title="{% trans 'Remove document' %}"> <button data-form-url="{% url 'compensation:acc:edit-doc' obj.id doc.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit document' %}">
{% fa5_icon 'edit' %}
</button>
<button data-form-url="{% url 'compensation:acc:remove-doc' obj.id doc.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove document' %}">
{% fa5_icon 'trash' %} {% fa5_icon 'trash' %}
</button> </button>
{% endif %} {% endif %}

View File

@ -36,7 +36,7 @@
{% trans 'Surface' %} {% trans 'Surface' %}
</th> </th>
{% if is_default_member and has_access %} {% if is_default_member and has_access %}
<th scope="col"> <th class="w-10" scope="col">
<span class="float-right"> <span class="float-right">
{% trans 'Action' %} {% trans 'Action' %}
</span> </span>
@ -57,9 +57,12 @@
{% endif %} {% endif %}
</td> </td>
<td>{{ state.surface|floatformat:2 }} m²</td> <td>{{ state.surface|floatformat:2 }} m²</td>
<td> <td class="align-middle float-right">
{% if is_default_member and has_access %} {% if is_default_member and has_access %}
<button data-form-url="{% url 'compensation:acc:state-remove' obj.id state.id %}" class="btn btn-default btn-modal float-right" title="{% trans 'Remove state' %}"> <button data-form-url="{% url 'compensation:acc:state-edit' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit state' %}">
{% fa5_icon 'edit' %}
</button>
<button data-form-url="{% url 'compensation:acc:state-remove' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove state' %}">
{% fa5_icon 'trash' %} {% fa5_icon 'trash' %}
</button> </button>
{% endif %} {% endif %}

View File

@ -36,7 +36,7 @@
{% trans 'Surface' %} {% trans 'Surface' %}
</th> </th>
{% if is_default_member and has_access %} {% if is_default_member and has_access %}
<th scope="col"> <th class="w-10" scope="col">
<span class="float-right"> <span class="float-right">
{% trans 'Action' %} {% trans 'Action' %}
</span> </span>
@ -57,9 +57,12 @@
{% endif %} {% endif %}
</td> </td>
<td>{{ state.surface|floatformat:2 }} m²</td> <td>{{ state.surface|floatformat:2 }} m²</td>
<td> <td class="align-middle float-right">
{% if is_default_member and has_access %} {% if is_default_member and has_access %}
<button data-form-url="{% url 'compensation:acc:state-remove' obj.id state.id %}" class="btn btn-default btn-modal float-right" title="{% trans 'Remove state' %}"> <button data-form-url="{% url 'compensation:acc:state-edit' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit state' %}">
{% fa5_icon 'edit' %}
</button>
<button data-form-url="{% url 'compensation:acc:state-remove' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove state' %}">
{% fa5_icon 'trash' %} {% fa5_icon 'trash' %}
</button> </button>
{% endif %} {% endif %}

View File

@ -0,0 +1,7 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 07.02.22
"""

View File

@ -0,0 +1,248 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 07.02.22
"""
from django.test.client import Client
from django.urls import reverse
from konova.models import Deadline, DeadlineType
from konova.settings import DEFAULT_GROUP
from konova.tests.test_views import BaseViewTestCase
class CompensationViewTestCase(BaseViewTestCase):
"""
These tests focus on proper returned views depending on the user's groups privileges and login status
"""
@classmethod
def setUpTestData(cls) -> None:
super().setUpTestData()
def setUp(self) -> None:
super().setUp()
state = self.create_dummy_states()
self.compensation.before_states.set([state])
self.compensation.after_states.set([state])
action = self.create_dummy_action()
self.compensation.actions.set([action])
self.deadline = Deadline.objects.get_or_create(
type=DeadlineType.FINISHED,
date="2020-01-01",
comment="TESTDEADDLINECOMMENT"
)[0]
self.compensation.deadlines.add(self.deadline)
# Prepare urls
self.index_url = reverse("compensation:index", args=())
self.new_url = reverse("compensation:new", args=(self.intervention.id,))
self.new_id_url = reverse("compensation:new-id", args=())
self.detail_url = reverse("compensation:detail", args=(self.compensation.id,))
self.log_url = reverse("compensation:log", args=(self.compensation.id,))
self.edit_url = reverse("compensation:edit", args=(self.compensation.id,))
self.remove_url = reverse("compensation:remove", args=(self.compensation.id,))
self.report_url = reverse("compensation:report", args=(self.compensation.id,))
self.state_new_url = reverse("compensation:new-state", args=(self.compensation.id,))
self.action_new_url = reverse("compensation:new-action", args=(self.compensation.id,))
self.deadline_new_url = reverse("compensation:new-deadline", args=(self.compensation.id,))
self.deadline_edit_url = reverse("compensation:deadline-edit", args=(self.compensation.id, self.deadline.id))
self.deadline_remove_url = reverse("compensation:deadline-remove", args=(self.compensation.id, self.deadline.id))
self.new_doc_url = reverse("compensation:new-doc", args=(self.compensation.id,))
self.state_remove_url = reverse("compensation:state-remove", args=(self.compensation.id, self.comp_state.id,))
self.action_remove_url = reverse("compensation:action-remove", args=(self.compensation.id, self.comp_action.id,))
def test_anonymous_user(self):
""" Check correct status code for all requests
Assumption: User not logged in
Returns:
"""
client = Client()
success_urls = [
self.report_url,
]
fail_urls = [
self.index_url,
self.detail_url,
self.new_url,
self.new_id_url,
self.log_url,
self.edit_url,
self.remove_url,
self.state_new_url,
self.action_new_url,
self.deadline_new_url,
self.deadline_edit_url,
self.deadline_remove_url,
self.state_remove_url,
self.action_remove_url,
self.new_doc_url,
]
self.assert_url_success(client, success_urls)
self.assert_url_fail(client, fail_urls)
def test_logged_in_no_groups_shared(self):
""" Check correct status code for all requests
Assumption: User logged in and has no groups and data is shared
Returns:
"""
client = Client()
client.login(username=self.superuser.username, password=self.superuser_pw)
self.superuser.groups.set([])
self.intervention.share_with_list([self.superuser])
# Since the user has no groups, it does not matter that data has been shared. There SHOULD not be any difference
# to a user without access, since the important permissions are missing
success_urls = [
self.index_url,
self.detail_url,
self.report_url,
]
fail_urls = [
self.new_url,
self.new_id_url,
self.log_url,
self.edit_url,
self.remove_url,
self.state_new_url,
self.action_new_url,
self.deadline_new_url,
self.deadline_edit_url,
self.deadline_remove_url,
self.state_remove_url,
self.action_remove_url,
self.new_doc_url,
]
self.assert_url_success(client, success_urls)
self.assert_url_fail(client, fail_urls)
def test_logged_in_no_groups_unshared(self):
""" Check correct status code for all requests
Assumption: User logged in and has no groups and data is not shared
Returns:
"""
client = Client()
client.login(username=self.superuser.username, password=self.superuser_pw)
self.superuser.groups.set([])
# Sharing is inherited by base intervention for compensation. Therefore configure the interventions share state
self.intervention.share_with_list([])
# Since the user has no groups, it does not matter that data is unshared. There SHOULD not be any difference
# to a user having shared access, since all important permissions are missing
success_urls = [
self.index_url,
self.detail_url,
self.report_url,
]
fail_urls = [
self.new_url,
self.new_id_url,
self.log_url,
self.edit_url,
self.remove_url,
self.state_new_url,
self.action_new_url,
self.deadline_new_url,
self.deadline_edit_url,
self.deadline_remove_url,
self.state_remove_url,
self.action_remove_url,
self.new_doc_url,
]
self.assert_url_success(client, success_urls)
self.assert_url_fail(client, fail_urls)
def test_logged_in_default_group_shared(self):
""" Check correct status code for all requests
Assumption: User logged in, is default group member and data is shared
--> Default group necessary since all base functionalities depend on this group membership
Returns:
"""
client = Client()
client.login(username=self.superuser.username, password=self.superuser_pw)
group = self.groups.get(name=DEFAULT_GROUP)
self.superuser.groups.set([group])
# Sharing is inherited by base intervention for compensation. Therefore configure the interventions share state
self.intervention.share_with_list([self.superuser])
success_urls = [
self.index_url,
self.detail_url,
self.report_url,
self.new_url,
self.new_id_url,
self.edit_url,
self.state_new_url,
self.action_new_url,
self.deadline_new_url,
self.deadline_edit_url,
self.deadline_remove_url,
self.state_remove_url,
self.action_remove_url,
self.new_doc_url,
self.log_url,
self.remove_url,
]
self.assert_url_success(client, success_urls)
def test_logged_in_default_group_unshared(self):
""" Check correct status code for all requests
Assumption: User logged in, is default group member and data is NOT shared
--> Default group necessary since all base functionalities depend on this group membership
Returns:
"""
client = Client()
client.login(username=self.superuser.username, password=self.superuser_pw)
group = self.groups.get(name=DEFAULT_GROUP)
self.superuser.groups.set([group])
# Sharing is inherited by base intervention for compensation. Therefore configure the interventions share state
self.intervention.share_with_list([])
success_urls = [
self.index_url,
self.detail_url,
self.report_url,
self.new_id_url,
]
fail_urls = [
self.new_url,
self.edit_url,
self.state_new_url,
self.action_new_url,
self.deadline_new_url,
self.deadline_edit_url,
self.deadline_remove_url,
self.state_remove_url,
self.action_remove_url,
self.new_doc_url,
self.log_url,
self.remove_url,
]
self.assert_url_fail(client, fail_urls)
self.assert_url_success(client, success_urls)

View File

@ -2,7 +2,7 @@
Author: Michel Peltriaux Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 11.11.21 Created on: 07.02.22
""" """
import datetime import datetime
@ -11,7 +11,7 @@ from django.contrib.gis.geos import MultiPolygon
from django.urls import reverse from django.urls import reverse
from compensation.models import Compensation from compensation.models import Compensation
from konova.settings import ETS_GROUP, ZB_GROUP from konova.settings import ZB_GROUP, ETS_GROUP
from konova.tests.test_views import BaseWorkflowTestCase from konova.tests.test_views import BaseWorkflowTestCase
from user.models import UserAction from user.models import UserAction
@ -21,17 +21,18 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
def setUpTestData(cls): def setUpTestData(cls):
super().setUpTestData() super().setUpTestData()
# Give the user shared access to the dummy intervention -> inherits the access to the compensation
cls.intervention.share_with(cls.superuser)
# Make sure the intervention itself would be fine with valid data
cls.intervention = cls.fill_out_intervention(cls.intervention)
# Make sure the compensation is linked to the intervention
cls.intervention.compensations.set([cls.compensation])
def setUp(self) -> None: def setUp(self) -> None:
super().setUp() super().setUp()
# Give the user shared access to the dummy intervention -> inherits the access to the compensation
self.intervention.share_with(self.superuser)
# Make sure the intervention itself would be fine with valid data
self.intervention = self.fill_out_intervention(self.intervention)
# Make sure the compensation is linked to the intervention
self.intervention.compensations.set([self.compensation])
# Delete all existing compensations, which might be created by tests # Delete all existing compensations, which might be created by tests
Compensation.objects.all().delete() Compensation.objects.all().delete()
@ -55,6 +56,7 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
"geom": test_geom.geojson, "geom": test_geom.geojson,
"intervention": self.intervention.id, "intervention": self.intervention.id,
} }
pre_creation_intervention_log_count = self.intervention.log.count()
# Preserve the current number of intervention's compensations # Preserve the current number of intervention's compensations
num_compensations = self.intervention.compensations.count() num_compensations = self.intervention.compensations.count()
@ -66,6 +68,13 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
self.assertEqual(new_compensation.identifier, test_id) self.assertEqual(new_compensation.identifier, test_id)
self.assertEqual(new_compensation.title, test_title) self.assertEqual(new_compensation.title, test_title)
self.assert_equal_geometries(new_compensation.geometry.geom, test_geom) self.assert_equal_geometries(new_compensation.geometry.geom, test_geom)
self.assertEqual(new_compensation.log.count(), 1)
# Expect logs to be set
self.assertEqual(pre_creation_intervention_log_count + 1, self.intervention.log.count())
self.assertEqual(new_compensation.log.count(), 1)
self.assertEqual(self.intervention.log.first().action, UserAction.EDITED)
self.assertEqual(new_compensation.log.first().action, UserAction.CREATED)
def test_new_from_intervention(self): def test_new_from_intervention(self):
""" Test the creation of a compensation from a given intervention """ Test the creation of a compensation from a given intervention
@ -83,6 +92,7 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
"title": test_title, "title": test_title,
"geom": test_geom.geojson, "geom": test_geom.geojson,
} }
pre_creation_intervention_log_count = self.intervention.log.count()
# Preserve the current number of intervention's compensations # Preserve the current number of intervention's compensations
num_compensations = self.intervention.compensations.count() num_compensations = self.intervention.compensations.count()
@ -95,6 +105,12 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
self.assertEqual(new_compensation.title, test_title) self.assertEqual(new_compensation.title, test_title)
self.assert_equal_geometries(new_compensation.geometry.geom, test_geom) self.assert_equal_geometries(new_compensation.geometry.geom, test_geom)
# Expect logs to be set
self.assertEqual(new_compensation.log.count(), 1)
self.assertEqual(new_compensation.log.first().action, UserAction.CREATED)
self.assertEqual(pre_creation_intervention_log_count + 1, self.intervention.log.count())
self.assertEqual(self.intervention.log.first().action, UserAction.EDITED)
def test_edit(self): def test_edit(self):
""" Checks that the editing of a compensation works """ Checks that the editing of a compensation works
@ -103,6 +119,7 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
""" """
url = reverse("compensation:edit", args=(self.compensation.id,)) url = reverse("compensation:edit", args=(self.compensation.id,))
self.compensation = self.fill_out_compensation(self.compensation) self.compensation = self.fill_out_compensation(self.compensation)
pre_edit_log_count = self.compensation.log.count()
new_title = self.create_dummy_string() new_title = self.create_dummy_string()
new_identifier = self.create_dummy_string() new_identifier = self.create_dummy_string()
@ -138,6 +155,10 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
self.assert_equal_geometries(self.compensation.geometry.geom, new_geometry) self.assert_equal_geometries(self.compensation.geometry.geom, new_geometry)
# Expect logs to be set
self.assertEqual(pre_edit_log_count + 1, self.compensation.log.count())
self.assertEqual(self.compensation.log.first().action, UserAction.EDITED)
def test_checkability(self): def test_checkability(self):
""" """
This tests if the checkability of the compensation (which is defined by the linked intervention's checked This tests if the checkability of the compensation (which is defined by the linked intervention's checked
@ -152,6 +173,8 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
# Add proper privilege for the user # Add proper privilege for the user
self.superuser.groups.add(self.groups.get(name=ZB_GROUP)) self.superuser.groups.add(self.groups.get(name=ZB_GROUP))
pre_check_log_count = self.compensation.log.count()
# Prepare url and form data # Prepare url and form data
url = reverse("intervention:check", args=(self.intervention.id,)) url = reverse("intervention:check", args=(self.intervention.id,))
post_data = { post_data = {
@ -186,6 +209,7 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
# Expect the user action to be in the log # Expect the user action to be in the log
self.assertIn(checked, self.compensation.log.all()) self.assertIn(checked, self.compensation.log.all())
self.assertEqual(pre_check_log_count + 1, self.compensation.log.count())
def test_recordability(self): def test_recordability(self):
""" """
@ -200,6 +224,7 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
""" """
# Add proper privilege for the user # Add proper privilege for the user
self.superuser.groups.add(self.groups.get(name=ETS_GROUP)) self.superuser.groups.add(self.groups.get(name=ETS_GROUP))
pre_record_log_count = self.compensation.log.count()
# Prepare url and form data # Prepare url and form data
record_url = reverse("intervention:record", args=(self.intervention.id,)) record_url = reverse("intervention:record", args=(self.intervention.id,))
@ -234,62 +259,5 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
# Expect the user action to be in the log # Expect the user action to be in the log
self.assertIn(recorded, self.compensation.log.all()) self.assertIn(recorded, self.compensation.log.all())
self.assertEqual(pre_record_log_count + 1, self.compensation.log.count())
class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
@classmethod
def setUpTestData(cls):
super().setUpTestData()
# Add user to conservation office group and give shared access to the account
cls.superuser.groups.add(cls.groups.get(name=ETS_GROUP))
cls.eco_account.share_with_list([cls.superuser])
def test_deductability(self):
"""
This tests the deductability of an eco account.
An eco account should only be deductible if it is recorded.
Returns:
"""
# Give user shared access to the dummy intervention, which will be needed here
self.intervention.share_with(self.superuser)
# Prepare data for deduction creation
deduct_url = reverse("compensation:acc:new-deduction", args=(self.eco_account.id,))
test_surface = 10.00
post_data = {
"surface": test_surface,
"account": self.id,
"intervention": self.intervention.id,
}
# Perform request --> expect to fail
self.client_user.post(deduct_url, post_data)
# Expect that no deduction has been created
self.assertEqual(0, self.eco_account.deductions.count())
self.assertEqual(0, self.intervention.deductions.count())
# Now mock the eco account as it would be recorded (with invalid data)
# Make sure the deductible surface is high enough for the request
self.eco_account.set_recorded(self.superuser)
self.eco_account.refresh_from_db()
self.eco_account.deductable_surface = test_surface + 1.00
self.eco_account.save()
self.assertIsNotNone(self.eco_account.recorded)
self.assertGreater(self.eco_account.deductable_surface, test_surface)
# Rerun the request
self.client_user.post(deduct_url, post_data)
# Expect that the deduction has been created
self.assertEqual(1, self.eco_account.deductions.count())
self.assertEqual(1, self.intervention.deductions.count())
deduction = self.eco_account.deductions.first()
self.assertEqual(deduction.surface, test_surface)
self.assertEqual(deduction.account, self.eco_account)
self.assertEqual(deduction.intervention, self.intervention)

View File

@ -0,0 +1,7 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 07.02.22
"""

View File

@ -0,0 +1,229 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 27.10.21
"""
from django.urls import reverse
from django.test import Client
from compensation.tests.compensation.test_views import CompensationViewTestCase
from konova.models import DeadlineType, Deadline
from konova.settings import DEFAULT_GROUP
class EcoAccountViewTestCase(CompensationViewTestCase):
"""
These tests focus on proper returned views depending on the user's groups privileges and login status
EcoAccounts can inherit the same tests used for compensations.
"""
comp_state = None
comp_action = None
@classmethod
def setUpTestData(cls) -> None:
super().setUpTestData()
def setUp(self) -> None:
super().setUp()
state = self.create_dummy_states()
self.eco_account.before_states.set([state])
self.eco_account.after_states.set([state])
action = self.create_dummy_action()
self.eco_account.actions.set([action])
# Prepare urls
self.index_url = reverse("compensation:acc:index", args=())
self.new_url = reverse("compensation:acc:new", args=())
self.new_id_url = reverse("compensation:acc:new-id", args=())
self.detail_url = reverse("compensation:acc:detail", args=(self.eco_account.id,))
self.log_url = reverse("compensation:acc:log", args=(self.eco_account.id,))
self.edit_url = reverse("compensation:acc:edit", args=(self.eco_account.id,))
self.remove_url = reverse("compensation:acc:remove", args=(self.eco_account.id,))
self.report_url = reverse("compensation:acc:report", args=(self.eco_account.id,))
self.state_new_url = reverse("compensation:acc:new-state", args=(self.eco_account.id,))
self.state_edit_url = reverse("compensation:acc:state-edit", args=(self.eco_account.id, self.comp_state.id))
self.state_remove_url = reverse("compensation:acc:state-remove", args=(self.eco_account.id, self.comp_state.id,))
self.action_new_url = reverse("compensation:acc:new-action", args=(self.eco_account.id,))
self.action_edit_url = reverse("compensation:acc:action-edit", args=(self.eco_account.id, self.comp_action.id))
self.action_remove_url = reverse("compensation:acc:action-remove", args=(self.eco_account.id, self.comp_action.id,))
self.deadline = Deadline.objects.get_or_create(
type=DeadlineType.FINISHED,
date="2020-01-01",
comment="DEADLINE COMMENT"
)[0]
self.eco_account.deadlines.add(self.deadline)
self.deadline_new_url = reverse("compensation:acc:new-deadline", args=(self.eco_account.id,))
self.deadline_edit_url = reverse("compensation:acc:deadline-edit", args=(self.eco_account.id, self.deadline.id))
self.deadline_remove_url = reverse("compensation:acc:deadline-remove", args=(self.eco_account.id, self.deadline.id))
self.new_doc_url = reverse("compensation:acc:new-doc", args=(self.eco_account.id,))
def test_logged_in_no_groups_shared(self):
""" Check correct status code for all requests
Assumption: User logged in and has no groups and data is shared
Returns:
"""
client = Client()
client.login(username=self.superuser.username, password=self.superuser_pw)
self.superuser.groups.set([])
self.eco_account.share_with_list([self.superuser])
# Since the user has no groups, it does not matter that data has been shared. There SHOULD not be any difference
# to a user without access, since the important permissions are missing
success_urls = [
self.index_url,
self.detail_url,
self.report_url,
]
fail_urls = [
self.new_url,
self.new_id_url,
self.log_url,
self.edit_url,
self.remove_url,
self.state_new_url,
self.state_edit_url,
self.state_remove_url,
self.action_new_url,
self.action_edit_url,
self.action_remove_url,
self.deadline_new_url,
self.deadline_edit_url,
self.deadline_remove_url,
self.new_doc_url,
]
self.assert_url_success(client, success_urls)
self.assert_url_fail(client, fail_urls)
def test_logged_in_no_groups_unshared(self):
""" Check correct status code for all requests
Assumption: User logged in and has no groups and data is shared
Returns:
"""
client = Client()
client.login(username=self.superuser.username, password=self.superuser_pw)
self.superuser.groups.set([])
self.eco_account.share_with_list([])
# Since the user has no groups, it does not matter that data is unshared. There SHOULD not be any difference
# to a user having shared access, since all important permissions are missing
success_urls = [
self.index_url,
self.detail_url,
self.report_url,
]
fail_urls = [
self.new_url,
self.new_id_url,
self.log_url,
self.edit_url,
self.remove_url,
self.state_new_url,
self.state_edit_url,
self.state_remove_url,
self.action_new_url,
self.action_edit_url,
self.action_remove_url,
self.deadline_new_url,
self.deadline_edit_url,
self.deadline_remove_url,
self.new_doc_url,
]
self.assert_url_success(client, success_urls)
self.assert_url_fail(client, fail_urls)
def test_logged_in_default_group_shared(self):
""" Check correct status code for all requests
Assumption: User logged in, is default group member and data is shared
--> Default group necessary since all base functionalities depend on this group membership
Returns:
"""
client = Client()
client.login(username=self.superuser.username, password=self.superuser_pw)
group = self.groups.get(name=DEFAULT_GROUP)
self.superuser.groups.set([group])
# Sharing is inherited by base intervention for compensation. Therefore configure the interventions share state
self.eco_account.share_with_list([self.superuser])
success_urls = [
self.index_url,
self.detail_url,
self.report_url,
self.new_url,
self.new_id_url,
self.edit_url,
self.state_new_url,
self.state_edit_url,
self.state_remove_url,
self.action_new_url,
self.action_edit_url,
self.action_remove_url,
self.new_doc_url,
self.deadline_new_url,
self.deadline_edit_url,
self.deadline_remove_url,
self.log_url,
self.remove_url,
]
self.assert_url_success(client, success_urls)
def test_logged_in_default_group_unshared(self):
""" Check correct status code for all requests
Assumption: User logged in, is default group member and data is NOT shared
--> Default group necessary since all base functionalities depend on this group membership
Returns:
"""
client = Client()
client.login(username=self.superuser.username, password=self.superuser_pw)
group = self.groups.get(name=DEFAULT_GROUP)
self.superuser.groups.set([group])
self.eco_account.share_with_list([])
success_urls = [
self.index_url,
self.detail_url,
self.report_url,
self.new_id_url,
self.new_url,
]
fail_urls = [
self.edit_url,
self.state_new_url,
self.state_edit_url,
self.state_remove_url,
self.action_new_url,
self.action_edit_url,
self.action_remove_url,
self.new_doc_url,
self.log_url,
self.remove_url,
self.deadline_new_url,
self.deadline_edit_url,
self.deadline_remove_url,
]
self.assert_url_fail(client, fail_urls)
self.assert_url_success(client, success_urls)

View File

@ -0,0 +1,302 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 11.11.21
"""
import datetime
from django.contrib.gis.geos import MultiPolygon
from django.core.exceptions import ObjectDoesNotExist
from django.urls import reverse
from compensation.models import EcoAccount, EcoAccountDeduction
from konova.settings import ETS_GROUP, DEFAULT_GROUP
from konova.tests.test_views import BaseWorkflowTestCase
from user.models import UserAction
class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
@classmethod
def setUpTestData(cls):
super().setUpTestData()
def setUp(self) -> None:
super().setUp()
# Add user to conservation office group and give shared access to the account
self.superuser.groups.add(self.groups.get(name=DEFAULT_GROUP))
self.superuser.groups.add(self.groups.get(name=ETS_GROUP))
self.eco_account.share_with_list([self.superuser])
def test_new(self):
""" Test the creation of an EcoAccount
Returns:
"""
# Prepare url and form data to be posted
new_url = reverse("compensation:acc:new")
test_id = self.create_dummy_string()
test_title = self.create_dummy_string()
test_geom = self.create_dummy_geometry()
test_deductable_surface = 1000
test_conservation_office = self.get_conservation_office_code()
post_data = {
"identifier": test_id,
"title": test_title,
"geom": test_geom.geojson,
"deductable_surface": test_deductable_surface,
"conservation_office": test_conservation_office.id
}
self.client_user.post(new_url, post_data)
try:
acc = EcoAccount.objects.get(
identifier=test_id
)
except ObjectDoesNotExist:
self.fail(msg="EcoAccount not created")
self.assertEqual(acc.identifier, test_id)
self.assertEqual(acc.title, test_title)
self.assert_equal_geometries(acc.geometry.geom, test_geom)
self.assertEqual(acc.log.count(), 1)
# Expect logs to be set
self.assertEqual(acc.log.count(), 1)
self.assertEqual(acc.log.first().action, UserAction.CREATED)
def test_edit(self):
""" Checks that the editing of an EcoAccount works
Returns:
"""
self.eco_account.share_with(self.superuser)
url = reverse("compensation:acc:edit", args=(self.eco_account.id,))
pre_edit_log_count = self.eco_account.log.count()
new_title = self.create_dummy_string()
new_identifier = self.create_dummy_string()
new_comment = self.create_dummy_string()
new_geometry = MultiPolygon(srid=4326) # Create an empty geometry
test_conservation_office = self.get_conservation_office_code()
test_deductable_surface = 10005
check_on_elements = {
self.eco_account.title: new_title,
self.eco_account.identifier: new_identifier,
self.eco_account.comment: new_comment,
self.eco_account.deductable_surface: test_deductable_surface,
}
for k, v in check_on_elements.items():
self.assertNotEqual(k, v)
post_data = {
"identifier": new_identifier,
"title": new_title,
"comment": new_comment,
"geom": new_geometry.geojson,
"surface": test_deductable_surface,
"conservation_office": test_conservation_office.id
}
self.client_user.post(url, post_data)
self.eco_account.refresh_from_db()
check_on_elements = {
self.eco_account.title: new_title,
self.eco_account.identifier: new_identifier,
self.eco_account.deductable_surface: test_deductable_surface,
self.eco_account.comment: new_comment,
}
for k, v in check_on_elements.items():
self.assertEqual(k, v)
self.assert_equal_geometries(self.eco_account.geometry.geom, new_geometry)
# Expect logs to be set
self.assertEqual(pre_edit_log_count + 1, self.eco_account.log.count())
self.assertEqual(self.eco_account.log.first().action, UserAction.EDITED)
def test_recordability(self):
"""
This tests if the recordability of the EcoAccount is triggered by the quality of it's data (e.g. not all fields filled)
Returns:
"""
# Add proper privilege for the user
self.eco_account.share_with(self.superuser)
pre_record_log_count = self.eco_account.log.count()
# Prepare url and form data
record_url = reverse("compensation:acc:record", args=(self.eco_account.id,))
post_data = {
"confirm": True,
}
self.eco_account.refresh_from_db()
# Make sure the account is not recorded
self.assertIsNone(self.eco_account.recorded)
# Run the request --> expect fail, since the account is not valid, yet
self.client_user.post(record_url, post_data)
# Check that the account is still not recorded
self.assertIsNone(self.eco_account.recorded)
# Now fill out the data for an ecoaccount
self.eco_account = self.fill_out_eco_account(self.eco_account)
# Rerun the request
self.client_user.post(record_url, post_data)
# Expect the EcoAccount now to be recorded
# Attention: We can only test the date part of the timestamp,
# since the delay in microseconds would lead to fail
self.eco_account.refresh_from_db()
recorded = self.eco_account.recorded
self.assertIsNotNone(recorded)
self.assertEqual(self.superuser, recorded.user)
self.assertEqual(UserAction.RECORDED, recorded.action)
self.assertEqual(datetime.date.today(), recorded.timestamp.date())
# Expect the user action to be in the log
self.assertIn(recorded, self.eco_account.log.all())
self.assertEqual(pre_record_log_count + 1, self.eco_account.log.count())
def test_new_deduction(self):
"""
This tests the deductability of an eco account.
An eco account should only be deductible if it is recorded.
Returns:
"""
# Give user shared access to the dummy intervention, which will be needed here
self.intervention.share_with(self.superuser)
pre_deduction_acc_log_count = self.eco_account.log.count()
pre_deduction_int_log_count = self.intervention.log.count()
# Prepare data for deduction creation
deduct_url = reverse("compensation:acc:new-deduction", args=(self.eco_account.id,))
test_surface = 10.00
post_data = {
"surface": test_surface,
"account": self.eco_account.id,
"intervention": self.intervention.id,
}
# Perform request --> expect to fail
self.client_user.post(deduct_url, post_data)
# Expect that no deduction has been created
self.assertEqual(0, self.eco_account.deductions.count())
self.assertEqual(0, self.intervention.deductions.count())
self.assertEqual(pre_deduction_acc_log_count, 0)
self.assertEqual(pre_deduction_int_log_count, 0)
# Now mock the eco account as it would be recorded (with invalid data)
# Make sure the deductible surface is high enough for the request
self.eco_account.set_recorded(self.superuser)
self.eco_account.refresh_from_db()
self.eco_account.deductable_surface = test_surface + 1.00
self.eco_account.save()
self.assertIsNotNone(self.eco_account.recorded)
self.assertGreater(self.eco_account.deductable_surface, test_surface)
# Expect the recorded entry in the log
self.assertEqual(pre_deduction_acc_log_count + 1, self.eco_account.log.count())
self.assertTrue(self.eco_account.log.first().action == UserAction.RECORDED)
# Rerun the request
self.client_user.post(deduct_url, post_data)
# Expect that the deduction has been created
self.assertEqual(1, self.eco_account.deductions.count())
self.assertEqual(1, self.intervention.deductions.count())
deduction = self.eco_account.deductions.first()
self.assertEqual(deduction.surface, test_surface)
self.assertEqual(deduction.account, self.eco_account)
self.assertEqual(deduction.intervention, self.intervention)
# Expect entries in the log
self.assertEqual(pre_deduction_acc_log_count + 2, self.eco_account.log.count())
self.assertTrue(self.eco_account.log.first().action == UserAction.EDITED)
self.assertEqual(pre_deduction_int_log_count + 1, self.intervention.log.count())
self.assertTrue(self.intervention.log.first().action == UserAction.EDITED)
def test_edit_deduction(self):
test_surface = self.eco_account.get_available_rest()[0]
self.eco_account.set_recorded(self.superuser)
self.eco_account.refresh_from_db()
deduction = EcoAccountDeduction.objects.create(
intervention=self.intervention,
account=self.eco_account,
surface=0
)
self.assertEqual(1, self.intervention.deductions.count())
self.assertEqual(1, self.eco_account.deductions.count())
# Prepare url and form data to be posted
new_url = reverse("compensation:acc:edit-deduction", args=(self.eco_account.id, deduction.id))
post_data = {
"intervention": deduction.intervention.id,
"account": deduction.account.id,
"surface": test_surface,
}
pre_edit_intervention_log_count = self.intervention.log.count()
pre_edit_account_log_count = self.eco_account.log.count()
num_deductions_intervention = self.intervention.deductions.count()
num_deductions_account = self.eco_account.deductions.count()
self.client_user.post(new_url, post_data)
self.intervention.refresh_from_db()
self.eco_account.refresh_from_db()
deduction.refresh_from_db()
self.assertEqual(num_deductions_intervention, self.intervention.deductions.count())
self.assertEqual(num_deductions_account, self.eco_account.deductions.count())
self.assertEqual(deduction.surface, test_surface)
# Expect logs to be set
self.assertEqual(pre_edit_intervention_log_count + 1, self.intervention.log.count())
self.assertEqual(pre_edit_account_log_count + 1, self.eco_account.log.count())
self.assertEqual(self.intervention.log.first().action, UserAction.EDITED)
self.assertEqual(self.eco_account.log.first().action, UserAction.EDITED)
def test_remove_deduction(self):
intervention = self.deduction.intervention
account = self.deduction.account
# Prepare url and form data to be posted
new_url = reverse("compensation:acc:remove-deduction", args=(account.id, self.deduction.id))
post_data = {
"confirm": True,
}
intervention.share_with(self.superuser)
account.share_with(self.superuser)
pre_edit_intervention_log_count = intervention.log.count()
pre_edit_account_log_count = account.log.count()
num_deductions_intervention = intervention.deductions.count()
num_deductions_account = account.deductions.count()
self.client_user.post(new_url, post_data)
intervention.refresh_from_db()
account.refresh_from_db()
self.assertEqual(num_deductions_intervention - 1, intervention.deductions.count())
self.assertEqual(num_deductions_account - 1, account.deductions.count())
# Expect logs to be set
self.assertEqual(pre_edit_intervention_log_count + 1, intervention.log.count())
self.assertEqual(pre_edit_account_log_count + 1, account.log.count())
self.assertEqual(intervention.log.first().action, UserAction.EDITED)
self.assertEqual(account.log.first().action, UserAction.EDITED)

View File

@ -0,0 +1,7 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 09.02.22
"""

View File

@ -0,0 +1,156 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 09.02.22
"""
from django.urls import reverse
from django.test.client import Client
from compensation.models import Payment
from konova.settings import DEFAULT_GROUP
from konova.tests.test_views import BaseViewTestCase
class PaymentViewTestCase(BaseViewTestCase):
@classmethod
def setUpTestData(cls) -> None:
super().setUpTestData()
def setUp(self) -> None:
super().setUp()
self.payment = Payment.objects.get_or_create(
intervention=self.intervention,
amount=1,
due_on="2020-01-01",
comment="Testcomment"
)[0]
self.new_url = reverse("compensation:pay:new", args=(self.intervention.id,))
self.edit_url = reverse("compensation:pay:edit", args=(self.intervention.id, self.payment.id))
self.remove_url = reverse("compensation:pay:remove", args=(self.intervention.id, self.payment.id))
def test_anonymous_user(self):
""" Check correct status code for all requests
Assumption: User not logged in
Returns:
"""
client = Client()
success_urls = [
]
fail_urls = [
self.new_url,
self.edit_url,
self.remove_url,
]
self.assert_url_success(client, success_urls)
self.assert_url_fail(client, fail_urls)
def test_logged_in_no_groups_shared(self):
""" Check correct status code for all requests
Assumption: User logged in and has no groups and data is shared
Returns:
"""
client = Client()
client.login(username=self.superuser.username, password=self.superuser_pw)
self.superuser.groups.set([])
self.intervention.share_with_list([self.superuser])
# Since the user has no groups, it does not matter that data has been shared. There SHOULD not be any difference
# to a user without access, since the important permissions are missing
success_urls = [
]
fail_urls = [
self.new_url,
self.edit_url,
self.remove_url,
]
self.assert_url_success(client, success_urls)
self.assert_url_fail(client, fail_urls)
def test_logged_in_no_groups_unshared(self):
""" Check correct status code for all requests
Assumption: User logged in and has no groups and data is not shared
Returns:
"""
client = Client()
client.login(username=self.superuser.username, password=self.superuser_pw)
self.superuser.groups.set([])
# Sharing is inherited by base intervention for compensation. Therefore configure the interventions share state
self.intervention.share_with_list([])
# Since the user has no groups, it does not matter that data is unshared. There SHOULD not be any difference
# to a user having shared access, since all important permissions are missing
success_urls = [
]
fail_urls = [
self.new_url,
self.edit_url,
self.remove_url,
]
self.assert_url_success(client, success_urls)
self.assert_url_fail(client, fail_urls)
def test_logged_in_default_group_shared(self):
""" Check correct status code for all requests
Assumption: User logged in, is default group member and data is shared
--> Default group necessary since all base functionalities depend on this group membership
Returns:
"""
client = Client()
client.login(username=self.superuser.username, password=self.superuser_pw)
group = self.groups.get(name=DEFAULT_GROUP)
self.superuser.groups.set([group])
# Sharing is inherited by base intervention for compensation. Therefore configure the interventions share state
self.intervention.share_with_list([self.superuser])
success_urls = [
self.new_url,
self.edit_url,
self.remove_url,
]
self.assert_url_success(client, success_urls)
def test_logged_in_default_group_unshared(self):
""" Check correct status code for all requests
Assumption: User logged in, is default group member and data is NOT shared
--> Default group necessary since all base functionalities depend on this group membership
Returns:
"""
client = Client()
client.login(username=self.superuser.username, password=self.superuser_pw)
group = self.groups.get(name=DEFAULT_GROUP)
self.superuser.groups.set([group])
# Sharing is inherited by base intervention for compensation. Therefore configure the interventions share state
self.intervention.share_with_list([])
success_urls = [
]
fail_urls = [
self.new_url,
self.edit_url,
self.remove_url,
]
self.assert_url_fail(client, fail_urls)
self.assert_url_success(client, success_urls)

View File

@ -0,0 +1,127 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 09.02.22
"""
from django.core.exceptions import ObjectDoesNotExist
from django.urls import reverse
from compensation.models import Payment
from konova.tests.test_views import BaseWorkflowTestCase
from user.models import UserAction
class PaymentWorkflowTestCase(BaseWorkflowTestCase):
@classmethod
def setUpTestData(cls):
super().setUpTestData()
def setUp(self) -> None:
super().setUp()
# Give the user shared access to the dummy intervention
self.intervention.share_with(self.superuser)
self.payment = Payment.objects.get_or_create(
intervention=self.intervention,
amount=1,
due_on="2020-01-01",
comment="Testcomment"
)[0]
def test_new(self):
""" Test the creation of a payment
Returns:
"""
# Prepare url and form data to be posted
new_url = reverse("compensation:pay:new", args=(self.intervention.id,))
test_amount = 12345
test_due_on = "1970-01-01"
test_comment = self.create_dummy_string()
post_data = {
"amount": test_amount,
"due": test_due_on,
"comment": test_comment,
}
pre_creation_intervention_log_count = self.intervention.log.count()
num_payments = self.intervention.payments.count()
self.client_user.post(new_url, post_data)
self.intervention.refresh_from_db()
self.assertEqual(num_payments + 1, self.intervention.payments.count())
new_payment = self.intervention.payments.get(amount=test_amount)
self.assertEqual(new_payment.amount, test_amount)
self.assertEqual(str(new_payment.due_on), test_due_on)
self.assertEqual(new_payment.comment, test_comment)
# Expect logs to be set
self.assertEqual(pre_creation_intervention_log_count + 1, self.intervention.log.count())
self.assertEqual(self.intervention.log.first().action, UserAction.EDITED)
def test_edit(self):
""" Test edit of a payment
Returns:
"""
# Prepare url and form data to be posted
new_url = reverse("compensation:pay:edit", args=(self.intervention.id, self.payment.id))
test_amount = self.payment.amount * 2
test_due_on = "1970-01-01"
test_comment = self.create_dummy_string()
post_data = {
"amount": test_amount,
"due": test_due_on,
"comment": test_comment,
}
pre_edit_intervention_log_count = self.intervention.log.count()
num_payments = self.intervention.payments.count()
self.client_user.post(new_url, post_data)
self.intervention.refresh_from_db()
self.payment.refresh_from_db()
self.assertEqual(num_payments, self.intervention.payments.count())
self.assertEqual(self.payment.amount, test_amount)
self.assertEqual(str(self.payment.due_on), test_due_on)
self.assertEqual(self.payment.comment, test_comment)
# Expect logs to be set
self.assertEqual(pre_edit_intervention_log_count + 1, self.intervention.log.count())
self.assertEqual(self.intervention.log.first().action, UserAction.EDITED)
def test_remove(self):
""" Test remove of a payment
Returns:
"""
# Prepare url and form data to be posted
new_url = reverse("compensation:pay:remove", args=(self.intervention.id, self.payment.id))
post_data = {
"confirm": True,
}
pre_remove_intervention_log_count = self.intervention.log.count()
num_payments = self.intervention.payments.count()
self.client_user.post(new_url, post_data)
self.intervention.refresh_from_db()
try:
self.payment.refresh_from_db()
self.fail(msg="Payment still exists after delete")
except ObjectDoesNotExist:
pass
self.assertEqual(num_payments - 1, self.intervention.payments.count())
# Expect logs to be set
self.assertEqual(pre_remove_intervention_log_count + 1, self.intervention.log.count())
self.assertEqual(self.intervention.log.first().action, UserAction.EDITED)

View File

@ -1,406 +0,0 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 27.10.21
"""
from django.urls import reverse
from django.test import Client
from konova.settings import DEFAULT_GROUP
from konova.tests.test_views import BaseViewTestCase
class CompensationViewTestCase(BaseViewTestCase):
"""
These tests focus on proper returned views depending on the user's groups privileges and login status
"""
@classmethod
def setUpTestData(cls) -> None:
super().setUpTestData()
state = cls.create_dummy_states()
cls.compensation.before_states.set([state])
cls.compensation.after_states.set([state])
action = cls.create_dummy_action()
cls.compensation.actions.set([action])
# Prepare urls
cls.index_url = reverse("compensation:index", args=())
cls.new_url = reverse("compensation:new", args=(cls.intervention.id,))
cls.new_id_url = reverse("compensation:new-id", args=())
cls.detail_url = reverse("compensation:detail", args=(cls.compensation.id,))
cls.log_url = reverse("compensation:log", args=(cls.compensation.id,))
cls.edit_url = reverse("compensation:edit", args=(cls.compensation.id,))
cls.remove_url = reverse("compensation:remove", args=(cls.compensation.id,))
cls.report_url = reverse("compensation:report", args=(cls.compensation.id,))
cls.state_new_url = reverse("compensation:new-state", args=(cls.compensation.id,))
cls.action_new_url = reverse("compensation:new-action", args=(cls.compensation.id,))
cls.deadline_new_url = reverse("compensation:new-deadline", args=(cls.compensation.id,))
cls.new_doc_url = reverse("compensation:new-doc", args=(cls.compensation.id,))
cls.state_remove_url = reverse("compensation:state-remove", args=(cls.compensation.id, cls.comp_state.id,))
cls.action_remove_url = reverse("compensation:action-remove", args=(cls.compensation.id, cls.comp_action.id,))
def test_anonymous_user(self):
""" Check correct status code for all requests
Assumption: User not logged in
Returns:
"""
client = Client()
success_urls = [
self.report_url,
]
fail_urls = [
self.index_url,
self.detail_url,
self.new_url,
self.new_id_url,
self.log_url,
self.edit_url,
self.remove_url,
self.state_new_url,
self.action_new_url,
self.deadline_new_url,
self.state_remove_url,
self.action_remove_url,
self.new_doc_url,
]
self.assert_url_success(client, success_urls)
self.assert_url_fail(client, fail_urls)
def test_logged_in_no_groups_shared(self):
""" Check correct status code for all requests
Assumption: User logged in and has no groups and data is shared
Returns:
"""
client = Client()
client.login(username=self.superuser.username, password=self.superuser_pw)
self.superuser.groups.set([])
self.intervention.share_with_list([self.superuser])
# Since the user has no groups, it does not matter that data has been shared. There SHOULD not be any difference
# to a user without access, since the important permissions are missing
success_urls = [
self.index_url,
self.detail_url,
self.report_url,
]
fail_urls = [
self.new_url,
self.new_id_url,
self.log_url,
self.edit_url,
self.remove_url,
self.state_new_url,
self.action_new_url,
self.deadline_new_url,
self.state_remove_url,
self.action_remove_url,
self.new_doc_url,
]
self.assert_url_success(client, success_urls)
self.assert_url_fail(client, fail_urls)
def test_logged_in_no_groups_unshared(self):
""" Check correct status code for all requests
Assumption: User logged in and has no groups and data is shared
Returns:
"""
client = Client()
client.login(username=self.superuser.username, password=self.superuser_pw)
self.superuser.groups.set([])
# Sharing is inherited by base intervention for compensation. Therefore configure the interventions share state
self.intervention.share_with_list([])
# Since the user has no groups, it does not matter that data is unshared. There SHOULD not be any difference
# to a user having shared access, since all important permissions are missing
success_urls = [
self.index_url,
self.detail_url,
self.report_url,
]
fail_urls = [
self.new_url,
self.new_id_url,
self.log_url,
self.edit_url,
self.remove_url,
self.state_new_url,
self.action_new_url,
self.deadline_new_url,
self.state_remove_url,
self.action_remove_url,
self.new_doc_url,
]
self.assert_url_success(client, success_urls)
self.assert_url_fail(client, fail_urls)
def test_logged_in_default_group_shared(self):
""" Check correct status code for all requests
Assumption: User logged in, is default group member and data is shared
--> Default group necessary since all base functionalities depend on this group membership
Returns:
"""
client = Client()
client.login(username=self.superuser.username, password=self.superuser_pw)
group = self.groups.get(name=DEFAULT_GROUP)
self.superuser.groups.set([group])
# Sharing is inherited by base intervention for compensation. Therefore configure the interventions share state
self.intervention.share_with_list([self.superuser])
success_urls = [
self.index_url,
self.detail_url,
self.report_url,
self.new_url,
self.new_id_url,
self.edit_url,
self.state_new_url,
self.action_new_url,
self.deadline_new_url,
self.state_remove_url,
self.action_remove_url,
self.new_doc_url,
self.log_url,
self.remove_url,
]
self.assert_url_success(client, success_urls)
def test_logged_in_default_group_unshared(self):
""" Check correct status code for all requests
Assumption: User logged in, is default group member and data is NOT shared
--> Default group necessary since all base functionalities depend on this group membership
Returns:
"""
client = Client()
client.login(username=self.superuser.username, password=self.superuser_pw)
group = self.groups.get(name=DEFAULT_GROUP)
self.superuser.groups.set([group])
# Sharing is inherited by base intervention for compensation. Therefore configure the interventions share state
self.intervention.share_with_list([])
success_urls = [
self.index_url,
self.detail_url,
self.report_url,
self.new_id_url,
]
fail_urls = [
self.new_url,
self.edit_url,
self.state_new_url,
self.action_new_url,
self.deadline_new_url,
self.state_remove_url,
self.action_remove_url,
self.new_doc_url,
self.log_url,
self.remove_url,
]
self.assert_url_fail(client, fail_urls)
self.assert_url_success(client, success_urls)
class EcoAccountViewTestCase(CompensationViewTestCase):
"""
These tests focus on proper returned views depending on the user's groups privileges and login status
EcoAccounts can inherit the same tests used for compensations.
"""
comp_state = None
comp_action = None
@classmethod
def setUpTestData(cls) -> None:
super().setUpTestData()
state = cls.create_dummy_states()
cls.eco_account.before_states.set([state])
cls.eco_account.after_states.set([state])
action = cls.create_dummy_action()
cls.eco_account.actions.set([action])
# Prepare urls
cls.index_url = reverse("compensation:acc:index", args=())
cls.new_url = reverse("compensation:acc:new", args=())
cls.new_id_url = reverse("compensation:acc:new-id", args=())
cls.detail_url = reverse("compensation:acc:detail", args=(cls.eco_account.id,))
cls.log_url = reverse("compensation:acc:log", args=(cls.eco_account.id,))
cls.edit_url = reverse("compensation:acc:edit", args=(cls.eco_account.id,))
cls.remove_url = reverse("compensation:acc:remove", args=(cls.eco_account.id,))
cls.report_url = reverse("compensation:acc:report", args=(cls.eco_account.id,))
cls.state_new_url = reverse("compensation:acc:new-state", args=(cls.eco_account.id,))
cls.action_new_url = reverse("compensation:acc:new-action", args=(cls.eco_account.id,))
cls.deadline_new_url = reverse("compensation:acc:new-deadline", args=(cls.eco_account.id,))
cls.new_doc_url = reverse("compensation:acc:new-doc", args=(cls.eco_account.id,))
cls.state_remove_url = reverse("compensation:acc:state-remove", args=(cls.eco_account.id, cls.comp_state.id,))
cls.action_remove_url = reverse("compensation:acc:action-remove", args=(cls.eco_account.id, cls.comp_action.id,))
def test_logged_in_no_groups_shared(self):
""" Check correct status code for all requests
Assumption: User logged in and has no groups and data is shared
Returns:
"""
client = Client()
client.login(username=self.superuser.username, password=self.superuser_pw)
self.superuser.groups.set([])
self.eco_account.share_with_list([self.superuser])
# Since the user has no groups, it does not matter that data has been shared. There SHOULD not be any difference
# to a user without access, since the important permissions are missing
success_urls = [
self.index_url,
self.detail_url,
self.report_url,
]
fail_urls = [
self.new_url,
self.new_id_url,
self.log_url,
self.edit_url,
self.remove_url,
self.state_new_url,
self.action_new_url,
self.deadline_new_url,
self.state_remove_url,
self.action_remove_url,
self.new_doc_url,
]
self.assert_url_success(client, success_urls)
self.assert_url_fail(client, fail_urls)
def test_logged_in_no_groups_unshared(self):
""" Check correct status code for all requests
Assumption: User logged in and has no groups and data is shared
Returns:
"""
client = Client()
client.login(username=self.superuser.username, password=self.superuser_pw)
self.superuser.groups.set([])
self.eco_account.share_with_list([])
# Since the user has no groups, it does not matter that data is unshared. There SHOULD not be any difference
# to a user having shared access, since all important permissions are missing
success_urls = [
self.index_url,
self.detail_url,
self.report_url,
]
fail_urls = [
self.new_url,
self.new_id_url,
self.log_url,
self.edit_url,
self.remove_url,
self.state_new_url,
self.action_new_url,
self.deadline_new_url,
self.state_remove_url,
self.action_remove_url,
self.new_doc_url,
]
self.assert_url_success(client, success_urls)
self.assert_url_fail(client, fail_urls)
def test_logged_in_default_group_shared(self):
""" Check correct status code for all requests
Assumption: User logged in, is default group member and data is shared
--> Default group necessary since all base functionalities depend on this group membership
Returns:
"""
client = Client()
client.login(username=self.superuser.username, password=self.superuser_pw)
group = self.groups.get(name=DEFAULT_GROUP)
self.superuser.groups.set([group])
# Sharing is inherited by base intervention for compensation. Therefore configure the interventions share state
self.eco_account.share_with_list([self.superuser])
success_urls = [
self.index_url,
self.detail_url,
self.report_url,
self.new_url,
self.new_id_url,
self.edit_url,
self.state_new_url,
self.action_new_url,
self.deadline_new_url,
self.state_remove_url,
self.action_remove_url,
self.new_doc_url,
self.log_url,
self.remove_url,
]
self.assert_url_success(client, success_urls)
def test_logged_in_default_group_unshared(self):
""" Check correct status code for all requests
Assumption: User logged in, is default group member and data is NOT shared
--> Default group necessary since all base functionalities depend on this group membership
Returns:
"""
client = Client()
client.login(username=self.superuser.username, password=self.superuser_pw)
group = self.groups.get(name=DEFAULT_GROUP)
self.superuser.groups.set([group])
self.eco_account.share_with_list([])
success_urls = [
self.index_url,
self.detail_url,
self.report_url,
self.new_id_url,
self.new_url,
]
fail_urls = [
self.edit_url,
self.state_new_url,
self.action_new_url,
self.deadline_new_url,
self.state_remove_url,
self.action_remove_url,
self.new_doc_url,
self.log_url,
self.remove_url,
]
self.assert_url_fail(client, fail_urls)
self.assert_url_success(client, success_urls)

View File

@ -18,17 +18,24 @@ urlpatterns = [
path('<id>/log', log_view, name='log'), path('<id>/log', log_view, name='log'),
path('<id>/edit', edit_view, name='edit'), path('<id>/edit', edit_view, name='edit'),
path('<id>/remove', remove_view, name='remove'), path('<id>/remove', remove_view, name='remove'),
path('<id>/state/new', state_new_view, name='new-state'), path('<id>/state/new', state_new_view, name='new-state'),
path('<id>/action/new', action_new_view, name='new-action'), path('<id>/state/<state_id>/edit', state_edit_view, name='state-edit'),
path('<id>/state/<state_id>/remove', state_remove_view, name='state-remove'), path('<id>/state/<state_id>/remove', state_remove_view, name='state-remove'),
path('<id>/action/new', action_new_view, name='new-action'),
path('<id>/action/<action_id>/edit', action_edit_view, name='action-edit'),
path('<id>/action/<action_id>/remove', action_remove_view, name='action-remove'), path('<id>/action/<action_id>/remove', action_remove_view, name='action-remove'),
path('<id>/deadline/new', deadline_new_view, name="new-deadline"), path('<id>/deadline/new', deadline_new_view, name="new-deadline"),
path('<id>/deadline/<deadline_id>/edit', deadline_edit_view, name='deadline-edit'),
path('<id>/deadline/<deadline_id>/remove', deadline_remove_view, name='deadline-remove'), path('<id>/deadline/<deadline_id>/remove', deadline_remove_view, name='deadline-remove'),
path('<id>/report', report_view, name='report'), path('<id>/report', report_view, name='report'),
# Documents # Documents
path('<id>/document/new/', new_document_view, name='new-doc'), path('<id>/document/new/', new_document_view, name='new-doc'),
path('document/<doc_id>', get_document_view, name='get-doc'), path('<id>/document/<doc_id>', get_document_view, name='get-doc'),
path('document/<doc_id>/remove/', remove_document_view, name='remove-doc'), path('<id>/document/<doc_id>/remove/', remove_document_view, name='remove-doc'),
path('<id>/document/<doc_id>/edit/', edit_document_view, name='edit-doc'),
] ]

View File

@ -19,22 +19,31 @@ urlpatterns = [
path('<id>/report', report_view, name='report'), path('<id>/report', report_view, name='report'),
path('<id>/edit', edit_view, name='edit'), path('<id>/edit', edit_view, name='edit'),
path('<id>/remove', remove_view, name='remove'), path('<id>/remove', remove_view, name='remove'),
path('<id>/state/new', state_new_view, name='new-state'), path('<id>/state/new', state_new_view, name='new-state'),
path('<id>/action/new', action_new_view, name='new-action'), path('<id>/state/<state_id>/edit', state_edit_view, name='state-edit'),
path('<id>/state/<state_id>/remove', state_remove_view, name='state-remove'), path('<id>/state/<state_id>/remove', state_remove_view, name='state-remove'),
path('<id>/action/new', action_new_view, name='new-action'),
path('<id>/action/<action_id>/edit', action_edit_view, name='action-edit'),
path('<id>/action/<action_id>/remove', action_remove_view, name='action-remove'), path('<id>/action/<action_id>/remove', action_remove_view, name='action-remove'),
path('<id>/deadline/<deadline_id>/remove', deadline_remove_view, name='deadline-remove'),
path('<id>/deadline/new', deadline_new_view, name="new-deadline"), path('<id>/deadline/new', deadline_new_view, name="new-deadline"),
path('<id>/deadline/<deadline_id>/edit', deadline_edit_view, name='deadline-edit'),
path('<id>/deadline/<deadline_id>/remove', deadline_remove_view, name='deadline-remove'),
path('<id>/share/<token>', share_view, name='share'), path('<id>/share/<token>', share_view, name='share'),
path('<id>/share', create_share_view, name='share-create'), path('<id>/share', create_share_view, name='share-create'),
# Documents # Documents
path('<id>/document/new/', new_document_view, name='new-doc'), path('<id>/document/new/', new_document_view, name='new-doc'),
path('document/<doc_id>', get_document_view, name='get-doc'), path('<id>/document/<doc_id>', get_document_view, name='get-doc'),
path('document/<doc_id>/remove/', remove_document_view, name='remove-doc'), path('<id>/document/<doc_id>/edit', edit_document_view, name='edit-doc'),
path('<id>/document/<doc_id>/remove/', remove_document_view, name='remove-doc'),
# Eco-account deductions # Eco-account deductions
path('<id>/remove/<deduction_id>', deduction_remove_view, name='remove-deduction'), path('<id>/deduction/<deduction_id>/remove', deduction_remove_view, name='remove-deduction'),
path('<id>/deduction/<deduction_id>/edit', deduction_edit_view, name='edit-deduction'),
path('<id>/deduct/new', new_deduction_view, name='new-deduction'), path('<id>/deduct/new', new_deduction_view, name='new-deduction'),
] ]

View File

@ -10,6 +10,7 @@ from compensation.views.payment import *
app_name = "pay" app_name = "pay"
urlpatterns = [ urlpatterns = [
path('<intervention_id>/new', new_payment_view, name='new'), path('<id>/new', new_payment_view, name='new'),
path('<id>/remove', payment_remove_view, name='remove'), path('<id>/remove/<payment_id>', payment_remove_view, name='remove'),
path('<id>/edit/<payment_id>', payment_edit_view, name='edit'),
] ]

View File

@ -6,19 +6,23 @@ 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 NewCompensationDocumentModalForm, RemoveCompensationActionModalForm, RemoveCompensationStateModalForm, \
EditCompensationStateModalForm, EditCompensationActionModalForm, EditDeadlineModalForm
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 from konova.forms import RemoveModalForm, SimpleGeomForm, RemoveDeadlineModalForm, EditDocumentModalForm
from konova.models import Deadline from konova.models import Deadline
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
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, \
CHECKED_RECORDED_RESET, COMPENSATION_ADDED_TEMPLATE, COMPENSATION_REMOVED_TEMPLATE, DOCUMENT_ADDED CHECKED_RECORDED_RESET, COMPENSATION_ADDED_TEMPLATE, COMPENSATION_REMOVED_TEMPLATE, DOCUMENT_ADDED, \
COMPENSATION_STATE_REMOVED, COMPENSATION_STATE_ADDED, COMPENSATION_ACTION_REMOVED, COMPENSATION_ACTION_ADDED, \
DEADLINE_ADDED, DEADLINE_REMOVED, DOCUMENT_EDITED, COMPENSATION_STATE_EDITED, COMPENSATION_ACTION_EDITED, \
DEADLINE_EDITED
from konova.utils.user_checks import in_group from konova.utils.user_checks import in_group
@ -274,7 +278,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 = NewCompensationDocumentForm(request.POST or None, request.FILES or None, instance=comp, request=request) form = NewCompensationDocumentModalForm(request.POST or None, request.FILES or None, instance=comp, request=request)
return form.process_request( return form.process_request(
request, request,
msg_success=DOCUMENT_ADDED, msg_success=DOCUMENT_ADDED,
@ -284,45 +288,42 @@ def new_document_view(request: HttpRequest, id: str):
@login_required @login_required
@default_group_required @default_group_required
def get_document_view(request: HttpRequest, doc_id: str): @shared_access_required(Compensation, "id")
def get_document_view(request: HttpRequest, id: str, doc_id: str):
""" Returns the document as downloadable file """ Returns the document as downloadable file
Wraps the generic document fetcher function from konova.utils. Wraps the generic document fetcher function from konova.utils.
Args: Args:
request (HttpRequest): The incoming request request (HttpRequest): The incoming request
id (str): The compensation id
doc_id (str): The document id doc_id (str): The document id
Returns: Returns:
""" """
comp = get_object_or_404(Compensation, id=id)
doc = get_object_or_404(CompensationDocument, id=doc_id) doc = get_object_or_404(CompensationDocument, id=doc_id)
user = request.user
instance = doc.instance
# File download only possible if related instance is shared with user
if not instance.users.filter(id=user.id):
messages.info(
request,
DATA_UNSHARED
)
return redirect("compensation:detail", id=instance.id)
return get_document(doc) return get_document(doc)
@login_required @login_required
@default_group_required @default_group_required
def remove_document_view(request: HttpRequest, doc_id: str): @shared_access_required(Compensation, "id")
def remove_document_view(request: HttpRequest, id: str, doc_id: str):
""" Removes the document from the database and file system """ Removes the document from the database and file system
Wraps the generic functionality from konova.utils. Wraps the generic functionality from konova.utils.
Args: Args:
request (HttpRequest): The incoming request request (HttpRequest): The incoming request
id (str): The compensation id
doc_id (str): The document id doc_id (str): The document id
Returns: Returns:
""" """
comp = get_object_or_404(Compensation, id=id)
doc = get_object_or_404(CompensationDocument, id=doc_id) doc = get_object_or_404(CompensationDocument, id=doc_id)
return remove_document( return remove_document(
request, request,
@ -330,6 +331,32 @@ def remove_document_view(request: HttpRequest, doc_id: str):
) )
@login_required
@default_group_required
@shared_access_required(Compensation, "id")
def edit_document_view(request: HttpRequest, id: str, doc_id: str):
""" Removes the document from the database and file system
Wraps the generic functionality from konova.utils.
Args:
request (HttpRequest): The incoming request
id (str): The compensation id
doc_id (str): The document id
Returns:
"""
comp = get_object_or_404(Compensation, id=id)
doc = get_object_or_404(CompensationDocument, id=doc_id)
form = EditDocumentModalForm(request.POST or None, request.FILES or None, instance=comp, document=doc, request=request)
return form.process_request(
request,
DOCUMENT_EDITED,
reverse("compensation:detail", args=(id,)) + "#related_data"
)
@login_required @login_required
@default_group_required @default_group_required
@shared_access_required(Compensation, "id") @shared_access_required(Compensation, "id")
@ -347,7 +374,7 @@ def state_new_view(request: HttpRequest, id: str):
form = NewStateModalForm(request.POST or None, instance=comp, request=request) form = NewStateModalForm(request.POST or None, instance=comp, request=request)
return form.process_request( return form.process_request(
request, request,
msg_success=_("State added"), msg_success=COMPENSATION_STATE_ADDED,
redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data" redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data"
) )
@ -369,7 +396,31 @@ def action_new_view(request: HttpRequest, id: str):
form = NewActionModalForm(request.POST or None, instance=comp, request=request) form = NewActionModalForm(request.POST or None, instance=comp, request=request)
return form.process_request( return form.process_request(
request, request,
msg_success=_("Action added"), msg_success=COMPENSATION_ACTION_ADDED,
redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data"
)
@login_required
@default_group_required
@shared_access_required(Compensation, "id")
def action_edit_view(request: HttpRequest, id: str, action_id: str):
""" Renders a form for editing actions for a compensation
Args:
request (HttpRequest): The incoming request
id (str): The compensation's id
action_id (str): The action's id
Returns:
"""
comp = get_object_or_404(Compensation, id=id)
action = get_object_or_404(CompensationAction, id=action_id)
form = EditCompensationActionModalForm(request.POST or None, instance=comp, action=action, request=request)
return form.process_request(
request,
msg_success=COMPENSATION_ACTION_EDITED,
redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data" redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data"
) )
@ -391,7 +442,31 @@ def deadline_new_view(request: HttpRequest, id: str):
form = NewDeadlineModalForm(request.POST or None, instance=comp, request=request) form = NewDeadlineModalForm(request.POST or None, instance=comp, request=request)
return form.process_request( return form.process_request(
request, request,
msg_success=_("Deadline added"), msg_success=DEADLINE_ADDED,
redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data"
)
@login_required
@default_group_required
@shared_access_required(Compensation, "id")
def deadline_edit_view(request: HttpRequest, id: str, deadline_id: str):
""" Renders a form for editing deadlines from a compensation
Args:
request (HttpRequest): The incoming request
id (str): The compensation's id
deadline_id (str): The deadline's id
Returns:
"""
comp = get_object_or_404(Compensation, id=id)
deadline = get_object_or_404(Deadline, id=deadline_id)
form = EditDeadlineModalForm(request.POST or None, instance=comp, deadline=deadline, request=request)
return form.process_request(
request,
msg_success=DEADLINE_EDITED,
redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data" redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data"
) )
@ -410,11 +485,12 @@ def deadline_remove_view(request: HttpRequest, id: str, deadline_id: str):
Returns: Returns:
""" """
comp = get_object_or_404(Compensation, id=id)
deadline = get_object_or_404(Deadline, id=deadline_id) deadline = get_object_or_404(Deadline, id=deadline_id)
form = RemoveModalForm(request.POST or None, instance=deadline, request=request) form = RemoveDeadlineModalForm(request.POST or None, instance=comp, deadline=deadline, request=request)
return form.process_request( return form.process_request(
request, request,
msg_success=_("Deadline removed"), msg_success=DEADLINE_REMOVED,
redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data" redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data"
) )
@ -433,11 +509,36 @@ def state_remove_view(request: HttpRequest, id: str, state_id: str):
Returns: Returns:
""" """
comp = get_object_or_404(Compensation, id=id)
state = get_object_or_404(CompensationState, id=state_id) state = get_object_or_404(CompensationState, id=state_id)
form = RemoveModalForm(request.POST or None, instance=state, request=request) form = RemoveCompensationStateModalForm(request.POST or None, instance=comp, state=state, request=request)
return form.process_request( return form.process_request(
request, request,
msg_success=_("State removed"), msg_success=COMPENSATION_STATE_REMOVED,
redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data"
)
@login_required
@default_group_required
@shared_access_required(Compensation, "id")
def state_edit_view(request: HttpRequest, id: str, state_id: str):
""" Renders a form for editing a compensation state
Args:
request (HttpRequest): The incoming request
id (str): The compensation's id
state_id (str): The state's id
Returns:
"""
comp = get_object_or_404(Compensation, id=id)
state = get_object_or_404(CompensationState, id=state_id)
form = EditCompensationStateModalForm(request.POST or None, instance=comp, state=state, request=request)
return form.process_request(
request,
msg_success=COMPENSATION_STATE_EDITED,
redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data" redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data"
) )
@ -456,11 +557,12 @@ def action_remove_view(request: HttpRequest, id: str, action_id: str):
Returns: Returns:
""" """
comp = get_object_or_404(Compensation, id=id)
action = get_object_or_404(CompensationAction, id=action_id) action = get_object_or_404(CompensationAction, id=action_id)
form = RemoveModalForm(request.POST or None, instance=action, request=request) form = RemoveCompensationActionModalForm(request.POST or None, instance=comp, action=action, request=request)
return form.process_request( return form.process_request(
request, request,
msg_success=_("Action removed"), msg_success=COMPENSATION_ACTION_REMOVED,
redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data" redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data"
) )

View File

@ -16,21 +16,26 @@ 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 NewEcoAccountDocumentModalForm, RemoveCompensationActionModalForm, RemoveCompensationStateModalForm, \
EditCompensationStateModalForm, EditCompensationActionModalForm, EditDeadlineModalForm
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, ShareModalForm from intervention.forms.modalForms import NewDeductionModalForm, ShareModalForm, RemoveEcoAccountDeductionModalForm, \
EditEcoAccountDeductionModalForm
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
from konova.forms import RemoveModalForm, SimpleGeomForm, NewDocumentForm, RecordModalForm from konova.forms import RemoveModalForm, SimpleGeomForm, NewDocumentModalForm, RecordModalForm, \
RemoveDeadlineModalForm, EditDocumentModalForm
from konova.models import Deadline from konova.models import Deadline
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
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 IDENTIFIER_REPLACED, FORM_INVALID, DATA_UNSHARED, DATA_UNSHARED_EXPLANATION, \ from konova.utils.message_templates import IDENTIFIER_REPLACED, FORM_INVALID, DATA_UNSHARED, DATA_UNSHARED_EXPLANATION, \
CANCEL_ACC_RECORDED_OR_DEDUCTED, DEDUCTION_REMOVED, DEDUCTION_ADDED, DOCUMENT_ADDED CANCEL_ACC_RECORDED_OR_DEDUCTED, DEDUCTION_REMOVED, DEDUCTION_ADDED, DOCUMENT_ADDED, COMPENSATION_STATE_REMOVED, \
COMPENSATION_STATE_ADDED, COMPENSATION_ACTION_REMOVED, COMPENSATION_ACTION_ADDED, DEADLINE_ADDED, DEADLINE_REMOVED, \
DEDUCTION_EDITED, DOCUMENT_EDITED, COMPENSATION_STATE_EDITED, COMPENSATION_ACTION_EDITED, DEADLINE_EDITED
from konova.utils.user_checks import in_group from konova.utils.user_checks import in_group
@ -285,7 +290,7 @@ def deduction_remove_view(request: HttpRequest, id: str, deduction_id: str):
except ObjectDoesNotExist: except ObjectDoesNotExist:
raise Http404("Unknown deduction") raise Http404("Unknown deduction")
form = RemoveModalForm(request.POST or None, instance=eco_deduction, request=request) form = RemoveEcoAccountDeductionModalForm(request.POST or None, instance=acc, deduction=eco_deduction, request=request)
return form.process_request( return form.process_request(
request=request, request=request,
msg_success=DEDUCTION_REMOVED, msg_success=DEDUCTION_REMOVED,
@ -293,6 +298,34 @@ def deduction_remove_view(request: HttpRequest, id: str, deduction_id: str):
) )
@login_required
@default_group_required
@shared_access_required(EcoAccount, "id")
def deduction_edit_view(request: HttpRequest, id: str, deduction_id: str):
""" Renders a modal view for editing deductions
Args:
request (HttpRequest): The incoming request
id (str): The eco account's id
deduction_id (str): The deduction's id
Returns:
"""
acc = get_object_or_404(EcoAccount, id=id)
try:
eco_deduction = acc.deductions.get(id=deduction_id)
except ObjectDoesNotExist:
raise Http404("Unknown deduction")
form = EditEcoAccountDeductionModalForm(request.POST or None, instance=acc, deduction=eco_deduction, request=request)
return form.process_request(
request=request,
msg_success=DEDUCTION_EDITED,
redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data"
)
@login_required @login_required
@default_group_required @default_group_required
@shared_access_required(EcoAccount, "id") @shared_access_required(EcoAccount, "id")
@ -359,7 +392,7 @@ def state_new_view(request: HttpRequest, id: str):
form = NewStateModalForm(request.POST or None, instance=acc, request=request) form = NewStateModalForm(request.POST or None, instance=acc, request=request)
return form.process_request( return form.process_request(
request, request,
msg_success=_("State added"), msg_success=COMPENSATION_STATE_ADDED,
redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data" redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data"
) )
@ -381,7 +414,7 @@ def action_new_view(request: HttpRequest, id: str):
form = NewActionModalForm(request.POST or None, instance=acc, request=request) form = NewActionModalForm(request.POST or None, instance=acc, request=request)
return form.process_request( return form.process_request(
request, request,
msg_success=_("Action added"), msg_success=COMPENSATION_ACTION_ADDED,
redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data" redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data"
) )
@ -400,11 +433,36 @@ def state_remove_view(request: HttpRequest, id: str, state_id: str):
Returns: Returns:
""" """
acc = get_object_or_404(EcoAccount, id=id)
state = get_object_or_404(CompensationState, id=state_id) state = get_object_or_404(CompensationState, id=state_id)
form = RemoveModalForm(request.POST or None, instance=state, request=request) form = RemoveCompensationStateModalForm(request.POST or None, instance=acc, state=state, request=request)
return form.process_request( return form.process_request(
request, request,
msg_success=_("State removed"), msg_success=COMPENSATION_STATE_REMOVED,
redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data"
)
@login_required
@default_group_required
@shared_access_required(EcoAccount, "id")
def state_edit_view(request: HttpRequest, id: str, state_id: str):
""" Renders a form for editing a compensation state
Args:
request (HttpRequest): The incoming request
id (str): The compensation's id
state_id (str): The state's id
Returns:
"""
acc = get_object_or_404(EcoAccount, id=id)
state = get_object_or_404(CompensationState, id=state_id)
form = EditCompensationStateModalForm(request.POST or None, instance=acc, state=state, request=request)
return form.process_request(
request,
msg_success=COMPENSATION_STATE_EDITED,
redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data" redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data"
) )
@ -423,11 +481,60 @@ def action_remove_view(request: HttpRequest, id: str, action_id: str):
Returns: Returns:
""" """
acc = get_object_or_404(EcoAccount, id=id)
action = get_object_or_404(CompensationAction, id=action_id) action = get_object_or_404(CompensationAction, id=action_id)
form = RemoveModalForm(request.POST or None, instance=action, request=request) form = RemoveCompensationActionModalForm(request.POST or None, instance=acc, action=action, request=request)
return form.process_request( return form.process_request(
request, request,
msg_success=_("Action removed"), msg_success=COMPENSATION_ACTION_REMOVED,
redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data"
)
@login_required
@default_group_required
@shared_access_required(EcoAccount, "id")
def action_edit_view(request: HttpRequest, id: str, action_id: str):
""" Renders a form for editing a compensation action
Args:
request (HttpRequest): The incoming request
id (str): The compensation's id
id (str): The action's id
Returns:
"""
acc = get_object_or_404(EcoAccount, id=id)
action = get_object_or_404(CompensationAction, id=action_id)
form = EditCompensationActionModalForm(request.POST or None, instance=acc, action=action, request=request)
return form.process_request(
request,
msg_success=COMPENSATION_ACTION_EDITED,
redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data"
)
@login_required
@default_group_required
@shared_access_required(EcoAccount, "id")
def deadline_edit_view(request: HttpRequest, id: str, deadline_id: str):
""" Renders a form for editing deadlines from a compensation
Args:
request (HttpRequest): The incoming request
id (str): The compensation's id
deadline_id (str): The deadline's id
Returns:
"""
comp = get_object_or_404(EcoAccount, id=id)
deadline = get_object_or_404(Deadline, id=deadline_id)
form = EditDeadlineModalForm(request.POST or None, instance=comp, deadline=deadline, request=request)
return form.process_request(
request,
msg_success=DEADLINE_EDITED,
redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data" redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data"
) )
@ -446,11 +553,12 @@ def deadline_remove_view(request: HttpRequest, id: str, deadline_id: str):
Returns: Returns:
""" """
comp = get_object_or_404(EcoAccount, id=id)
deadline = get_object_or_404(Deadline, id=deadline_id) deadline = get_object_or_404(Deadline, id=deadline_id)
form = RemoveModalForm(request.POST or None, instance=deadline, request=request) form = RemoveDeadlineModalForm(request.POST or None, instance=comp, deadline=deadline, request=request)
return form.process_request( return form.process_request(
request, request,
msg_success=_("Deadline removed"), msg_success=DEADLINE_REMOVED,
redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data" redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data"
) )
@ -472,7 +580,7 @@ def deadline_new_view(request: HttpRequest, id: str):
form = NewDeadlineModalForm(request.POST or None, instance=acc, request=request) form = NewDeadlineModalForm(request.POST or None, instance=acc, request=request)
return form.process_request( return form.process_request(
request, request,
msg_success=_("Deadline added"), msg_success=DEADLINE_ADDED,
redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data" redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data"
) )
@ -490,7 +598,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 = NewEcoAccountDocumentForm(request.POST or None, request.FILES or None, instance=acc, request=request) form = NewEcoAccountDocumentModalForm(request.POST or None, request.FILES or None, instance=acc, request=request)
return form.process_request( return form.process_request(
request, request,
msg_success=DOCUMENT_ADDED, msg_success=DOCUMENT_ADDED,
@ -500,46 +608,68 @@ def new_document_view(request: HttpRequest, id: str):
@login_required @login_required
@default_group_required @default_group_required
def get_document_view(request: HttpRequest, doc_id: str): @shared_access_required(EcoAccount, "id")
def get_document_view(request: HttpRequest, id:str, doc_id: str):
""" Returns the document as downloadable file """ Returns the document as downloadable file
Wraps the generic document fetcher function from konova.utils. Wraps the generic document fetcher function from konova.utils.
Args: Args:
request (HttpRequest): The incoming request request (HttpRequest): The incoming request
id (str): The account id
doc_id (str): The document id doc_id (str): The document id
Returns: Returns:
""" """
acc = get_object_or_404(EcoAccount, id=id)
doc = get_object_or_404(EcoAccountDocument, id=doc_id) doc = get_object_or_404(EcoAccountDocument, id=doc_id)
user = request.user
instance = doc.instance
# File download only possible if related instance is shared with user
if not instance.users.filter(id=user.id):
messages.info(
request,
DATA_UNSHARED
)
return redirect("compensation:acc:detail", id=instance.id)
return get_document(doc) return get_document(doc)
@login_required @login_required
@default_group_required @default_group_required
@shared_access_required(EcoAccount, "id") @shared_access_required(EcoAccount, "id")
def remove_document_view(request: HttpRequest, doc_id: str): def edit_document_view(request: HttpRequest, id: str, doc_id: str):
""" Removes the document from the database and file system """ Removes the document from the database and file system
Wraps the generic functionality from konova.utils. Wraps the generic functionality from konova.utils.
Args: Args:
request (HttpRequest): The incoming request request (HttpRequest): The incoming request
id (str): The account id
doc_id (str): The document id doc_id (str): The document id
Returns: Returns:
""" """
acc = get_object_or_404(EcoAccount, id=id)
doc = get_object_or_404(EcoAccountDocument, id=doc_id)
form = EditDocumentModalForm(request.POST or None, request.FILES or None, instance=acc, document=doc, request=request)
return form.process_request(
request,
DOCUMENT_EDITED,
reverse("compensation:acc:detail", args=(id,)) + "#related_data"
)
@login_required
@default_group_required
@shared_access_required(EcoAccount, "id")
def remove_document_view(request: HttpRequest, id: str, doc_id: str):
""" Removes the document from the database and file system
Wraps the generic functionality from konova.utils.
Args:
request (HttpRequest): The incoming request
id (str): The account id
doc_id (str): The document id
Returns:
"""
acc = get_object_or_404(EcoAccount, id=id)
doc = get_object_or_404(EcoAccountDocument, id=doc_id) doc = get_object_or_404(EcoAccountDocument, id=doc_id)
return remove_document( return remove_document(
request, request,

View File

@ -11,52 +11,80 @@ from django.contrib.auth.decorators import login_required
from django.http import HttpRequest from django.http import HttpRequest
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from compensation.forms.modalForms import NewPaymentForm from compensation.forms.modalForms import NewPaymentForm, RemovePaymentModalForm, EditPaymentModalForm
from compensation.models import Payment from compensation.models import Payment
from intervention.models import Intervention from intervention.models import Intervention
from konova.decorators import default_group_required from konova.decorators import default_group_required, shared_access_required
from konova.forms import RemoveModalForm from konova.forms import RemoveModalForm
from konova.utils.message_templates import PAYMENT_ADDED, PAYMENT_REMOVED from konova.utils.message_templates import PAYMENT_ADDED, PAYMENT_REMOVED, PAYMENT_EDITED
@login_required @login_required
@default_group_required @default_group_required
def new_payment_view(request: HttpRequest, intervention_id: str): @shared_access_required(Intervention, "id")
def new_payment_view(request: HttpRequest, id: str):
""" Renders a modal view for adding new payments """ Renders a modal view for adding new payments
Args: Args:
request (HttpRequest): The incoming request request (HttpRequest): The incoming request
intervention_id (str): The intervention's id for which a new payment shall be added id (str): The intervention's id for which a new payment shall be added
Returns: Returns:
""" """
intervention = get_object_or_404(Intervention, id=intervention_id) intervention = get_object_or_404(Intervention, id=id)
form = NewPaymentForm(request.POST or None, instance=intervention, request=request) form = NewPaymentForm(request.POST or None, instance=intervention, request=request)
return form.process_request( return form.process_request(
request, request,
msg_success=PAYMENT_ADDED, msg_success=PAYMENT_ADDED,
redirect_url=reverse("intervention:detail", args=(intervention_id,)) + "#related_data" redirect_url=reverse("intervention:detail", args=(id,)) + "#related_data"
) )
@login_required @login_required
@default_group_required @default_group_required
def payment_remove_view(request: HttpRequest, id: str): @shared_access_required(Intervention, "id")
def payment_remove_view(request: HttpRequest, id: str, payment_id: str):
""" Renders a modal view for removing payments """ Renders a modal view for removing payments
Args: Args:
request (HttpRequest): The incoming request request (HttpRequest): The incoming request
id (str): The payment's id id (str): The intervention's id
payment_id (str): The payment's id
Returns: Returns:
""" """
payment = get_object_or_404(Payment, id=id) intervention = get_object_or_404(Intervention, id=id)
form = RemoveModalForm(request.POST or None, instance=payment, request=request) payment = get_object_or_404(Payment, id=payment_id)
form = RemovePaymentModalForm(request.POST or None, instance=intervention, payment=payment, request=request)
return form.process_request( return form.process_request(
request=request, request=request,
msg_success=PAYMENT_REMOVED, msg_success=PAYMENT_REMOVED,
redirect_url=reverse("intervention:detail", args=(payment.intervention_id,)) + "#related_data" redirect_url=reverse("intervention:detail", args=(payment.intervention_id,)) + "#related_data"
) )
@login_required
@default_group_required
@shared_access_required(Intervention, "id")
def payment_edit_view(request: HttpRequest, id: str, payment_id: str):
""" Renders a modal view for editing payments
Args:
request (HttpRequest): The incoming request
id (str): The intervention's id
payment_id (str): The payment's id
Returns:
"""
intervention = get_object_or_404(Intervention, id=id)
payment = get_object_or_404(Payment, id=payment_id)
form = EditPaymentModalForm(request.POST or None, instance=intervention, payment=payment, request=request)
return form.process_request(
request=request,
msg_success=PAYMENT_EDITED,
redirect_url=reverse("intervention:detail", args=(payment.intervention_id,)) + "#related_data"
)

View File

@ -15,7 +15,7 @@ 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, EmaDocument from ema.models import Ema, EmaDocument
from intervention.models import Responsibility from intervention.models import Responsibility
from konova.forms import SimpleGeomForm, NewDocumentForm from konova.forms import SimpleGeomForm, NewDocumentModalForm
from user.models import UserActionLogEntry from user.models import UserActionLogEntry
@ -150,5 +150,5 @@ class EditEmaForm(NewEmaForm):
return self.instance return self.instance
class NewEmaDocumentForm(NewDocumentForm): class NewEmaDocumentModalForm(NewDocumentModalForm):
document_model = EmaDocument document_model = EmaDocument

View File

@ -51,28 +51,6 @@ class Ema(AbstractCompensation, ShareableObjectMixin, RecordableObjectMixin):
self.identifier = new_id self.identifier = new_id
super().save(*args, **kwargs) super().save(*args, **kwargs)
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) -> EmaQualityChecker: def quality_check(self) -> EmaQualityChecker:
""" Quality check """ Quality check

View File

@ -6,6 +6,7 @@ Created on: 19.08.21
""" """
from django.http import HttpRequest from django.http import HttpRequest
from django.template.loader import render_to_string
from django.utils.html import format_html from django.utils.html import format_html
from django.utils.timezone import localtime from django.utils.timezone import localtime
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -34,6 +35,11 @@ class EmaTable(BaseTable, TableRenderMixin):
orderable=True, orderable=True,
accessor="title", accessor="title",
) )
d = tables.Column(
verbose_name=_("Parcel gmrkng"),
orderable=True,
accessor="geometry",
)
r = tables.Column( r = tables.Column(
verbose_name=_("Recorded"), verbose_name=_("Recorded"),
orderable=True, orderable=True,
@ -87,6 +93,29 @@ class EmaTable(BaseTable, TableRenderMixin):
) )
return format_html(html) return format_html(html)
def render_d(self, value, record: Ema):
""" Renders the parcel district column for a ema
Args:
value (str): The geometry
record (Ema): The ema record
Returns:
"""
parcels = value.get_underlying_parcels().values_list(
"gmrkng",
flat=True
).distinct()
html = render_to_string(
"table/gmrkng_col.html",
{
"entries": parcels
}
)
return html
def render_r(self, value, record: Ema): def render_r(self, value, record: Ema):
""" Renders the registered column for a EMA """ Renders the registered column for a EMA

View File

@ -33,7 +33,7 @@
<th scope="col"> <th scope="col">
{% trans 'Comment' %} {% trans 'Comment' %}
</th> </th>
<th scope="col"> <th class="w-10" scope="col">
<span class="float-right"> <span class="float-right">
{% trans 'Action' %} {% trans 'Action' %}
</span> </span>
@ -58,9 +58,12 @@
{{ action.comment }} {{ action.comment }}
</div> </div>
</td> </td>
<td class=""> <td class="align-middle float-right">
{% if is_default_member and has_access %} {% if is_default_member and has_access %}
<button data-form-url="{% url 'ema:action-remove' obj.id action.id %}" class="btn btn-default btn-modal float-right" title="{% trans 'Remove action' %}"> <button data-form-url="{% url 'ema:action-edit' obj.id action.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit action' %}">
{% fa5_icon 'edit' %}
</button>
<button data-form-url="{% url 'ema:action-remove' obj.id action.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove action' %}">
{% fa5_icon 'trash' %} {% fa5_icon 'trash' %}
</button> </button>
{% endif %} {% endif %}

View File

@ -33,7 +33,7 @@
<th scope="col"> <th scope="col">
{% trans 'Comment' %} {% trans 'Comment' %}
</th> </th>
<th scope="col"> <th class="w-10" scope="col">
<span class="float-right"> <span class="float-right">
{% trans 'Action' %} {% trans 'Action' %}
</span> </span>
@ -52,9 +52,12 @@
{{ deadline.comment }} {{ deadline.comment }}
</div> </div>
</td> </td>
<td class="align-middle"> <td class="align-middle float-right">
{% if is_default_member and has_access %} {% if is_default_member and has_access %}
<button data-form-url="{% url 'ema:deadline-remove' obj.id deadline.id %}" class="btn btn-default btn-modal float-right" title="{% trans 'Remove deadline' %}"> <button data-form-url="{% url 'ema:deadline-edit' obj.id deadline.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit deadline' %}">
{% fa5_icon 'edit' %}
</button>
<button data-form-url="{% url 'ema:deadline-remove' obj.id deadline.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove deadline' %}">
{% fa5_icon 'trash' %} {% fa5_icon 'trash' %}
</button> </button>
{% endif %} {% endif %}

View File

@ -28,9 +28,12 @@
{% trans 'Title' %} {% trans 'Title' %}
</th> </th>
<th scope="col"> <th scope="col">
{% trans 'Comment' %} {% trans 'Created on' %}
</th> </th>
<th scope="col"> <th scope="col">
{% trans 'Comment' %}
</th>
<th class="w-10" scope="col">
<span class="float-right"> <span class="float-right">
{% trans 'Action' %} {% trans 'Action' %}
</span> </span>
@ -41,18 +44,24 @@
{% for doc in obj.documents.all %} {% for doc in obj.documents.all %}
<tr> <tr>
<td class="align-middle"> <td class="align-middle">
<a href="{% url 'ema:get-doc' doc.id %}"> <a href="{% url 'ema:get-doc' obj.id doc.id %}">
{{ doc.title }} {{ doc.title }}
</a> </a>
</td> </td>
<td class="align-middle">
{{ doc.date_of_creation }}
</td>
<td class="align-middle"> <td class="align-middle">
<div class="scroll-150"> <div class="scroll-150">
{{ doc.comment }} {{ doc.comment }}
</div> </div>
</td> </td>
<td class="align-middle"> <td class="align-middle float-right">
{% if is_default_member and has_access %} {% if is_default_member and has_access %}
<button data-form-url="{% url 'ema:remove-doc' doc.id %}" class="btn btn-default btn-modal float-right" title="{% trans 'Remove document' %}"> <button data-form-url="{% url 'ema:edit-doc' obj.id doc.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit document' %}">
{% fa5_icon 'edit' %}
</button>
<button data-form-url="{% url 'ema:remove-doc' obj.id doc.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove document' %}">
{% fa5_icon 'trash' %} {% fa5_icon 'trash' %}
</button> </button>
{% endif %} {% endif %}

View File

@ -35,7 +35,7 @@
<th scope="col"> <th scope="col">
{% trans 'Surface' %} {% trans 'Surface' %}
</th> </th>
<th scope="col"> <th class="w-10" scope="col">
<span class="float-right"> <span class="float-right">
{% trans 'Action' %} {% trans 'Action' %}
</span> </span>
@ -55,9 +55,12 @@
{% endif %} {% endif %}
</td> </td>
<td>{{ state.surface|floatformat:2 }} m²</td> <td>{{ state.surface|floatformat:2 }} m²</td>
<td> <td class="align-middle float-right">
{% if is_default_member and has_access %} {% if is_default_member and has_access %}
<button data-form-url="{% url 'ema:state-remove' obj.id state.id %}" class="btn btn-default btn-modal float-right" title="{% trans 'Remove state' %}"> <button data-form-url="{% url 'ema:state-edit' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit state' %}">
{% fa5_icon 'edit' %}
</button>
<button data-form-url="{% url 'ema:state-remove' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove state' %}">
{% fa5_icon 'trash' %} {% fa5_icon 'trash' %}
</button> </button>
{% endif %} {% endif %}

View File

@ -35,7 +35,7 @@
<th scope="col"> <th scope="col">
{% trans 'Surface' %} {% trans 'Surface' %}
</th> </th>
<th scope="col"> <th class="w-10" scope="col">
<span class="float-right"> <span class="float-right">
{% trans 'Action' %} {% trans 'Action' %}
</span> </span>
@ -55,9 +55,12 @@
{% endif %} {% endif %}
</td> </td>
<td>{{ state.surface|floatformat:2 }} m²</td> <td>{{ state.surface|floatformat:2 }} m²</td>
<td> <td class="align-middle float-right">
{% if is_default_member and has_access %} {% if is_default_member and has_access %}
<button data-form-url="{% url 'ema:state-remove' obj.id state.id %}" class="btn btn-default btn-modal float-right" title="{% trans 'Remove state' %}"> <button data-form-url="{% url 'ema:state-edit' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit state' %}">
{% fa5_icon 'edit' %}
</button>
<button data-form-url="{% url 'ema:state-remove' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove state' %}">
{% fa5_icon 'trash' %} {% fa5_icon 'trash' %}
</button> </button>
{% endif %} {% endif %}

View File

@ -5,14 +5,15 @@ Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 26.10.21 Created on: 26.10.21
""" """
from django.db.models import Q from django.db.models import Q
from django.urls import reverse from django.urls import reverse
from django.test.client import Client from django.test.client import Client
from compensation.tests.test_views import CompensationViewTestCase from compensation.tests.compensation.test_views import CompensationViewTestCase
from ema.models import Ema from ema.models import Ema
from intervention.models import Responsibility from intervention.models import Responsibility
from konova.models import Geometry from konova.models import Geometry, Deadline, DeadlineType
from konova.settings import DEFAULT_GROUP, ETS_GROUP from konova.settings import DEFAULT_GROUP, ETS_GROUP
from user.models import UserActionLogEntry from user.models import UserActionLogEntry
@ -30,42 +31,57 @@ class EmaViewTestCase(CompensationViewTestCase):
def setUpTestData(cls) -> None: def setUpTestData(cls) -> None:
super().setUpTestData() super().setUpTestData()
def setUp(self) -> None:
super().setUp()
# Create dummy data and related objects, like states or actions # Create dummy data and related objects, like states or actions
cls.create_dummy_data() self.create_dummy_data()
state = cls.create_dummy_states() state = self.create_dummy_states()
action = cls.create_dummy_action() action = self.create_dummy_action()
cls.ema.before_states.set([state]) self.ema.before_states.set([state])
cls.ema.after_states.set([state]) self.ema.after_states.set([state])
cls.ema.actions.set([action]) self.ema.actions.set([action])
# Prepare urls # Prepare urls
cls.index_url = reverse("ema:index", args=()) self.index_url = reverse("ema:index", args=())
cls.new_url = reverse("ema:new", args=()) self.new_url = reverse("ema:new", args=())
cls.new_id_url = reverse("ema:new-id", args=()) self.new_id_url = reverse("ema:new-id", args=())
cls.detail_url = reverse("ema:detail", args=(cls.ema.id,)) self.detail_url = reverse("ema:detail", args=(self.ema.id,))
cls.log_url = reverse("ema:log", args=(cls.ema.id,)) self.log_url = reverse("ema:log", args=(self.ema.id,))
cls.edit_url = reverse("ema:edit", args=(cls.ema.id,)) self.edit_url = reverse("ema:edit", args=(self.ema.id,))
cls.remove_url = reverse("ema:remove", args=(cls.ema.id,)) self.remove_url = reverse("ema:remove", args=(self.ema.id,))
cls.share_url = reverse("ema:share", args=(cls.ema.id, cls.ema.access_token,)) self.share_url = reverse("ema:share", args=(self.ema.id, self.ema.access_token,))
cls.share_create_url = reverse("ema:share-create", args=(cls.ema.id,)) self.share_create_url = reverse("ema:share-create", args=(self.ema.id,))
cls.record_url = reverse("ema:record", args=(cls.ema.id,)) self.record_url = reverse("ema:record", args=(self.ema.id,))
cls.report_url = reverse("ema:report", args=(cls.ema.id,)) self.report_url = reverse("ema:report", args=(self.ema.id,))
cls.new_doc_url = reverse("ema:new-doc", args=(cls.ema.id,)) self.new_doc_url = reverse("ema:new-doc", args=(self.ema.id,))
cls.state_new_url = reverse("ema:new-state", args=(cls.ema.id,))
cls.action_new_url = reverse("ema:new-action", args=(cls.ema.id,))
cls.deadline_new_url = reverse("ema:new-deadline", args=(cls.ema.id,))
cls.state_remove_url = reverse("ema:state-remove", args=(cls.ema.id, state.id,))
cls.action_remove_url = reverse("ema:action-remove", args=(cls.ema.id, action.id,))
@classmethod self.state_new_url = reverse("ema:new-state", args=(self.ema.id,))
def create_dummy_data(cls): self.state_edit_url = reverse("ema:state-edit", args=(self.ema.id, state.id))
self.state_remove_url = reverse("ema:state-remove", args=(self.ema.id, state.id,))
self.action_new_url = reverse("ema:new-action", args=(self.ema.id,))
self.action_edit_url = reverse("ema:action-edit", args=(self.ema.id, action.id))
self.action_remove_url = reverse("ema:action-remove", args=(self.ema.id, action.id,))
self.deadline = Deadline.objects.get_or_create(
type=DeadlineType.FINISHED,
date="2020-01-01",
comment="TESTCOMMENT",
)[0]
self.ema.deadlines.add(self.deadline)
self.deadline_new_url = reverse("ema:new-deadline", args=(self.ema.id,))
self.deadline_edit_url = reverse("ema:deadline-edit", args=(self.ema.id, self.deadline.id))
self.deadline_remove_url = reverse("ema:deadline-remove", args=(self.ema.id, self.deadline.id))
def create_dummy_data(self):
# Create dummy data # Create dummy data
# Create log entry # Create log entry
action = UserActionLogEntry.get_created_action(cls.superuser) action = UserActionLogEntry.get_created_action(self.superuser)
# Create responsible data object # Create responsible data object
responsibility_data = Responsibility.objects.create() responsibility_data = Responsibility.objects.create()
geometry = Geometry.objects.create() geometry = Geometry.objects.create()
cls.ema = Ema.objects.create( self.ema = Ema.objects.create(
identifier="TEST", identifier="TEST",
title="Test_title", title="Test_title",
created=action, created=action,
@ -106,10 +122,14 @@ class EmaViewTestCase(CompensationViewTestCase):
self.new_id_url, self.new_id_url,
self.edit_url, self.edit_url,
self.state_new_url, self.state_new_url,
self.action_new_url,
self.deadline_new_url,
self.state_remove_url, self.state_remove_url,
self.state_edit_url,
self.deadline_new_url,
self.deadline_edit_url,
self.deadline_remove_url,
self.action_edit_url,
self.action_remove_url, self.action_remove_url,
self.action_new_url,
self.new_doc_url, self.new_doc_url,
self.log_url, self.log_url,
self.remove_url, self.remove_url,
@ -152,10 +172,14 @@ class EmaViewTestCase(CompensationViewTestCase):
self.new_id_url, self.new_id_url,
self.edit_url, self.edit_url,
self.state_new_url, self.state_new_url,
self.action_new_url, self.state_edit_url,
self.deadline_new_url,
self.state_remove_url, self.state_remove_url,
self.action_new_url,
self.action_edit_url,
self.action_remove_url, self.action_remove_url,
self.deadline_new_url,
self.deadline_edit_url,
self.deadline_remove_url,
self.new_doc_url, self.new_doc_url,
self.log_url, self.log_url,
self.remove_url, self.remove_url,
@ -189,9 +213,13 @@ class EmaViewTestCase(CompensationViewTestCase):
self.new_id_url, self.new_id_url,
self.edit_url, self.edit_url,
self.state_new_url, self.state_new_url,
self.action_new_url, self.state_edit_url,
self.deadline_new_url,
self.state_remove_url, self.state_remove_url,
self.deadline_new_url,
self.deadline_edit_url,
self.deadline_remove_url,
self.action_new_url,
self.action_edit_url,
self.action_remove_url, self.action_remove_url,
self.new_doc_url, self.new_doc_url,
self.log_url, self.log_url,
@ -227,9 +255,13 @@ class EmaViewTestCase(CompensationViewTestCase):
fail_urls = [ fail_urls = [
self.edit_url, self.edit_url,
self.state_new_url, self.state_new_url,
self.action_new_url, self.state_edit_url,
self.deadline_new_url,
self.state_remove_url, self.state_remove_url,
self.deadline_new_url,
self.deadline_edit_url,
self.deadline_remove_url,
self.action_new_url,
self.action_edit_url,
self.action_remove_url, self.action_remove_url,
self.new_doc_url, self.new_doc_url,
self.log_url, self.log_url,

165
ema/tests/test_workflow.py Normal file
View File

@ -0,0 +1,165 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 08.02.22
"""
import datetime
from django.contrib.gis.geos import MultiPolygon
from django.core.exceptions import ObjectDoesNotExist
from django.urls import reverse
from ema.models import Ema
from konova.settings import ETS_GROUP
from konova.tests.test_views import BaseWorkflowTestCase
from user.models import UserAction
class EmaWorkflowTestCase(BaseWorkflowTestCase):
ema = None
@classmethod
def setUpTestData(cls) -> None:
super().setUpTestData()
def setUp(self) -> None:
super().setUp()
# Create a fresh dummy (non-valid) compensation before each test
self.ema = self.create_dummy_ema()
def test_new(self):
""" Test the creation of an Ema
Returns:
"""
self.superuser.groups.add(self.groups.get(name=ETS_GROUP))
# Prepare url and form data to be posted
new_url = reverse("ema:new")
test_id = self.create_dummy_string()
test_title = self.create_dummy_string()
test_geom = self.create_dummy_geometry()
test_conservation_office = self.get_conservation_office_code()
post_data = {
"identifier": test_id,
"title": test_title,
"geom": test_geom.geojson,
"conservation_office": test_conservation_office.id
}
self.client_user.post(new_url, post_data)
try:
ema = Ema.objects.get(
identifier=test_id
)
except ObjectDoesNotExist:
self.fail(msg="Ema not created")
self.assertEqual(ema.identifier, test_id)
self.assertEqual(ema.title, test_title)
self.assert_equal_geometries(ema.geometry.geom, test_geom)
self.assertEqual(ema.log.count(), 1)
# Expect logs to be set
self.assertEqual(ema.log.count(), 1)
self.assertEqual(ema.log.first().action, UserAction.CREATED)
def test_edit(self):
""" Checks that the editing of an Ema works
Returns:
"""
self.superuser.groups.add(self.groups.get(name=ETS_GROUP))
self.ema.users.add(self.superuser)
url = reverse("ema:edit", args=(self.ema.id,))
self.ema = self.fill_out_ema(self.ema)
pre_edit_log_count = self.ema.log.count()
new_title = self.create_dummy_string()
new_identifier = self.create_dummy_string()
new_comment = self.create_dummy_string()
new_geometry = MultiPolygon(srid=4326) # Create an empty geometry
test_conservation_office = self.get_conservation_office_code()
check_on_elements = {
self.ema.title: new_title,
self.ema.identifier: new_identifier,
self.ema.comment: new_comment,
}
for k, v in check_on_elements.items():
self.assertNotEqual(k, v)
post_data = {
"identifier": new_identifier,
"title": new_title,
"comment": new_comment,
"geom": new_geometry.geojson,
"conservation_office": test_conservation_office.id
}
self.client_user.post(url, post_data)
self.ema.refresh_from_db()
check_on_elements = {
self.ema.title: new_title,
self.ema.identifier: new_identifier,
self.ema.comment: new_comment,
}
for k, v in check_on_elements.items():
self.assertEqual(k, v)
self.assert_equal_geometries(self.ema.geometry.geom, new_geometry)
# Expect logs to be set
self.assertEqual(pre_edit_log_count + 1, self.ema.log.count())
self.assertEqual(self.ema.log.first().action, UserAction.EDITED)
def test_recordability(self):
"""
This tests if the recordability of the Ema is triggered by the quality of it's data (e.g. not all fields filled)
Returns:
"""
# Add proper privilege for the user
self.superuser.groups.add(self.groups.get(name=ETS_GROUP))
self.ema.users.add(self.superuser)
pre_record_log_count = self.ema.log.count()
# Prepare url and form data
record_url = reverse("ema:record", args=(self.ema.id,))
post_data = {
"confirm": True,
}
# Make sure the ema is not recorded
self.assertIsNone(self.ema.recorded)
# Run the request --> expect fail, since the Ema is not valid, yet
self.client_user.post(record_url, post_data)
# Check that the Ema is still not recorded
self.assertIsNone(self.ema.recorded)
# Now fill out the data for a compensation
self.ema = self.fill_out_ema(self.ema)
# Rerun the request
self.client_user.post(record_url, post_data)
# Expect the Ema now to be recorded
# Attention: We can only test the date part of the timestamp,
# since the delay in microseconds would lead to fail
self.ema.refresh_from_db()
recorded = self.ema.recorded
self.assertIsNotNone(recorded)
self.assertEqual(self.superuser, recorded.user)
self.assertEqual(UserAction.RECORDED, recorded.action)
self.assertEqual(datetime.date.today(), recorded.timestamp.date())
# Expect the user action to be in the log
self.assertIn(recorded, self.ema.log.all())
self.assertEqual(pre_record_log_count + 1, self.ema.log.count())

View File

@ -19,18 +19,26 @@ urlpatterns = [
path('<id>/remove', remove_view, name='remove'), path('<id>/remove', remove_view, name='remove'),
path('<id>/record', record_view, name='record'), path('<id>/record', record_view, name='record'),
path('<id>/report', report_view, name='report'), path('<id>/report', report_view, name='report'),
path('<id>/state/new', state_new_view, name='new-state'), path('<id>/state/new', state_new_view, name='new-state'),
path('<id>/action/new', action_new_view, name='new-action'),
path('<id>/state/<state_id>/remove', state_remove_view, name='state-remove'), path('<id>/state/<state_id>/remove', state_remove_view, name='state-remove'),
path('<id>/state/<state_id>/edit', state_edit_view, name='state-edit'),
path('<id>/action/new', action_new_view, name='new-action'),
path('<id>/action/<action_id>/edit', action_edit_view, name='action-edit'),
path('<id>/action/<action_id>/remove', action_remove_view, name='action-remove'), path('<id>/action/<action_id>/remove', action_remove_view, name='action-remove'),
path('<id>/deadline/<deadline_id>/remove', deadline_remove_view, name='deadline-remove'),
path('<id>/deadline/new', deadline_new_view, name="new-deadline"), path('<id>/deadline/new', deadline_new_view, name="new-deadline"),
path('<id>/deadline/<deadline_id>/edit', deadline_edit_view, name='deadline-edit'),
path('<id>/deadline/<deadline_id>/remove', deadline_remove_view, name='deadline-remove'),
path('<id>/share/<token>', share_view, name='share'), path('<id>/share/<token>', share_view, name='share'),
path('<id>/share', create_share_view, name='share-create'), path('<id>/share', create_share_view, name='share-create'),
# Documents # Documents
path('<id>/document/new/', document_new_view, name='new-doc'), path('<id>/document/new/', document_new_view, name='new-doc'),
path('document/<doc_id>', get_document_view, name='get-doc'), path('<id>/document/<doc_id>', get_document_view, name='get-doc'),
path('document/<doc_id>/remove/', remove_document_view, name='remove-doc'), path('<id>/document/<doc_id>/edit/', edit_document_view, name='edit-doc'),
path('<id>/document/<doc_id>/remove/', remove_document_view, name='remove-doc'),
] ]

View File

@ -6,22 +6,27 @@ from django.shortcuts import render, get_object_or_404, redirect
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from compensation.forms.modalForms import NewStateModalForm, NewActionModalForm, NewDeadlineModalForm from compensation.forms.modalForms import NewStateModalForm, NewActionModalForm, NewDeadlineModalForm, \
RemoveCompensationActionModalForm, RemoveCompensationStateModalForm, EditCompensationStateModalForm, \
EditCompensationActionModalForm, EditDeadlineModalForm
from compensation.models import CompensationAction, CompensationState from compensation.models import CompensationAction, CompensationState
from ema.forms import NewEmaForm, EditEmaForm, NewEmaDocumentForm from ema.forms import NewEmaForm, EditEmaForm, NewEmaDocumentModalForm
from ema.tables import EmaTable from ema.tables import EmaTable
from intervention.forms.modalForms import ShareModalForm 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, SimpleGeomForm, RecordModalForm from konova.forms import RemoveModalForm, SimpleGeomForm, RecordModalForm, RemoveDeadlineModalForm, \
EditDocumentModalForm
from konova.models import Deadline from konova.models import Deadline
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
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 IDENTIFIER_REPLACED, FORM_INVALID, DATA_UNSHARED, DATA_UNSHARED_EXPLANATION, \ from konova.utils.message_templates import IDENTIFIER_REPLACED, FORM_INVALID, DATA_UNSHARED, DATA_UNSHARED_EXPLANATION, \
DOCUMENT_ADDED DOCUMENT_ADDED, COMPENSATION_STATE_REMOVED, COMPENSATION_STATE_ADDED, COMPENSATION_ACTION_REMOVED, \
COMPENSATION_ACTION_ADDED, DEADLINE_ADDED, DEADLINE_REMOVED, DOCUMENT_EDITED, COMPENSATION_STATE_EDITED, \
COMPENSATION_ACTION_EDITED, DEADLINE_EDITED
from konova.utils.user_checks import in_group from konova.utils.user_checks import in_group
@ -292,7 +297,7 @@ def state_new_view(request: HttpRequest, id: str):
form = NewStateModalForm(request.POST or None, instance=ema, request=request) form = NewStateModalForm(request.POST or None, instance=ema, request=request)
return form.process_request( return form.process_request(
request, request,
msg_success=_("State added"), msg_success=COMPENSATION_STATE_ADDED,
redirect_url=reverse("ema:detail", args=(id,)) + "#related_data" redirect_url=reverse("ema:detail", args=(id,)) + "#related_data"
) )
@ -314,7 +319,31 @@ def action_new_view(request: HttpRequest, id: str):
form = NewActionModalForm(request.POST or None, instance=ema, request=request) form = NewActionModalForm(request.POST or None, instance=ema, request=request)
return form.process_request( return form.process_request(
request, request,
msg_success=_("Action added"), msg_success=COMPENSATION_ACTION_ADDED,
redirect_url=reverse("ema:detail", args=(id,)) + "#related_data"
)
@login_required
@conservation_office_group_required
@shared_access_required(Ema, "id")
def action_edit_view(request: HttpRequest, id: str, action_id: str):
""" Renders a form for editing an actions for an EMA
Args:
request (HttpRequest): The incoming request
id (str): The EMA's id
action_id (str): The action id
Returns:
"""
ema = get_object_or_404(Ema, id=id)
action = get_object_or_404(CompensationAction, id=action_id)
form = EditCompensationActionModalForm(request.POST or None, instance=ema, action=action, request=request)
return form.process_request(
request,
msg_success=COMPENSATION_ACTION_EDITED,
redirect_url=reverse("ema:detail", args=(id,)) + "#related_data" redirect_url=reverse("ema:detail", args=(id,)) + "#related_data"
) )
@ -336,7 +365,7 @@ def deadline_new_view(request: HttpRequest, id: str):
form = NewDeadlineModalForm(request.POST or None, instance=ema, request=request) form = NewDeadlineModalForm(request.POST or None, instance=ema, request=request)
return form.process_request( return form.process_request(
request, request,
msg_success=_("Deadline added"), msg_success=DEADLINE_ADDED,
redirect_url=reverse("ema:detail", args=(id,)) + "#related_data" redirect_url=reverse("ema:detail", args=(id,)) + "#related_data"
) )
@ -354,7 +383,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 = NewEmaDocumentForm(request.POST or None, request.FILES or None, instance=ema, request=request) form = NewEmaDocumentModalForm(request.POST or None, request.FILES or None, instance=ema, request=request)
return form.process_request( return form.process_request(
request, request,
msg_success=DOCUMENT_ADDED, msg_success=DOCUMENT_ADDED,
@ -364,45 +393,68 @@ def document_new_view(request: HttpRequest, id: str):
@login_required @login_required
@conservation_office_group_required @conservation_office_group_required
def get_document_view(request: HttpRequest, doc_id: str): @shared_access_required(Ema, "id")
def get_document_view(request: HttpRequest, id: str, doc_id: str):
""" Returns the document as downloadable file """ Returns the document as downloadable file
Wraps the generic document fetcher function from konova.utils. Wraps the generic document fetcher function from konova.utils.
Args: Args:
request (HttpRequest): The incoming request request (HttpRequest): The incoming request
id (str): The EMA id
doc_id (str): The document id doc_id (str): The document id
Returns: Returns:
""" """
ema = get_object_or_404(Ema, id=id)
doc = get_object_or_404(EmaDocument, id=doc_id) doc = get_object_or_404(EmaDocument, id=doc_id)
user = request.user
instance = doc.instance
# File download only possible if related instance is shared with user
if not instance.users.filter(id=user.id):
messages.info(
request,
DATA_UNSHARED
)
return redirect("ema:detail", id=instance.id)
return get_document(doc) return get_document(doc)
@login_required @login_required
@conservation_office_group_required @conservation_office_group_required
def remove_document_view(request: HttpRequest, doc_id: str): @shared_access_required(Ema, "id")
def edit_document_view(request: HttpRequest, id: str, doc_id: str):
""" Removes the document from the database and file system """ Removes the document from the database and file system
Wraps the generic functionality from konova.utils. Wraps the generic functionality from konova.utils.
Args: Args:
request (HttpRequest): The incoming request request (HttpRequest): The incoming request
id (str): The EMA id
doc_id (str): The document id doc_id (str): The document id
Returns: Returns:
""" """
ema = get_object_or_404(Ema, id=id)
doc = get_object_or_404(EmaDocument, id=doc_id)
form = EditDocumentModalForm(request.POST or None, request.FILES or None, instance=ema, document=doc, request=request)
return form.process_request(
request,
DOCUMENT_EDITED,
reverse("ema:detail", args=(id,)) + "#related_data"
)
@login_required
@conservation_office_group_required
@shared_access_required(Ema, "id")
def remove_document_view(request: HttpRequest, id:str, doc_id: str):
""" Removes the document from the database and file system
Wraps the generic functionality from konova.utils.
Args:
request (HttpRequest): The incoming request
id (str): The EMA id
doc_id (str): The document id
Returns:
"""
ema = get_object_or_404(Ema, id=id)
doc = get_object_or_404(EmaDocument, id=doc_id) doc = get_object_or_404(EmaDocument, id=doc_id)
return remove_document( return remove_document(
request, request,
@ -424,11 +476,36 @@ def state_remove_view(request: HttpRequest, id: str, state_id: str):
Returns: Returns:
""" """
ema = get_object_or_404(Ema, id=id)
state = get_object_or_404(CompensationState, id=state_id) state = get_object_or_404(CompensationState, id=state_id)
form = RemoveModalForm(request.POST or None, instance=state, request=request) form = RemoveCompensationStateModalForm(request.POST or None, instance=ema, state=state, request=request)
return form.process_request( return form.process_request(
request, request,
msg_success=_("State removed"), msg_success=COMPENSATION_STATE_REMOVED,
redirect_url=reverse("ema:detail", args=(id,)) + "#related_data"
)
@login_required
@conservation_office_group_required
@shared_access_required(Ema, "id")
def state_edit_view(request: HttpRequest, id: str, state_id: str):
""" Renders a form for editing an EMA state
Args:
request (HttpRequest): The incoming request
id (str): The ema id
state_id (str): The state's id
Returns:
"""
ema = get_object_or_404(Ema, id=id)
state = get_object_or_404(CompensationState, id=state_id)
form = EditCompensationStateModalForm(request.POST or None, instance=ema, state=state, request=request)
return form.process_request(
request,
msg_success=COMPENSATION_STATE_EDITED,
redirect_url=reverse("ema:detail", args=(id,)) + "#related_data" redirect_url=reverse("ema:detail", args=(id,)) + "#related_data"
) )
@ -447,11 +524,12 @@ def action_remove_view(request: HttpRequest, id: str, action_id: str):
Returns: Returns:
""" """
ema = get_object_or_404(Ema, id=id)
action = get_object_or_404(CompensationAction, id=action_id) action = get_object_or_404(CompensationAction, id=action_id)
form = RemoveModalForm(request.POST or None, instance=action, request=request) form = RemoveCompensationActionModalForm(request.POST or None, instance=ema, action=action, request=request)
return form.process_request( return form.process_request(
request, request,
msg_success=_("Action removed"), msg_success=COMPENSATION_ACTION_REMOVED,
redirect_url=reverse("ema:detail", args=(id,)) + "#related_data" redirect_url=reverse("ema:detail", args=(id,)) + "#related_data"
) )
@ -575,6 +653,30 @@ def create_share_view(request: HttpRequest, id: str):
) )
@login_required
@conservation_office_group_required
@shared_access_required(Ema, "id")
def deadline_edit_view(request: HttpRequest, id: str, deadline_id: str):
""" Renders a form for editing deadlines from a compensation
Args:
request (HttpRequest): The incoming request
id (str): The compensation's id
deadline_id (str): The deadline's id
Returns:
"""
ema = get_object_or_404(Ema, id=id)
deadline = get_object_or_404(Deadline, id=deadline_id)
form = EditDeadlineModalForm(request.POST or None, instance=ema, deadline=deadline, request=request)
return form.process_request(
request,
msg_success=DEADLINE_EDITED,
redirect_url=reverse("ema:detail", args=(id,)) + "#related_data"
)
@login_required @login_required
@conservation_office_group_required @conservation_office_group_required
@shared_access_required(Ema, "id") @shared_access_required(Ema, "id")
@ -589,10 +691,11 @@ def deadline_remove_view(request: HttpRequest, id: str, deadline_id: str):
Returns: Returns:
""" """
ema = get_object_or_404(Ema, id=id)
deadline = get_object_or_404(Deadline, id=deadline_id) deadline = get_object_or_404(Deadline, id=deadline_id)
form = RemoveModalForm(request.POST or None, instance=deadline, request=request) form = RemoveDeadlineModalForm(request.POST or None, instance=ema, deadline=deadline, request=request)
return form.process_request( return form.process_request(
request, request,
msg_success=_("Deadline removed"), msg_success=DEADLINE_REMOVED,
redirect_url=reverse("ema:detail", args=(id,)) + "#related_data" redirect_url=reverse("ema:detail", args=(id,)) + "#related_data"
) )

View File

@ -6,8 +6,11 @@ Created on: 27.09.21
""" """
from dal import autocomplete from dal import autocomplete
from django.core.exceptions import ObjectDoesNotExist
from django.db.models.fields.files import FieldFile
from konova.utils.message_templates import DEDUCTION_ADDED, REVOCATION_ADDED from konova.utils.message_templates import DEDUCTION_ADDED, REVOCATION_ADDED, DEDUCTION_REMOVED, DEDUCTION_EDITED, \
REVOCATION_EDITED, FILE_TYPE_UNSUPPORTED, FILE_SIZE_TOO_LARGE
from user.models import User, UserActionLogEntry from user.models import User, UserActionLogEntry
from django.db import transaction from django.db import transaction
from django import forms from django import forms
@ -15,8 +18,8 @@ from django.utils.translation import gettext_lazy as _
from compensation.models import EcoAccount, EcoAccountDeduction from compensation.models import EcoAccount, EcoAccountDeduction
from intervention.inputs import TextToClipboardInput from intervention.inputs import TextToClipboardInput
from intervention.models import Intervention, InterventionDocument from intervention.models import Intervention, InterventionDocument, RevocationDocument
from konova.forms import BaseModalForm, NewDocumentForm from konova.forms import BaseModalForm, NewDocumentModalForm, RemoveModalForm
from konova.utils.general import format_german_float from konova.utils.general import format_german_float
from konova.utils.user_checks import is_default_group_only from konova.utils.user_checks import is_default_group_only
@ -157,6 +160,7 @@ class NewRevocationModalForm(BaseModalForm):
} }
) )
) )
document_model = RevocationDocument
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@ -172,6 +176,46 @@ class NewRevocationModalForm(BaseModalForm):
return revocation 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): class CheckModalForm(BaseModalForm):
""" The modal form for running a check on interventions and their compensations """ The modal form for running a check on interventions and their compensations
@ -332,6 +376,21 @@ class NewDeductionModalForm(BaseModalForm):
else: else:
raise NotImplementedError 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): def is_valid(self):
""" Custom validity check """ Custom validity check
@ -350,10 +409,7 @@ class NewDeductionModalForm(BaseModalForm):
) )
return False return False
# Calculate valid surface rest_surface = self._get_available_surface(acc)
deductable_surface = acc.deductable_surface
sum_surface_deductions = acc.get_deductions_surface()
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:
@ -386,9 +442,80 @@ class NewDeductionModalForm(BaseModalForm):
def save(self): def save(self):
deduction = self.__create_deduction() deduction = self.__create_deduction()
self.cleaned_data["intervention"].mark_as_edited(self.user, edit_comment=DEDUCTION_ADDED) 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, reset_recorded=False) self.cleaned_data["account"].mark_as_edited(self.user, edit_comment=DEDUCTION_ADDED)
return deduction return deduction
class NewInterventionDocumentForm(NewDocumentForm): 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 document_model = InterventionDocument

View File

@ -8,6 +8,8 @@ Created on: 15.11.21
import shutil import shutil
from django.contrib import messages from django.contrib import messages
from django.core.exceptions import ObjectDoesNotExist
from django.db.models.fields.files import FieldFile
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
@ -16,7 +18,6 @@ from django.db import models, transaction
from django.db.models import QuerySet from django.db.models import QuerySet
from django.http import HttpRequest from django.http import HttpRequest
from compensation.models import EcoAccountDeduction
from intervention.managers import InterventionManager from intervention.managers import InterventionManager
from intervention.models.legal import Legal from intervention.models.legal import Legal
from intervention.models.responsibility import Responsibility from intervention.models.responsibility import Responsibility
@ -26,7 +27,8 @@ from konova.models import generate_document_file_upload_path, AbstractDocument,
ShareableObjectMixin, \ ShareableObjectMixin, \
RecordableObjectMixin, CheckableObjectMixin, GeoReferencedMixin RecordableObjectMixin, CheckableObjectMixin, GeoReferencedMixin
from konova.settings import LANIS_LINK_TEMPLATE, LANIS_ZOOM_LUT, DEFAULT_SRID_RLP from konova.settings import LANIS_LINK_TEMPLATE, LANIS_ZOOM_LUT, DEFAULT_SRID_RLP
from konova.utils.message_templates import DATA_UNSHARED_EXPLANATION, DEDUCTION_ADDED, DOCUMENT_REMOVED_TEMPLATE from konova.utils.message_templates import DATA_UNSHARED_EXPLANATION, DOCUMENT_REMOVED_TEMPLATE, \
PAYMENT_REMOVED, PAYMENT_ADDED, REVOCATION_REMOVED, INTERVENTION_HAS_REVOCATIONS_TEMPLATE
from user.models import UserActionLogEntry from user.models import UserActionLogEntry
@ -100,34 +102,6 @@ class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, Chec
checker.run_check() checker.run_check()
return checker return checker
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
area = int(geom.envelope.area)
z_l = 16
for k_area, v_zoom in LANIS_ZOOM_LUT.items():
if k_area < area:
z_l = v_zoom
break
zoom_lvl = z_l
except (AttributeError, IndexError) as e:
# 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, QuerySet): def get_documents(self) -> (QuerySet, QuerySet):
""" Getter for all documents of an intervention """ Getter for all documents of an intervention
@ -196,6 +170,7 @@ class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, Chec
comment=form_data.get("comment", None), comment=form_data.get("comment", None),
intervention=self, intervention=self,
) )
self.mark_as_edited(user, form.request, edit_comment=PAYMENT_ADDED)
return pay return pay
def add_revocation(self, form): def add_revocation(self, form):
@ -229,6 +204,56 @@ class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, Chec
) )
return revocation return revocation
def edit_revocation(self, form):
""" Updates a revocation of the intervention
Args:
form (EditRevocationModalForm): The form holding the data
Returns:
"""
form_data = form.cleaned_data
file = form_data.get("file", None)
revocation = form.revocation
revocation.date = form_data.get("date", None)
revocation.comment = form_data.get("comment", None)
with transaction.atomic():
try:
revocation.document.date_of_creation = revocation.date
revocation.document.comment = revocation.comment
if not isinstance(file, FieldFile):
revocation.document.replace_file(file)
revocation.document.save()
except ObjectDoesNotExist:
revocation.document = RevocationDocument.objects.create(
title="revocation_of_{}".format(self.identifier),
date_of_creation=revocation.date,
comment=revocation.comment,
file=file,
instance=revocation
)
revocation.save()
return revocation
def remove_revocation(self, form):
""" Removes a revocation from the intervention
Args:
form (RemoveRevocationModalForm): The form holding all relevant data
Returns:
"""
revocation = form.revocation
user = form.user
with transaction.atomic():
revocation.delete()
self.mark_as_edited(user, request=form.request, edit_comment=REVOCATION_REMOVED)
def mark_as_edited(self, performing_user: User, request: HttpRequest = None, edit_comment: str = None, reset_recorded: bool = True): def mark_as_edited(self, performing_user: User, request: HttpRequest = None, edit_comment: str = None, reset_recorded: bool = True):
""" In case the object or a related object changed, internal processes need to be started, such as """ In case the object or a related object changed, internal processes need to be started, such as
unrecord and uncheck unrecord and uncheck
@ -242,7 +267,9 @@ class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, Chec
Returns: Returns:
""" """
action = super().mark_as_edited(performing_user, request, edit_comment, reset_recorded) action = super().mark_as_edited(performing_user, edit_comment=edit_comment)
if reset_recorded:
self.unrecord(performing_user, request)
if self.checked: if self.checked:
self.set_unchecked() self.set_unchecked()
return action return action
@ -258,6 +285,13 @@ class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, Chec
Returns: Returns:
request (HttpRequest): The modified request request (HttpRequest): The modified request
""" """
# Inform user about revocation
if self.legal.revocations.exists():
messages.error(
request,
INTERVENTION_HAS_REVOCATIONS_TEMPLATE.format(self.legal.revocations.count()),
extra_tags="danger",
)
if not self.is_shared_with(request.user): if not self.is_shared_with(request.user):
messages.info(request, DATA_UNSHARED_EXPLANATION) messages.info(request, DATA_UNSHARED_EXPLANATION)
request = self.set_geometry_conflict_message(request) request = self.set_geometry_conflict_message(request)
@ -287,6 +321,21 @@ class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, Chec
""" """
return reverse("intervention:share", args=(self.id, self.access_token)) return reverse("intervention:share", args=(self.id, self.access_token))
def remove_payment(self, form):
""" Removes a Payment from the intervention
Args:
form (RemovePaymentModalForm): The form holding all relevant data
Returns:
"""
payment = form.payment
user = form.user
with transaction.atomic():
payment.delete()
self.mark_as_edited(user, request=form.request, edit_comment=PAYMENT_REMOVED)
class InterventionDocument(AbstractDocument): class InterventionDocument(AbstractDocument):
""" """

View File

@ -23,7 +23,7 @@ class Revocation(BaseResource):
legal = models.ForeignKey("Legal", null=False, blank=False, on_delete=models.CASCADE, help_text="Refers to 'Widerspruch am'", related_name="revocations") 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) comment = models.TextField(null=True, blank=True)
def delete(self, user=None, *args, **kwargs): def delete(self, *args, **kwargs):
# Make sure related objects are being removed as well # Make sure related objects are being removed as well
try: try:
self.document.delete(*args, **kwargs) self.document.delete(*args, **kwargs)
@ -31,9 +31,6 @@ class Revocation(BaseResource):
# No file to delete # No file to delete
pass pass
if user is not None:
self.legal.intervention.mark_as_edited(user, edit_comment=REVOCATION_REMOVED)
super().delete() super().delete()
@property @property

View File

@ -6,6 +6,7 @@ Created on: 01.12.20
""" """
from django.http import HttpRequest from django.http import HttpRequest
from django.template.loader import render_to_string
from django.urls import reverse from django.urls import reverse
from django.utils.html import format_html from django.utils.html import format_html
from django.utils.timezone import localtime from django.utils.timezone import localtime
@ -29,6 +30,11 @@ class InterventionTable(BaseTable, TableRenderMixin):
orderable=True, orderable=True,
accessor="title", accessor="title",
) )
d = tables.Column(
verbose_name=_("Parcel gmrkng"),
orderable=True,
accessor="geometry",
)
c = tables.Column( c = tables.Column(
verbose_name=_("Checked"), verbose_name=_("Checked"),
orderable=True, orderable=True,
@ -41,12 +47,6 @@ class InterventionTable(BaseTable, TableRenderMixin):
empty_values=[], empty_values=[],
accessor="recorded", accessor="recorded",
) )
rev = tables.Column(
verbose_name=_("Revocation"),
orderable=True,
empty_values=[],
accessor="legal__revocation",
)
e = tables.Column( e = tables.Column(
verbose_name=_("Editable"), verbose_name=_("Editable"),
orderable=True, orderable=True,
@ -84,14 +84,17 @@ class InterventionTable(BaseTable, TableRenderMixin):
Returns: Returns:
""" """
html = "" context = {
html += self.render_link( "tooltip": _("Open {}").format(_("Intervention")),
tooltip=_("Open {}").format(_("Intervention")), "content": value,
href=reverse("intervention:detail", args=(record.id,)), "url": reverse("intervention:detail", args=(record.id,)),
txt=value, "has_revocations": record.legal.revocations.exists()
new_tab=False, }
html = render_to_string(
"table/revocation_warning_col.html",
context
) )
return format_html(html) return html
def render_c(self, value, record: Intervention): def render_c(self, value, record: Intervention):
""" Renders the checked column for an intervention """ Renders the checked column for an intervention
@ -117,6 +120,28 @@ class InterventionTable(BaseTable, TableRenderMixin):
) )
return format_html(html) return format_html(html)
def render_d(self, value, record: Intervention):
""" Renders the parcel district column for an intervention
Args:
value (str): The intervention geometry
record (Intervention): The intervention record
Returns:
"""
parcels = value.get_underlying_parcels().values_list(
"gmrkng",
flat=True
).distinct()
html = render_to_string(
"table/gmrkng_col.html",
{
"entries": parcels
}
)
return html
def render_r(self, value, record: Intervention): def render_r(self, value, record: Intervention):
""" Renders the recorded column for an intervention """ Renders the recorded column for an intervention
@ -162,28 +187,3 @@ class InterventionTable(BaseTable, TableRenderMixin):
) )
return format_html(html) return format_html(html)
def render_rev(self, value, record: Intervention):
""" Renders the revocation column for an intervention
Args:
value (str): The revocation value
record (Intervention): The intervention record
Returns:
"""
html = ""
exists = value is not None
tooltip = _("No revocation")
if exists:
_date = value.date
added_ts = localtime(value.created.timestamp)
added_ts = added_ts.strftime(DEFAULT_DATE_TIME_FORMAT)
on = _date.strftime(DEFAULT_DATE_FORMAT)
tooltip = _("Revocation from {}, added on {} by {}").format(on, added_ts, value.created.user)
html += self.render_stop(
tooltip=tooltip,
icn_filled=exists,
)
return format_html(html)

View File

@ -33,7 +33,7 @@
{% trans 'Title' %} {% trans 'Title' %}
</th> </th>
{% if is_default_member and has_access %} {% if is_default_member and has_access %}
<th scope="col"> <th class="w-10" scope="col">
<span class="float-right"> <span class="float-right">
{% trans 'Action' %} {% trans 'Action' %}
</span> </span>

View File

@ -34,7 +34,7 @@
{% trans 'Created' %} {% trans 'Created' %}
</th> </th>
{% if is_default_member and has_access %} {% if is_default_member and has_access %}
<th scope="col"> <th class="w-10" scope="col">
<span class="float-right"> <span class="float-right">
{% trans 'Action' %} {% trans 'Action' %}
</span> </span>
@ -55,9 +55,12 @@
</td> </td>
<td class="align-middle">{{ deduction.surface|floatformat:2|intcomma }} m²</td> <td class="align-middle">{{ deduction.surface|floatformat:2|intcomma }} m²</td>
<td class="align-middle">{{ deduction.created.timestamp|default_if_none:""|naturalday}}</td> <td class="align-middle">{{ deduction.created.timestamp|default_if_none:""|naturalday}}</td>
<td> <td class="align-middle float-right">
{% if is_default_member and has_access %} {% if is_default_member and has_access %}
<button data-form-url="{% url 'intervention:remove-deduction' obj.id deduction.id %}" class="btn btn-default btn-modal float-right" title="{% trans 'Remove Deduction' %}"> <button data-form-url="{% url 'intervention:edit-deduction' obj.id deduction.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit Deduction' %}">
{% fa5_icon 'edit' %}
</button>
<button data-form-url="{% url 'intervention:remove-deduction' obj.id deduction.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove Deduction' %}">
{% fa5_icon 'trash' %} {% fa5_icon 'trash' %}
</button> </button>
{% endif %} {% endif %}

View File

@ -27,11 +27,14 @@
<th scope="col"> <th scope="col">
{% trans 'Title' %} {% trans 'Title' %}
</th> </th>
<th class="w-50" scope="col"> <th scope="col">
{% trans 'Created on' %}
</th>
<th scope="col">
{% trans 'Comment' %} {% trans 'Comment' %}
</th> </th>
{% if is_default_member and has_access %} {% if is_default_member and has_access %}
<th scope="col"> <th class="w-10" scope="col">
<span class="float-right"> <span class="float-right">
{% trans 'Action' %} {% trans 'Action' %}
</span> </span>
@ -43,18 +46,26 @@
{% for doc in obj.documents.all %} {% for doc in obj.documents.all %}
<tr> <tr>
<td> <td>
<a href="{% url 'intervention:get-doc' doc.id %}"> <a href="{% url 'intervention:get-doc' obj.id doc.id %}">
{{ doc.title }} {{ doc.title }}
</a> </a>
</td> </td>
<td>
<div class="scroll-150">
{{ doc.date_of_creation }}
</div>
</td>
<td> <td>
<div class="scroll-150"> <div class="scroll-150">
{{ doc.comment }} {{ doc.comment }}
</div> </div>
</td> </td>
<td> <td class="align-middle float-right">
{% if is_default_member and has_access %} {% if is_default_member and has_access %}
<button data-form-url="{% url 'intervention:remove-doc' doc.id %}" class="btn btn-default btn-modal float-right" title="{% trans 'Remove document' %}"> <button data-form-url="{% url 'intervention:edit-doc' obj.id doc.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit document' %}">
{% fa5_icon 'edit' %}
</button>
<button data-form-url="{% url 'intervention:remove-doc' obj.id doc.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove document' %}">
{% fa5_icon 'trash' %} {% fa5_icon 'trash' %}
</button> </button>
{% endif %} {% endif %}

View File

@ -34,7 +34,7 @@
{% trans 'Comment' %} {% trans 'Comment' %}
</th> </th>
{% if is_default_member and has_access %} {% if is_default_member and has_access %}
<th scope="col"> <th class="w-10" scope="col">
<span class="float-right"> <span class="float-right">
{% trans 'Action' %} {% trans 'Action' %}
</span> </span>
@ -54,9 +54,12 @@
{{ pay.comment }} {{ pay.comment }}
</div> </div>
</td> </td>
<td class="align-middle"> <td class="align-middle float-right">
{% if is_default_member and has_access %} {% if is_default_member and has_access %}
<button data-form-url="{% url 'compensation:pay:remove' pay.id %}" class="btn btn-default btn-modal float-right" title="{% trans 'Remove payment' %}"> <button data-form-url="{% url 'compensation:pay:edit' obj.id pay.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit payment' %}">
{% fa5_icon 'edit' %}
</button>
<button data-form-url="{% url 'compensation:pay:remove' obj.id pay.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove payment' %}">
{% fa5_icon 'trash' %} {% fa5_icon 'trash' %}
</button> </button>
{% endif %} {% endif %}

View File

@ -37,7 +37,7 @@
{% trans 'Comment' %} {% trans 'Comment' %}
</th> </th>
{% if is_default_member and has_access %} {% if is_default_member and has_access %}
<th scope="col"> <th class="w-10" scope="col">
<span class="float-right"> <span class="float-right">
{% trans 'Action' %} {% trans 'Action' %}
</span> </span>
@ -63,9 +63,12 @@
{{ rev.comment }} {{ rev.comment }}
</div> </div>
</td> </td>
<td class="align-middle"> <td class="align-middle float-right">
{% if is_default_member and has_access %} {% if is_default_member and has_access %}
<button data-form-url="{% url 'intervention:remove-revocation' rev.id %}" class="btn btn-default btn-modal float-right" title="{% trans 'Remove revocation' %}"> <button data-form-url="{% url 'intervention:edit-revocation' obj.id rev.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit revocation' %}">
{% fa5_icon 'edit' %}
</button>
<button data-form-url="{% url 'intervention:remove-revocation' obj.id rev.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove revocation' %}">
{% fa5_icon 'trash' %} {% fa5_icon 'trash' %}
</button> </button>
{% endif %} {% endif %}

View File

@ -10,6 +10,7 @@ from django.test import Client
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from django.urls import reverse from django.urls import reverse
from intervention.models import Revocation
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
from konova.tests.test_views import BaseViewTestCase from konova.tests.test_views import BaseViewTestCase
@ -20,19 +21,34 @@ class InterventionViewTestCase(BaseViewTestCase):
def setUpTestData(cls) -> None: def setUpTestData(cls) -> None:
super().setUpTestData() super().setUpTestData()
def setUp(self) -> None:
super().setUp()
# Prepare urls # Prepare urls
cls.index_url = reverse("intervention:index", args=()) self.index_url = reverse("intervention:index", args=())
cls.new_url = reverse("intervention:new", args=()) self.new_url = reverse("intervention:new", args=())
cls.new_id_url = reverse("intervention:new-id", args=()) self.new_id_url = reverse("intervention:new-id", args=())
cls.detail_url = reverse("intervention:detail", args=(cls.intervention.id,)) self.detail_url = reverse("intervention:detail", args=(self.intervention.id,))
cls.log_url = reverse("intervention:log", args=(cls.intervention.id,)) self.log_url = reverse("intervention:log", args=(self.intervention.id,))
cls.edit_url = reverse("intervention:edit", args=(cls.intervention.id,)) self.edit_url = reverse("intervention:edit", args=(self.intervention.id,))
cls.remove_url = reverse("intervention:remove", args=(cls.intervention.id,)) self.remove_url = reverse("intervention:remove", args=(self.intervention.id,))
cls.share_url = reverse("intervention:share", args=(cls.intervention.id, cls.intervention.access_token,)) self.share_url = reverse("intervention:share", args=(self.intervention.id, self.intervention.access_token,))
cls.share_create_url = reverse("intervention:share-create", args=(cls.intervention.id,)) self.share_create_url = reverse("intervention:share-create", args=(self.intervention.id,))
cls.run_check_url = reverse("intervention:check", args=(cls.intervention.id,)) self.run_check_url = reverse("intervention:check", args=(self.intervention.id,))
cls.record_url = reverse("intervention:record", args=(cls.intervention.id,)) self.record_url = reverse("intervention:record", args=(self.intervention.id,))
cls.report_url = reverse("intervention:report", args=(cls.intervention.id,)) self.report_url = reverse("intervention:report", args=(self.intervention.id,))
self.deduction.intervention = self.intervention
self.deduction.save()
self.deduction_new_url = reverse("intervention:new-deduction", args=(self.intervention.id,))
self.deduction_edit_url = reverse("intervention:edit-deduction", args=(self.intervention.id, self.deduction.id,))
self.deduction_remove_url = reverse("intervention:remove-deduction", args=(self.intervention.id, self.deduction.id))
self.revocation = Revocation.objects.create(
legal=self.intervention.legal
)
self.revocation_new_url = reverse("intervention:new-revocation", args=(self.intervention.id,))
self.revocation_edit_url = reverse("intervention:edit-revocation", args=(self.intervention.id, self.revocation.id))
self.revocation_remove_url = reverse("intervention:remove-revocation", args=(self.intervention.id, self.revocation.id))
def test_views_anonymous_user(self): def test_views_anonymous_user(self):
""" Check correct status code for all requests """ Check correct status code for all requests
@ -61,6 +77,12 @@ class InterventionViewTestCase(BaseViewTestCase):
self.share_create_url: f"{login_redirect_base}{self.share_create_url}", self.share_create_url: f"{login_redirect_base}{self.share_create_url}",
self.run_check_url: f"{login_redirect_base}{self.run_check_url}", self.run_check_url: f"{login_redirect_base}{self.run_check_url}",
self.record_url: f"{login_redirect_base}{self.record_url}", self.record_url: f"{login_redirect_base}{self.record_url}",
self.deduction_new_url: f"{login_redirect_base}{self.deduction_new_url}",
self.deduction_edit_url: f"{login_redirect_base}{self.deduction_edit_url}",
self.deduction_remove_url: f"{login_redirect_base}{self.deduction_remove_url}",
self.revocation_new_url: f"{login_redirect_base}{self.revocation_new_url}",
self.revocation_edit_url: f"{login_redirect_base}{self.revocation_edit_url}",
self.revocation_remove_url: f"{login_redirect_base}{self.revocation_remove_url}",
} }
self.assert_url_success(client, success_urls) self.assert_url_success(client, success_urls)
@ -96,6 +118,12 @@ class InterventionViewTestCase(BaseViewTestCase):
self.share_create_url, self.share_create_url,
self.run_check_url, self.run_check_url,
self.record_url, self.record_url,
self.revocation_new_url,
self.revocation_edit_url,
self.revocation_remove_url,
self.deduction_new_url,
self.deduction_edit_url,
self.deduction_remove_url,
] ]
self.assert_url_success(client, success_urls) self.assert_url_success(client, success_urls)
@ -128,6 +156,12 @@ class InterventionViewTestCase(BaseViewTestCase):
self.edit_url, self.edit_url,
self.remove_url, self.remove_url,
self.share_create_url, self.share_create_url,
self.revocation_new_url,
self.revocation_edit_url,
self.revocation_remove_url,
self.deduction_new_url,
self.deduction_edit_url,
self.deduction_remove_url,
] ]
fail_urls = [ fail_urls = [
self.run_check_url, self.run_check_url,
@ -172,6 +206,12 @@ class InterventionViewTestCase(BaseViewTestCase):
self.remove_url, self.remove_url,
self.share_create_url, self.share_create_url,
self.log_url, self.log_url,
self.revocation_new_url,
self.revocation_edit_url,
self.revocation_remove_url,
self.deduction_new_url,
self.deduction_edit_url,
self.deduction_remove_url,
] ]
success_urls_redirect = { success_urls_redirect = {
self.share_url: self.detail_url self.share_url: self.detail_url
@ -212,6 +252,12 @@ class InterventionViewTestCase(BaseViewTestCase):
self.remove_url, self.remove_url,
self.share_create_url, self.share_create_url,
self.record_url, self.record_url,
self.revocation_new_url,
self.revocation_edit_url,
self.revocation_remove_url,
self.deduction_new_url,
self.deduction_edit_url,
self.deduction_remove_url,
] ]
success_urls_redirect = { success_urls_redirect = {
self.share_url: self.detail_url self.share_url: self.detail_url
@ -252,6 +298,12 @@ class InterventionViewTestCase(BaseViewTestCase):
self.share_create_url, self.share_create_url,
self.record_url, self.record_url,
self.run_check_url, self.run_check_url,
self.revocation_new_url,
self.revocation_edit_url,
self.revocation_remove_url,
self.deduction_new_url,
self.deduction_edit_url,
self.deduction_remove_url,
] ]
success_urls_redirect = { success_urls_redirect = {
self.share_url: self.detail_url self.share_url: self.detail_url
@ -292,6 +344,12 @@ class InterventionViewTestCase(BaseViewTestCase):
self.remove_url, self.remove_url,
self.share_create_url, self.share_create_url,
self.run_check_url, self.run_check_url,
self.revocation_new_url,
self.revocation_edit_url,
self.revocation_remove_url,
self.deduction_new_url,
self.deduction_edit_url,
self.deduction_remove_url,
] ]
success_urls_redirect = { success_urls_redirect = {
self.share_url: self.detail_url self.share_url: self.detail_url
@ -332,6 +390,12 @@ class InterventionViewTestCase(BaseViewTestCase):
self.remove_url, self.remove_url,
self.share_create_url, self.share_create_url,
self.run_check_url, self.run_check_url,
self.revocation_new_url,
self.revocation_edit_url,
self.revocation_remove_url,
self.deduction_new_url,
self.deduction_edit_url,
self.deduction_remove_url,
] ]
# Define urls where a redirect to a specific location is the proper response # Define urls where a redirect to a specific location is the proper response
success_urls_redirect = { success_urls_redirect = {

View File

@ -74,6 +74,9 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase):
self.assertEqual(obj.identifier, test_id) self.assertEqual(obj.identifier, test_id)
self.assertEqual(obj.title, test_title) self.assertEqual(obj.title, test_title)
self.assert_equal_geometries(obj.geometry.geom, test_geom) self.assert_equal_geometries(obj.geometry.geom, test_geom)
self.assertEqual(1, obj.log.count())
self.assertEqual(obj.log.first().action, UserAction.CREATED)
self.assertEqual(obj.log.first().user, self.superuser)
except ObjectDoesNotExist: except ObjectDoesNotExist:
# Fail if there is no such object # Fail if there is no such object
self.fail() self.fail()
@ -215,6 +218,8 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase):
# Make sure there are no payments on the intervention, yet # Make sure there are no payments on the intervention, yet
self.assertEqual(0, self.intervention.payments.count()) self.assertEqual(0, self.intervention.payments.count())
pre_payment_logs_count = self.intervention.log.count()
# Create form data to be sent to the url # Create form data to be sent to the url
test_amount = 10.00 test_amount = 10.00
test_due = "2021-01-01" test_due = "2021-01-01"
@ -239,6 +244,10 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase):
self.assertEqual(payment.amount, test_amount) self.assertEqual(payment.amount, test_amount)
self.assertEqual(payment.due_on, datetime.date.fromisoformat(test_due)) self.assertEqual(payment.due_on, datetime.date.fromisoformat(test_due))
self.assertEqual(payment.comment, test_comment) self.assertEqual(payment.comment, test_comment)
# Make sure a log entry has been created
self.assertEqual(self.intervention.log.first().action, UserAction.EDITED)
self.assertEqual(pre_payment_logs_count + 1, self.intervention.log.count())
return payment return payment
def subtest_delete_payment(self, payment: Payment): def subtest_delete_payment(self, payment: Payment):
@ -250,8 +259,10 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase):
Returns: Returns:
""" """
pre_payment_logs_count = self.intervention.log.count()
# Create removing url for the payment # Create removing url for the payment
remove_url = reverse("compensation:pay:remove", args=(payment.id,)) remove_url = reverse("compensation:pay:remove", args=(self.intervention.id, payment.id,))
post_data = { post_data = {
"confirm": True, "confirm": True,
} }
@ -266,6 +277,11 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase):
# Now make sure the intervention has no payments anymore # Now make sure the intervention has no payments anymore
self.assertEqual(0, self.intervention.payments.count()) self.assertEqual(0, self.intervention.payments.count())
# Make sure a log entry has been created
self.assertEqual(self.intervention.log.first().action, UserAction.EDITED)
self.assertEqual(self.intervention.log.first().user, self.superuser)
self.assertEqual(pre_payment_logs_count + 1, self.intervention.log.count())
def test_payments(self): def test_payments(self):
""" """
Checks a 'normal' case of adding a payment. Checks a 'normal' case of adding a payment.
@ -353,6 +369,8 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase):
Returns: Returns:
""" """
pre_deduction_logs_count = self.intervention.log.count()
# 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:
@ -376,6 +394,11 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase):
) )
self.assertEqual(deduction.surface, test_surface) self.assertEqual(deduction.surface, test_surface)
# Make sure a log entry has been created
self.assertEqual(self.intervention.log.first().action, UserAction.EDITED)
self.assertEqual(self.intervention.log.first().user, self.superuser)
self.assertEqual(pre_deduction_logs_count + 1, self.intervention.log.count())
# Return deduction for further usage in tests # Return deduction for further usage in tests
return deduction return deduction
@ -414,6 +437,8 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase):
Returns: Returns:
""" """
pre_delete_logs_count = self.intervention.log.count()
# Prepare url for deleting of this deduction # Prepare url for deleting of this deduction
delete_url = reverse("compensation:acc:remove-deduction", args=(self.eco_account.id, deduction.id,)) delete_url = reverse("compensation:acc:remove-deduction", args=(self.eco_account.id, deduction.id,))
post_data = { post_data = {
@ -433,6 +458,11 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase):
# Expect the deduction to be totally gone # Expect the deduction to be totally gone
self.assert_object_is_deleted(deduction) self.assert_object_is_deleted(deduction)
# Make sure a log entry has been created
self.assertEqual(self.intervention.log.first().action, UserAction.EDITED)
self.assertEqual(self.intervention.log.first().user, self.superuser)
self.assertEqual(pre_delete_logs_count + 1, self.intervention.log.count())
def test_deduction(self): def test_deduction(self):
""" """
Checks a 'normal case of adding a deduction. Checks a 'normal case of adding a deduction.

View File

@ -10,7 +10,7 @@ from django.urls import path
from intervention.views import index_view, new_view, detail_view, edit_view, remove_view, new_document_view, share_view, \ from intervention.views import index_view, new_view, detail_view, edit_view, remove_view, new_document_view, share_view, \
create_share_view, remove_revocation_view, new_revocation_view, check_view, log_view, new_deduction_view, \ create_share_view, remove_revocation_view, new_revocation_view, check_view, log_view, new_deduction_view, \
record_view, remove_document_view, get_document_view, get_revocation_view, new_id_view, report_view, \ record_view, remove_document_view, get_document_view, get_revocation_view, new_id_view, report_view, \
remove_deduction_view, remove_compensation_view remove_deduction_view, remove_compensation_view, edit_deduction_view, edit_revocation_view, edit_document_view
app_name = "intervention" app_name = "intervention"
urlpatterns = [ urlpatterns = [
@ -28,19 +28,22 @@ urlpatterns = [
path('<id>/report', report_view, name='report'), path('<id>/report', report_view, name='report'),
# Compensations # Compensations
path('<id>/remove/<comp_id>', remove_compensation_view, name='remove-compensation'), path('<id>/compensation/<comp_id>/remove', remove_compensation_view, name='remove-compensation'),
# Documents # Documents
path('<id>/document/new/', new_document_view, name='new-doc'), path('<id>/document/new/', new_document_view, name='new-doc'),
path('document/<doc_id>', get_document_view, name='get-doc'), path('<id>/document/<doc_id>', get_document_view, name='get-doc'),
path('document/<doc_id>/remove/', remove_document_view, name='remove-doc'), path('<id>/document/<doc_id>/remove/', remove_document_view, name='remove-doc'),
path('<id>/document/<doc_id>/edit/', edit_document_view, name='edit-doc'),
# Deductions # Deductions
path('<id>/deduction/new', new_deduction_view, name='new-deduction'), path('<id>/deduction/new', new_deduction_view, name='new-deduction'),
path('<id>/remove/<deduction_id>', remove_deduction_view, name='remove-deduction'), path('<id>/deduction/<deduction_id>/edit', edit_deduction_view, name='edit-deduction'),
path('<id>/deduction/<deduction_id>/remove', remove_deduction_view, name='remove-deduction'),
# Revocation routes # Revocation routes
path('<id>/revocation/new', new_revocation_view, name='new-revocation'), path('<id>/revocation/new', new_revocation_view, name='new-revocation'),
path('revocation/<id>/remove', remove_revocation_view, name='remove-revocation'), path('<id>/revocation/<revocation_id>/edit', edit_revocation_view, name='edit-revocation'),
path('<id>/revocation/<revocation_id>/remove', remove_revocation_view, name='remove-revocation'),
path('revocation/<doc_id>', get_revocation_view, name='get-doc-revocation'), path('revocation/<doc_id>', get_revocation_view, name='get-doc-revocation'),
] ]

View File

@ -6,18 +6,19 @@ from django.shortcuts import render
from intervention.forms.forms import NewInterventionForm, EditInterventionForm from intervention.forms.forms import NewInterventionForm, EditInterventionForm
from intervention.forms.modalForms import ShareModalForm, NewRevocationModalForm, \ from intervention.forms.modalForms import ShareModalForm, NewRevocationModalForm, \
CheckModalForm, NewDeductionModalForm, NewInterventionDocumentForm CheckModalForm, NewDeductionModalForm, NewInterventionDocumentModalForm, RemoveEcoAccountDeductionModalForm, \
RemoveRevocationModalForm, EditEcoAccountDeductionModalForm, EditRevocationModalForm
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
from konova.decorators import * from konova.decorators import *
from konova.forms import SimpleGeomForm, RemoveModalForm, RecordModalForm from konova.forms import SimpleGeomForm, RemoveModalForm, RecordModalForm, EditDocumentModalForm
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.documents import remove_document, get_document from konova.utils.documents import remove_document, get_document
from konova.utils.generators import generate_qr_code from konova.utils.generators import generate_qr_code
from konova.utils.message_templates import INTERVENTION_INVALID, FORM_INVALID, IDENTIFIER_REPLACED, \ from konova.utils.message_templates import INTERVENTION_INVALID, FORM_INVALID, IDENTIFIER_REPLACED, \
CHECKED_RECORDED_RESET, DEDUCTION_REMOVED, DEDUCTION_ADDED, REVOCATION_ADDED, REVOCATION_REMOVED, \ CHECKED_RECORDED_RESET, DEDUCTION_REMOVED, DEDUCTION_ADDED, REVOCATION_ADDED, REVOCATION_REMOVED, \
COMPENSATION_REMOVED_TEMPLATE, DOCUMENT_ADDED COMPENSATION_REMOVED_TEMPLATE, DOCUMENT_ADDED, DEDUCTION_EDITED, REVOCATION_EDITED, DOCUMENT_EDITED
from konova.utils.user_checks import in_group from konova.utils.user_checks import in_group
@ -128,7 +129,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 = NewInterventionDocumentForm(request.POST or None, request.FILES or None, instance=intervention, request=request) form = NewInterventionDocumentModalForm(request.POST or None, request.FILES or None, instance=intervention, request=request)
return form.process_request( return form.process_request(
request, request,
msg_success=DOCUMENT_ADDED, msg_success=DOCUMENT_ADDED,
@ -160,48 +161,44 @@ def get_revocation_view(request: HttpRequest, doc_id: str):
return redirect("intervention:detail", id=doc.instance.id) return redirect("intervention:detail", id=doc.instance.id)
return get_document(doc) return get_document(doc)
@login_required @login_required
@default_group_required @default_group_required
def get_document_view(request: HttpRequest, doc_id: str): @shared_access_required(Intervention, "id")
def get_document_view(request: HttpRequest, id: str, doc_id: str):
""" Returns the document as downloadable file """ Returns the document as downloadable file
Wraps the generic document fetcher function from konova.utils. Wraps the generic document fetcher function from konova.utils.
Args: Args:
request (HttpRequest): The incoming request request (HttpRequest): The incoming request
id (str): The intervention id
doc_id (str): The document id doc_id (str): The document id
Returns: Returns:
""" """
intervention = get_object_or_404(Intervention, id=id)
doc = get_object_or_404(InterventionDocument, id=doc_id) doc = get_object_or_404(InterventionDocument, id=doc_id)
user = request.user
instance = doc.instance
# File download only possible if related instance is shared with user
if not instance.users.filter(id=user.id):
messages.info(
request,
DATA_UNSHARED
)
return redirect("intervention:detail", id=instance.id)
return get_document(doc) return get_document(doc)
@login_required @login_required
@default_group_required @default_group_required
def remove_document_view(request: HttpRequest, doc_id: str): @shared_access_required(Intervention, "id")
def remove_document_view(request: HttpRequest, id: str, doc_id: str):
""" Removes the document from the database and file system """ Removes the document from the database and file system
Wraps the generic functionality from konova.utils. Wraps the generic functionality from konova.utils.
Args: Args:
request (HttpRequest): The incoming request request (HttpRequest): The incoming request
id (str): The intervention id
doc_id (str): The document id doc_id (str): The document id
Returns: Returns:
""" """
intervention = get_object_or_404(Intervention, id=id)
doc = get_object_or_404(InterventionDocument, id=doc_id) doc = get_object_or_404(InterventionDocument, id=doc_id)
return remove_document( return remove_document(
request, request,
@ -209,6 +206,32 @@ def remove_document_view(request: HttpRequest, doc_id: str):
) )
@login_required
@default_group_required
@shared_access_required(Intervention, "id")
def edit_document_view(request: HttpRequest, id: str, doc_id: str):
""" Removes the document from the database and file system
Wraps the generic functionality from konova.utils.
Args:
request (HttpRequest): The incoming request
id (str): The intervention id
doc_id (str): The document id
Returns:
"""
intervention = get_object_or_404(Intervention, id=id)
doc = get_object_or_404(InterventionDocument, id=doc_id)
form = EditDocumentModalForm(request.POST or None, request.FILES or None, instance=intervention, document=doc, request=request)
return form.process_request(
request,
DOCUMENT_EDITED,
redirect_url=reverse("intervention:detail", args=(intervention.id,)) + "#related_data"
)
@login_required @login_required
@any_group_check @any_group_check
def detail_view(request: HttpRequest, id: str): def detail_view(request: HttpRequest, id: str):
@ -244,14 +267,6 @@ def detail_view(request: HttpRequest, id: str):
parcels = intervention.get_underlying_parcels() parcels = intervention.get_underlying_parcels()
# Inform user about revocation
if intervention.legal.revocations.exists():
messages.error(
request,
_("This intervention has {} revocations").format(intervention.legal.revocations.count()),
extra_tags="danger",
)
context = { context = {
"obj": intervention, "obj": intervention,
"compensations": compensations, "compensations": compensations,
@ -340,23 +355,51 @@ def remove_view(request: HttpRequest, id: str):
@login_required @login_required
@default_group_required @default_group_required
def remove_revocation_view(request: HttpRequest, id: str): @shared_access_required(Intervention, "id")
""" Renders a remove view for a revocation def edit_revocation_view(request: HttpRequest, id: str, revocation_id: str):
""" Renders a edit view for a revocation
Args: Args:
request (HttpRequest): The incoming request request (HttpRequest): The incoming request
id (str): The revocation's id as string id (str): The intervention's id as string
revocation_id (str): The revocation's id as string
Returns: Returns:
""" """
obj = Revocation.objects.get(id=id) intervention = get_object_or_404(Intervention, id=id)
revocation = get_object_or_404(Revocation, id=revocation_id)
form = RemoveModalForm(request.POST or None, instance=obj, request=request) form = EditRevocationModalForm(request.POST or None, request.FILES or None, instance=intervention, revocation=revocation, request=request)
return form.process_request(
request,
REVOCATION_EDITED,
redirect_url=reverse("intervention:detail", args=(intervention.id,)) + "#related_data"
)
@login_required
@default_group_required
@shared_access_required(Intervention, "id")
def remove_revocation_view(request: HttpRequest, id: str, revocation_id: str):
""" Renders a remove view for a revocation
Args:
request (HttpRequest): The incoming request
id (str): The intervention's id as string
revocation_id (str): The revocation's id as string
Returns:
"""
intervention = get_object_or_404(Intervention, id=id)
revocation = get_object_or_404(Revocation, id=revocation_id)
form = RemoveRevocationModalForm(request.POST or None, instance=intervention, revocation=revocation, request=request)
return form.process_request( return form.process_request(
request, request,
REVOCATION_REMOVED, REVOCATION_REMOVED,
redirect_url=reverse("intervention:detail", args=(obj.intervention.id,)) + "#related_data" redirect_url=reverse("intervention:detail", args=(intervention.id,)) + "#related_data"
) )
@ -533,7 +576,7 @@ def remove_deduction_view(request: HttpRequest, id: str, deduction_id: str):
except ObjectDoesNotExist: except ObjectDoesNotExist:
raise Http404("Unknown deduction") raise Http404("Unknown deduction")
form = RemoveModalForm(request.POST or None, instance=eco_deduction, request=request) form = RemoveEcoAccountDeductionModalForm(request.POST or None, instance=intervention, deduction=eco_deduction, request=request)
return form.process_request( return form.process_request(
request=request, request=request,
msg_success=DEDUCTION_REMOVED, msg_success=DEDUCTION_REMOVED,
@ -541,6 +584,34 @@ def remove_deduction_view(request: HttpRequest, id: str, deduction_id: str):
) )
@login_required
@default_group_required
@shared_access_required(Intervention, "id")
def edit_deduction_view(request: HttpRequest, id: str, deduction_id: str):
""" Renders a modal view for removing deductions
Args:
request (HttpRequest): The incoming request
id (str): The intervention's id
deduction_id (str): The deduction's id
Returns:
"""
intervention = get_object_or_404(Intervention, id=id)
try:
eco_deduction = intervention.deductions.get(id=deduction_id)
except ObjectDoesNotExist:
raise Http404("Unknown deduction")
form = EditEcoAccountDeductionModalForm(request.POST or None, instance=intervention, deduction=eco_deduction, request=request)
return form.process_request(
request=request,
msg_success=DEDUCTION_EDITED,
redirect_url=reverse("intervention:detail", args=(id,)) + "#related_data"
)
@login_required @login_required
@conservation_office_group_required @conservation_office_group_required
@shared_access_required(Intervention, "id") @shared_access_required(Intervention, "id")

View File

@ -35,8 +35,9 @@ class EcoAccountAutocomplete(Select2QuerySetView):
) )
if self.q: if self.q:
qs = qs.filter( qs = qs.filter(
identifier__icontains=self.q Q(identifier__icontains=self.q) |
) Q(title__icontains=self.q)
).distinct()
return qs return qs
@ -57,8 +58,9 @@ class InterventionAutocomplete(Select2QuerySetView):
) )
if self.q: if self.q:
qs = qs.filter( qs = qs.filter(
identifier__icontains=self.q Q(identifier__icontains=self.q) |
) Q(title__icontains=self.q)
).distinct()
return qs return qs
@ -81,8 +83,9 @@ class ShareUserAutocomplete(Select2QuerySetView):
if self.q: if self.q:
# Due to privacy concerns only a full username match will return the proper user entry # Due to privacy concerns only a full username match will return the proper user entry
qs = qs.filter( qs = qs.filter(
username=self.q Q(username=self.q) |
) Q(email=self.q)
).distinct()
return qs return qs

View File

@ -12,6 +12,8 @@ from bootstrap_modal_forms.forms import BSModalForm
from bootstrap_modal_forms.utils import is_ajax from bootstrap_modal_forms.utils import is_ajax
from django import forms from django import forms
from django.contrib import messages from django.contrib import messages
from django.db.models.fields.files import FieldFile
from user.models import User from user.models import User
from django.contrib.gis.forms import OSMWidget, MultiPolygonField from django.contrib.gis.forms import OSMWidget, MultiPolygonField
from django.contrib.gis.geos import MultiPolygon from django.contrib.gis.geos import MultiPolygon
@ -21,10 +23,10 @@ from django.shortcuts import render
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from konova.contexts import BaseContext from konova.contexts import BaseContext
from konova.models import BaseObject, Geometry, RecordableObjectMixin from konova.models import BaseObject, Geometry, RecordableObjectMixin, AbstractDocument
from konova.settings import DEFAULT_SRID from konova.settings import DEFAULT_SRID
from konova.tasks import celery_update_parcels from konova.tasks import celery_update_parcels
from konova.utils.message_templates import FORM_INVALID from konova.utils.message_templates import FORM_INVALID, FILE_TYPE_UNSUPPORTED, FILE_SIZE_TOO_LARGE, DOCUMENT_EDITED
from user.models import UserActionLogEntry from user.models import UserActionLogEntry
@ -87,7 +89,7 @@ class BaseForm(forms.Form):
""" """
self.fields[field].widget.attrs["placeholder"] = val self.fields[field].widget.attrs["placeholder"] = val
def load_initial_data(self, form_data: dict, disabled_fields: list): def load_initial_data(self, form_data: dict, disabled_fields: list = None):
""" Initializes form data from instance """ Initializes form data from instance
Inserts instance data into form and disables form fields Inserts instance data into form and disables form fields
@ -99,8 +101,9 @@ class BaseForm(forms.Form):
return return
for k, v in form_data.items(): for k, v in form_data.items():
self.initialize_form_field(k, v) self.initialize_form_field(k, v)
for field in disabled_fields: if disabled_fields:
self.disable_form_field(field) for field in disabled_fields:
self.disable_form_field(field)
def add_widget_html_class(self, field: str, cls: str): def add_widget_html_class(self, field: str, cls: str):
""" Adds a HTML class string to the widget of a field """ Adds a HTML class string to the widget of a field
@ -327,10 +330,27 @@ class RemoveModalForm(BaseModalForm):
self.instance.mark_as_deleted(self.user) self.instance.mark_as_deleted(self.user)
else: else:
# If the class does not provide restorable delete functionality, we must delete the entry finally # If the class does not provide restorable delete functionality, we must delete the entry finally
self.instance.delete(self.user) self.instance.delete()
class NewDocumentForm(BaseModalForm): class RemoveDeadlineModalForm(RemoveModalForm):
""" Removing modal form for deadlines
Can be used for anything, where removing shall be confirmed by the user a second time.
"""
deadline = None
def __init__(self, *args, **kwargs):
deadline = kwargs.pop("deadline", None)
self.deadline = deadline
super().__init__(*args, **kwargs)
def save(self):
self.instance.remove_deadline(self)
class NewDocumentModalForm(BaseModalForm):
""" Modal form for new documents """ Modal form for new documents
""" """
@ -402,18 +422,22 @@ class NewDocumentForm(BaseModalForm):
_file = self.cleaned_data.get("file", None) _file = self.cleaned_data.get("file", None)
if _file is None or isinstance(_file, FieldFile):
# FieldFile declares that no new file has been uploaded and we do not need to check on the file again
return super_valid
mime_type_valid = self.document_model.is_mime_type_valid(_file) mime_type_valid = self.document_model.is_mime_type_valid(_file)
if not mime_type_valid: if not mime_type_valid:
self.add_error( self.add_error(
"file", "file",
_("Unsupported file type") FILE_TYPE_UNSUPPORTED
) )
file_size_valid = self.document_model.is_file_size_valid(_file) file_size_valid = self.document_model.is_file_size_valid(_file)
if not file_size_valid: if not file_size_valid:
self.add_error( self.add_error(
"file", "file",
_("File too large") FILE_SIZE_TOO_LARGE
) )
file_valid = mime_type_valid and file_size_valid file_valid = mime_type_valid and file_size_valid
@ -440,6 +464,39 @@ class NewDocumentForm(BaseModalForm):
return doc return doc
class EditDocumentModalForm(NewDocumentModalForm):
document = None
document_model = AbstractDocument
def __init__(self, *args, **kwargs):
self.document = kwargs.pop("document", None)
super().__init__(*args, **kwargs)
form_data = {
"title": self.document.title,
"comment": self.document.comment,
"creation_date": str(self.document.date_of_creation),
"file": self.document.file,
}
self.load_initial_data(form_data)
def save(self):
with transaction.atomic():
document = self.document
file = self.cleaned_data.get("file", None)
document.title = self.cleaned_data.get("title", None)
document.comment = self.cleaned_data.get("comment", None)
document.date_of_creation = self.cleaned_data.get("creation_date", None)
if not isinstance(file, FieldFile):
document.replace_file(file)
document.save()
self.instance.mark_as_edited(self.user, self.request, edit_comment=DOCUMENT_EDITED)
return document
class RecordModalForm(BaseModalForm): class RecordModalForm(BaseModalForm):
""" Modal form for recording data """ Modal form for recording data

View File

@ -0,0 +1,54 @@
# Generated by Django 3.1.3 on 2022-02-08 17:01
from django.db import migrations, models
import django.db.models.deletion
import uuid
def migrate_parcels(apps, schema_editor):
Geometry = apps.get_model('konova', 'Geometry')
SpatialIntersection = apps.get_model('konova', 'SpatialIntersection')
all_geoms = Geometry.objects.all()
for geom in all_geoms:
SpatialIntersection.objects.bulk_create([
SpatialIntersection(geometry=geom, parcel=parcel)
for parcel in geom.parcels.all()
])
class Migration(migrations.Migration):
dependencies = [
('konova', '0002_auto_20220114_0936'),
]
operations = [
migrations.CreateModel(
name='SpatialIntersection',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('calculated_on', models.DateTimeField(auto_now_add=True, null=True)),
('geometry', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='konova.geometry')),
('parcel', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='konova.parcel')),
],
options={
'abstract': False,
},
),
migrations.RunPython(migrate_parcels),
migrations.AddField(
model_name='parcel',
name='geometries_tmp',
field=models.ManyToManyField(blank=True, related_name='parcels', through='konova.SpatialIntersection', to='konova.Geometry'),
),
migrations.RemoveField(
model_name='parcel',
name='geometries',
),
migrations.RenameField(
model_name='parcel',
old_name='geometries_tmp',
new_name='geometries',
),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 3.1.3 on 2022-02-09 07:39
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('konova', '0003_auto_20220208_1801'),
]
operations = [
migrations.RenameModel(
old_name='SpatialIntersection',
new_name='ParcelIntersection',
),
]

View File

@ -101,3 +101,19 @@ class AbstractDocument(BaseResource):
def is_file_size_valid(cls, _file): def is_file_size_valid(cls, _file):
max_size = cls._maximum_file_size * pow(1000, 2) max_size = cls._maximum_file_size * pow(1000, 2)
return _file.size <= max_size return _file.size <= max_size
def replace_file(self, new_file):
""" Replaces the old file on the hard drive with the new one
Args:
new_file (File): The new file
Returns:
"""
try:
os.remove(self.file.file.name)
except FileNotFoundError:
pass
self.file = new_file
self.save()

View File

@ -99,7 +99,7 @@ class Geometry(BaseResource):
Returns: Returns:
""" """
from konova.models import Parcel, District from konova.models import Parcel, District, ParcelIntersection
parcel_fetcher = ParcelWFSFetcher( parcel_fetcher = ParcelWFSFetcher(
geometry_id=self.id, geometry_id=self.id,
) )
@ -107,6 +107,7 @@ class Geometry(BaseResource):
fetched_parcels = parcel_fetcher.get_features( fetched_parcels = parcel_fetcher.get_features(
typename typename
) )
_now = timezone.now()
underlying_parcels = [] underlying_parcels = []
for result in fetched_parcels: for result in fetched_parcels:
fetched_parcel = result[typename] fetched_parcel = result[typename]
@ -125,19 +126,35 @@ class Geometry(BaseResource):
krs=fetched_parcel["ave:kreis"], krs=fetched_parcel["ave:kreis"],
)[0] )[0]
parcel_obj.district = district parcel_obj.district = district
parcel_obj.updated_on = timezone.now() parcel_obj.updated_on = _now
parcel_obj.save() parcel_obj.save()
underlying_parcels.append(parcel_obj) underlying_parcels.append(parcel_obj)
# Update the linked parcels
self.parcels.set(underlying_parcels) self.parcels.set(underlying_parcels)
# Set the calculated_on intermediate field, so this related data will be found on lookups
intersections_without_ts = self.parcelintersection_set.filter(
parcel__in=self.parcels.all(),
calculated_on__isnull=True,
)
for entry in intersections_without_ts:
entry.calculated_on = _now
ParcelIntersection.objects.bulk_update(
intersections_without_ts,
["calculated_on"]
)
def get_underlying_parcels(self): def get_underlying_parcels(self):
""" Getter for related parcels and their districts """ Getter for related parcels and their districts
Returns: Returns:
parcels (QuerySet): The related parcels as queryset parcels (QuerySet): The related parcels as queryset
""" """
parcels = self.parcels.all().prefetch_related(
parcels = self.parcels.filter(
parcelintersection__calculated_on__isnull=False,
).prefetch_related(
"district" "district"
).order_by( ).order_by(
"gmrkng", "gmrkng",

View File

@ -12,6 +12,7 @@ from abc import abstractmethod
from django.contrib import messages from django.contrib import messages
from django.db.models import QuerySet from django.db.models import QuerySet
from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP, LANIS_ZOOM_LUT, LANIS_LINK_TEMPLATE
from konova.tasks import celery_send_mail_shared_access_removed, celery_send_mail_shared_access_given, \ from konova.tasks import celery_send_mail_shared_access_removed, celery_send_mail_shared_access_given, \
celery_send_mail_shared_data_recorded, celery_send_mail_shared_data_unrecorded, \ celery_send_mail_shared_data_recorded, celery_send_mail_shared_data_unrecorded, \
celery_send_mail_shared_data_deleted, celery_send_mail_shared_data_checked celery_send_mail_shared_data_deleted, celery_send_mail_shared_data_checked
@ -128,10 +129,27 @@ class BaseObject(BaseResource):
# Send mail # Send mail
shared_users = self.shared_users.values_list("id", flat=True) shared_users = self.shared_users.values_list("id", flat=True)
for user_id in shared_users: for user_id in shared_users:
celery_send_mail_shared_data_deleted.delay(self.identifier, user_id) celery_send_mail_shared_data_deleted.delay(self.identifier, self.title, user_id)
self.save() self.save()
def mark_as_edited(self, performing_user: User, request: HttpRequest = None, edit_comment: str = None):
""" In case the object or a related object changed the log history needs to be updated
Args:
performing_user (User): The user which performed the editing action
request (HttpRequest): The used request for this action
edit_comment (str): Additional comment for the log entry
Returns:
"""
edit_action = UserActionLogEntry.get_edited_action(performing_user, edit_comment)
self.modified = edit_action
self.log.add(edit_action)
self.save()
return edit_action
def add_log_entry(self, action: UserAction, user: User, comment: str): def add_log_entry(self, action: UserAction, user: User, comment: str):
""" Wraps adding of UserActionLogEntry to log """ Wraps adding of UserActionLogEntry to log
@ -200,6 +218,10 @@ class BaseObject(BaseResource):
_str = "{}{}-{}".format(curr_month, curr_year, rand_str) _str = "{}{}-{}".format(curr_month, curr_year, rand_str)
return definitions[self.__class__]["template"].format(_str) return definitions[self.__class__]["template"].format(_str)
@abstractmethod
def get_detail_url(self):
raise NotImplementedError()
class RecordableObjectMixin(models.Model): class RecordableObjectMixin(models.Model):
""" Wraps record related fields and functionality """ Wraps record related fields and functionality
@ -236,7 +258,7 @@ class RecordableObjectMixin(models.Model):
shared_users = self.users.all().values_list("id", flat=True) shared_users = self.users.all().values_list("id", flat=True)
for user_id in shared_users: for user_id in shared_users:
celery_send_mail_shared_data_unrecorded.delay(self.identifier, user_id) celery_send_mail_shared_data_unrecorded.delay(self.identifier, self.title, user_id)
return action return action
@ -258,29 +280,22 @@ class RecordableObjectMixin(models.Model):
shared_users = self.users.all().values_list("id", flat=True) shared_users = self.users.all().values_list("id", flat=True)
for user_id in shared_users: for user_id in shared_users:
celery_send_mail_shared_data_recorded.delay(self.identifier, user_id) celery_send_mail_shared_data_recorded.delay(self.identifier, self.title, user_id)
return action return action
def mark_as_edited(self, performing_user: User, request: HttpRequest = None, edit_comment: str = None, reset_recorded: bool = True): def unrecord(self, performing_user: User, request: HttpRequest = None):
""" In case the object or a related object changed, internal processes need to be started, such as """ Unrecords a dataset
unrecord and uncheck
Args: Args:
performing_user (User): The user which performed the editing action performing_user (User): The user which performed the editing action
request (HttpRequest): The used request for this action request (HttpRequest): The used request for this action
edit_comment (str): Additional comment for the log entry
reset_recorded (bool): Whether the record-state of the object should be reset
Returns: Returns:
""" """
edit_action = UserActionLogEntry.get_edited_action(performing_user, edit_comment) action = None
self.modified = edit_action if self.recorded:
self.log.add(edit_action)
self.save()
if self.recorded and reset_recorded:
action = self.set_unrecorded(performing_user) action = self.set_unrecorded(performing_user)
self.log.add(action) self.log.add(action)
if request: if request:
@ -288,7 +303,7 @@ class RecordableObjectMixin(models.Model):
request, request,
CHECKED_RECORDED_RESET CHECKED_RECORDED_RESET
) )
return edit_action return action
@abstractmethod @abstractmethod
def is_ready_for_publish(self) -> bool: def is_ready_for_publish(self) -> bool:
@ -350,7 +365,7 @@ class CheckableObjectMixin(models.Model):
# Send mail # Send mail
shared_users = self.users.all().values_list("id", flat=True) shared_users = self.users.all().values_list("id", flat=True)
for user_id in shared_users: for user_id in shared_users:
celery_send_mail_shared_data_checked.delay(self.identifier, user_id) celery_send_mail_shared_data_checked.delay(self.identifier, self.title, user_id)
self.log.add(action) self.log.add(action)
return action return action
@ -464,9 +479,9 @@ class ShareableObjectMixin(models.Model):
# Send mails # Send mails
for user in removed_users: for user in removed_users:
celery_send_mail_shared_access_removed.delay(self.identifier, user["id"]) celery_send_mail_shared_access_removed.delay(self.identifier, self.title, user["id"])
for user in new_accessing_users: for user in new_accessing_users:
celery_send_mail_shared_access_given.delay(self.identifier, user) celery_send_mail_shared_access_given.delay(self.identifier, self.title, user)
# Set new shared users # Set new shared users
self.share_with_list(users) self.share_with_list(users)
@ -530,3 +545,31 @@ class GeoReferencedMixin(models.Model):
message_str = GEOMETRY_CONFLICT_WITH_TEMPLATE.format(instance_identifiers) message_str = GEOMETRY_CONFLICT_WITH_TEMPLATE.format(instance_identifiers)
messages.info(request, message_str) messages.info(request, message_str)
return request return request
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
area = int(geom.envelope.area)
z_l = 16
for k_area, v_zoom in LANIS_ZOOM_LUT.items():
if k_area < area:
z_l = v_zoom
break
zoom_lvl = z_l
except (AttributeError, IndexError) as e:
# If no geometry has been added, yet.
x = 1
y = 1
zoom_lvl = 6
return LANIS_LINK_TEMPLATE.format(
zoom_lvl,
x,
y,
)

View File

@ -22,7 +22,7 @@ class Parcel(UuidModel):
To avoid conflicts due to german Umlaute, the field names are shortened and vocals are dropped. To avoid conflicts due to german Umlaute, the field names are shortened and vocals are dropped.
""" """
geometries = models.ManyToManyField("konova.Geometry", related_name="parcels", blank=True) geometries = models.ManyToManyField("konova.Geometry", blank=True, related_name="parcels", through='ParcelIntersection')
district = models.ForeignKey("konova.District", on_delete=models.SET_NULL, null=True, blank=True, related_name="parcels") district = models.ForeignKey("konova.District", on_delete=models.SET_NULL, null=True, blank=True, related_name="parcels")
gmrkng = models.CharField( gmrkng = models.CharField(
max_length=1000, max_length=1000,
@ -77,3 +77,22 @@ class District(UuidModel):
def __str__(self): def __str__(self):
return f"{self.gmnd} | {self.krs}" return f"{self.gmnd} | {self.krs}"
class ParcelIntersection(UuidModel):
""" ParcelIntersection is an intermediary model, which is used to configure the
M2M relation between Parcel and Geometry.
Based on uuids, we will not have (practically) any problems on outrunning primary keys
and extending the model with calculated_on timestamp, we can 'hide' entries while they
are being recalculated and keep track on the last time they have been calculated this
way.
Please note: The calculated_on describes when the relation between the Parcel and the Geometry
has been established. The updated_on field of Parcel describes when this Parcel has been
changed the last time.
"""
parcel = models.ForeignKey(Parcel, on_delete=models.CASCADE)
geometry = models.ForeignKey("konova.Geometry", on_delete=models.CASCADE)
calculated_on = models.DateTimeField(auto_now_add=True, null=True, blank=True)

View File

@ -219,6 +219,13 @@ Overwrites bootstrap .btn:focus box shadow color
overflow: auto; overflow: auto;
} }
.w-20{
width: 20%;
}
.w-10{
width: 20%;
}
/* /*
Extends css for django autocomplete light (dal) Extends css for django autocomplete light (dal)
No other approach worked to get the autocomplete fields to full width of parent containers No other approach worked to get the autocomplete fields to full width of parent containers

View File

@ -4,13 +4,19 @@ from celery import shared_task
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
@shared_task @shared_task
def celery_update_parcels(geometry_id: str, recheck: bool = True): def celery_update_parcels(geometry_id: str, recheck: bool = True):
from konova.models import Geometry from konova.models import Geometry, ParcelIntersection
try: try:
geom = Geometry.objects.get(id=geometry_id) geom = Geometry.objects.get(id=geometry_id)
geom.parcels.clear() objs = geom.parcelintersection_set.all()
for obj in objs:
obj.calculated_on = None
ParcelIntersection.objects.bulk_update(
objs,
["calculated_on"]
)
geom.update_parcels() geom.update_parcels()
except ObjectDoesNotExist: except ObjectDoesNotExist:
if recheck: if recheck:
@ -19,42 +25,42 @@ def celery_update_parcels(geometry_id: str, recheck: bool = True):
@shared_task @shared_task
def celery_send_mail_shared_access_removed(obj_identifier, user_id): def celery_send_mail_shared_access_removed(obj_identifier, obj_title=None, user_id=None):
from user.models import User from user.models import User
user = User.objects.get(id=user_id) user = User.objects.get(id=user_id)
user.send_mail_shared_access_removed(obj_identifier) user.send_mail_shared_access_removed(obj_identifier, obj_title)
@shared_task @shared_task
def celery_send_mail_shared_access_given(obj_identifier, user_id): def celery_send_mail_shared_access_given(obj_identifier, obj_title=None, user_id=None):
from user.models import User from user.models import User
user = User.objects.get(id=user_id) user = User.objects.get(id=user_id)
user.send_mail_shared_access_given(obj_identifier) user.send_mail_shared_access_given(obj_identifier, obj_title)
@shared_task @shared_task
def celery_send_mail_shared_data_recorded(obj_identifier, user_id): def celery_send_mail_shared_data_recorded(obj_identifier, obj_title=None, user_id=None):
from user.models import User from user.models import User
user = User.objects.get(id=user_id) user = User.objects.get(id=user_id)
user.send_mail_shared_data_recorded(obj_identifier) user.send_mail_shared_data_recorded(obj_identifier, obj_title)
@shared_task @shared_task
def celery_send_mail_shared_data_unrecorded(obj_identifier, user_id): def celery_send_mail_shared_data_unrecorded(obj_identifier, obj_title=None, user_id=None):
from user.models import User from user.models import User
user = User.objects.get(id=user_id) user = User.objects.get(id=user_id)
user.send_mail_shared_data_unrecorded(obj_identifier) user.send_mail_shared_data_unrecorded(obj_identifier, obj_title)
@shared_task @shared_task
def celery_send_mail_shared_data_deleted(obj_identifier, user_id): def celery_send_mail_shared_data_deleted(obj_identifier, obj_title=None, user_id=None):
from user.models import User from user.models import User
user = User.objects.get(id=user_id) user = User.objects.get(id=user_id)
user.send_mail_shared_data_deleted(obj_identifier) user.send_mail_shared_data_deleted(obj_identifier, obj_title)
@shared_task @shared_task
def celery_send_mail_shared_data_checked(obj_identifier, user_id): def celery_send_mail_shared_data_checked(obj_identifier, obj_title=None, user_id=None):
from user.models import User from user.models import User
user = User.objects.get(id=user_id) user = User.objects.get(id=user_id)
user.send_mail_shared_data_checked(obj_identifier) user.send_mail_shared_data_checked(obj_identifier, obj_title)

View File

@ -7,6 +7,7 @@ Created on: 26.10.21
""" """
import datetime import datetime
from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID
from ema.models import Ema from ema.models import Ema
from user.models import User from user.models import User
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
@ -15,7 +16,7 @@ from django.core.exceptions import ObjectDoesNotExist
from django.test import TestCase, Client from django.test import TestCase, Client
from django.urls import reverse from django.urls import reverse
from codelist.models import KonovaCode from codelist.models import KonovaCode, KonovaCodeList
from compensation.models import Compensation, CompensationState, CompensationAction, EcoAccount, EcoAccountDeduction from compensation.models import Compensation, CompensationState, CompensationAction, EcoAccount, EcoAccountDeduction
from intervention.models import Legal, Responsibility, 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
@ -46,43 +47,58 @@ class BaseTestCase(TestCase):
class Meta: class Meta:
abstract = True abstract = True
@classmethod def setUp(self) -> None:
def setUpTestData(cls): """ Setup data before each test run
cls.create_users()
cls.create_groups()
cls.intervention = cls.create_dummy_intervention()
cls.compensation = cls.create_dummy_compensation()
cls.eco_account = cls.create_dummy_eco_account()
cls.ema = cls.create_dummy_ema()
cls.deduction = cls.create_dummy_deduction()
cls.create_dummy_states()
cls.create_dummy_action()
cls.codes = cls.create_dummy_codes()
@classmethod Returns:
def create_users(cls):
"""
super().setUp()
self.create_users()
self.create_groups()
self.intervention = self.create_dummy_intervention()
self.compensation = self.create_dummy_compensation()
self.eco_account = self.create_dummy_eco_account()
self.ema = self.create_dummy_ema()
self.deduction = self.create_dummy_deduction()
self.create_dummy_states()
self.create_dummy_action()
self.codes = self.create_dummy_codes()
# Set the default group as only group for the user
default_group = self.groups.get(name=DEFAULT_GROUP)
self.superuser.groups.set([default_group])
# Create fresh logged in client and a non-logged in client (anon) for each test
self.client_user = Client()
self.client_user.login(username=self.superuser.username, password=self.superuser_pw)
self.client_anon = Client()
def create_users(self):
# Create superuser and regular user # Create superuser and regular user
cls.superuser = User.objects.create_superuser( self.superuser = User.objects.create_superuser(
username="root", username="root",
email="root@root.com", email="root@root.com",
password=cls.superuser_pw, password=self.superuser_pw,
) )
cls.user = User.objects.create_user( self.user = User.objects.create_user(
username="user1", username="user1",
email="user@root.com", email="user@root.com",
password=cls.user_pw password=self.user_pw
) )
cls.users = User.objects.all() self.users = User.objects.all()
@classmethod
def create_groups(cls): def create_groups(self):
# Create groups # Create groups
for group_data in GROUPS_DATA: for group_data in GROUPS_DATA:
name = group_data.get("name") name = group_data.get("name")
Group.objects.get_or_create( Group.objects.get_or_create(
name=name, name=name,
) )
cls.groups = Group.objects.all() self.groups = Group.objects.all()
@staticmethod @staticmethod
def create_dummy_string(prefix: str = ""): def create_dummy_string(prefix: str = ""):
@ -93,8 +109,7 @@ class BaseTestCase(TestCase):
""" """
return f"{prefix}{generate_random_string(3, True)}" return f"{prefix}{generate_random_string(3, True)}"
@classmethod def create_dummy_intervention(self):
def create_dummy_intervention(cls):
""" Creates an intervention which can be used for tests """ Creates an intervention which can be used for tests
Returns: Returns:
@ -102,7 +117,7 @@ class BaseTestCase(TestCase):
""" """
# Create dummy data # Create dummy data
# Create log entry # Create log entry
action = UserActionLogEntry.get_created_action(cls.superuser) action = UserActionLogEntry.get_created_action(self.superuser)
# Create legal data object (without M2M laws first) # Create legal data object (without M2M laws first)
legal_data = Legal.objects.create() legal_data = Legal.objects.create()
# Create responsible data object # Create responsible data object
@ -121,32 +136,30 @@ class BaseTestCase(TestCase):
intervention.generate_access_token(make_unique=True) intervention.generate_access_token(make_unique=True)
return intervention return intervention
@classmethod def create_dummy_compensation(self):
def create_dummy_compensation(cls):
""" Creates a compensation which can be used for tests """ Creates a compensation which can be used for tests
Returns: Returns:
""" """
if cls.intervention is None: if self.intervention is None:
cls.intervention = cls.create_dummy_intervention() self.intervention = self.create_dummy_intervention()
# Create dummy data # Create dummy data
# Create log entry # Create log entry
action = UserActionLogEntry.get_created_action(cls.superuser) action = UserActionLogEntry.get_created_action(self.superuser)
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(
identifier="TEST", identifier="TEST",
title="Test_title", title="Test_title",
intervention=cls.intervention, intervention=self.intervention,
created=action, created=action,
geometry=geometry, geometry=geometry,
comment="Test", comment="Test",
) )
return compensation return compensation
@classmethod def create_dummy_eco_account(self):
def create_dummy_eco_account(cls):
""" Creates an eco account which can be used for tests """ Creates an eco account which can be used for tests
Returns: Returns:
@ -154,7 +167,7 @@ class BaseTestCase(TestCase):
""" """
# Create dummy data # Create dummy data
# Create log entry # Create log entry
action = UserActionLogEntry.get_created_action(cls.superuser) action = UserActionLogEntry.get_created_action(self.superuser)
geometry = Geometry.objects.create() geometry = Geometry.objects.create()
# Create responsible data object # Create responsible data object
lega_data = Legal.objects.create() lega_data = Legal.objects.create()
@ -171,8 +184,7 @@ class BaseTestCase(TestCase):
) )
return eco_account return eco_account
@classmethod def create_dummy_ema(self):
def create_dummy_ema(cls):
""" Creates an ema which can be used for tests """ Creates an ema which can be used for tests
Returns: Returns:
@ -180,7 +192,7 @@ class BaseTestCase(TestCase):
""" """
# Create dummy data # Create dummy data
# Create log entry # Create log entry
action = UserActionLogEntry.get_created_action(cls.superuser) action = UserActionLogEntry.get_created_action(self.superuser)
geometry = Geometry.objects.create() geometry = Geometry.objects.create()
# Create responsible data object # Create responsible data object
responsible_data = Responsibility.objects.create() responsible_data = Responsibility.objects.create()
@ -195,51 +207,47 @@ class BaseTestCase(TestCase):
) )
return ema return ema
@classmethod def create_dummy_deduction(self):
def create_dummy_deduction(cls):
return EcoAccountDeduction.objects.create( return EcoAccountDeduction.objects.create(
account=cls.create_dummy_eco_account(), account=self.create_dummy_eco_account(),
intervention=cls.create_dummy_intervention(), intervention=self.create_dummy_intervention(),
surface=100, surface=100,
) )
@classmethod def create_dummy_states(self):
def create_dummy_states(cls):
""" Creates an intervention which can be used for tests """ Creates an intervention which can be used for tests
Returns: Returns:
""" """
cls.comp_state = CompensationState.objects.create( self.comp_state = CompensationState.objects.create(
surface=10.00, surface=10.00,
biotope_type=None, biotope_type=None,
) )
return cls.comp_state return self.comp_state
@classmethod def create_dummy_action(self):
def create_dummy_action(cls):
""" Creates an intervention which can be used for tests """ Creates an intervention which can be used for tests
Returns: Returns:
""" """
cls.comp_action = CompensationAction.objects.create( self.comp_action = CompensationAction.objects.create(
amount=10 amount=10
) )
return cls.comp_action return self.comp_action
@classmethod def create_dummy_codes(self):
def create_dummy_codes(cls):
""" Creates some dummy KonovaCodes which can be used for testing """ Creates some dummy KonovaCodes which can be used for testing
Returns: Returns:
""" """
codes = KonovaCode.objects.bulk_create([ codes = KonovaCode.objects.bulk_create([
KonovaCode(id=1, is_selectable=True, long_name="Test1"), KonovaCode(id=1, is_selectable=True, is_archived=False, is_leaf=True, long_name="Test1"),
KonovaCode(id=2, is_selectable=True, long_name="Test2"), KonovaCode(id=2, is_selectable=True, is_archived=False, is_leaf=True, long_name="Test2"),
KonovaCode(id=3, is_selectable=True, long_name="Test3"), KonovaCode(id=3, is_selectable=True, is_archived=False, is_leaf=True, long_name="Test3"),
KonovaCode(id=4, is_selectable=True, long_name="Test4"), KonovaCode(id=4, is_selectable=True, is_archived=False, is_leaf=True, long_name="Test4"),
]) ])
return codes return codes
@ -255,8 +263,7 @@ class BaseTestCase(TestCase):
polygon = polygon.transform(3857, clone=True) polygon = polygon.transform(3857, clone=True)
return MultiPolygon(polygon, srid=3857) # 3857 is the default srid used for MultiPolygonField in the form return MultiPolygon(polygon, srid=3857) # 3857 is the default srid used for MultiPolygonField in the form
@classmethod def fill_out_intervention(self, intervention: Intervention) -> Intervention:
def fill_out_intervention(cls, intervention: Intervention) -> Intervention:
""" Adds all required (dummy) data to an intervention """ Adds all required (dummy) data to an intervention
Args: Args:
@ -276,13 +283,12 @@ class BaseTestCase(TestCase):
intervention.legal.process_type = KonovaCode.objects.get(id=3) intervention.legal.process_type = KonovaCode.objects.get(id=3)
intervention.legal.save() intervention.legal.save()
intervention.legal.laws.set([KonovaCode.objects.get(id=(4))]) intervention.legal.laws.set([KonovaCode.objects.get(id=(4))])
intervention.geometry.geom = cls.create_dummy_geometry() intervention.geometry.geom = self.create_dummy_geometry()
intervention.geometry.save() intervention.geometry.save()
intervention.save() intervention.save()
return intervention return intervention
@classmethod def fill_out_compensation(self, compensation: Compensation) -> Compensation:
def fill_out_compensation(cls, compensation: Compensation) -> Compensation:
""" Adds all required (dummy) data to a compensation """ Adds all required (dummy) data to a compensation
Args: Args:
@ -291,13 +297,62 @@ class BaseTestCase(TestCase):
Returns: Returns:
compensation (Compensation): The modified compensation compensation (Compensation): The modified compensation
""" """
compensation.after_states.add(cls.comp_state) compensation.after_states.add(self.comp_state)
compensation.before_states.add(cls.comp_state) compensation.before_states.add(self.comp_state)
compensation.actions.add(cls.comp_action) compensation.actions.add(self.comp_action)
compensation.geometry.geom = cls.create_dummy_geometry() compensation.geometry.geom = self.create_dummy_geometry()
compensation.geometry.save() compensation.geometry.save()
return compensation return compensation
def get_conservation_office_code(self):
""" Returns a dummy KonovaCode as conservation office code
Returns:
"""
codelist = KonovaCodeList.objects.get_or_create(
id=CODELIST_CONSERVATION_OFFICE_ID
)[0]
code = KonovaCode.objects.get(id=2)
codelist.codes.add(code)
return code
def fill_out_ema(self, ema):
""" Adds all required (dummy) data to an Ema
Returns:
"""
ema.responsible.conservation_office = self.get_conservation_office_code()
ema.responsible.conservation_file_number = "test"
ema.responsible.handler = "handler"
ema.responsible.save()
ema.after_states.add(self.comp_state)
ema.before_states.add(self.comp_state)
ema.actions.add(self.comp_action)
ema.geometry.geom = self.create_dummy_geometry()
ema.geometry.save()
return ema
def fill_out_eco_account(self, eco_account):
""" Adds all required (dummy) data to an EcoAccount
Returns:
"""
eco_account.legal.registration_date = "2022-01-01"
eco_account.legal.save()
eco_account.responsible.conservation_office = self.get_conservation_office_code()
eco_account.responsible.conservation_file_number = "test"
eco_account.responsible.handler = "handler"
eco_account.responsible.save()
eco_account.after_states.add(self.comp_state)
eco_account.before_states.add(self.comp_state)
eco_account.actions.add(self.comp_action)
eco_account.geometry.geom = self.create_dummy_geometry()
eco_account.geometry.save()
eco_account.deductable_surface = eco_account.get_state_after_surface_sum()
eco_account.save()
return eco_account
def assert_equal_geometries(self, geom1: MultiPolygon, geom2: MultiPolygon): def assert_equal_geometries(self, geom1: MultiPolygon, geom2: MultiPolygon):
""" Assert for geometries to be equal """ Assert for geometries to be equal
@ -337,7 +392,10 @@ class BaseViewTestCase(BaseTestCase):
@classmethod @classmethod
def setUpTestData(cls) -> None: def setUpTestData(cls) -> None:
super().setUpTestData() super().setUpTestData()
cls.login_url = reverse("simple-sso-login")
def setUp(self) -> None:
super().setUp()
self.login_url = reverse("simple-sso-login")
def assert_url_success(self, client: Client, urls: list): def assert_url_success(self, client: Client, urls: list):
""" Assert for all given urls a direct 200 response """ Assert for all given urls a direct 200 response
@ -496,21 +554,6 @@ class BaseWorkflowTestCase(BaseTestCase):
def setUpTestData(cls): def setUpTestData(cls):
super().setUpTestData() super().setUpTestData()
def setUp(self) -> None:
""" Setup data before each test run
Returns:
"""
# Set the default group as only group for the user
default_group = self.groups.get(name=DEFAULT_GROUP)
self.superuser.groups.set([default_group])
# Create fresh logged in client and a non-logged in client (anon) for each test
self.client_user = Client()
self.client_user.login(username=self.superuser.username, password=self.superuser_pw)
self.client_anon = Client()
def assert_object_is_deleted(self, obj): def assert_object_is_deleted(self, obj):
""" Provides a quick check whether an object has been removed from the database or not """ Provides a quick check whether an object has been removed from the database or not

View File

@ -50,5 +50,5 @@ def remove_document(request: HttpRequest, doc: AbstractDocument):
form = RemoveModalForm(request.POST or None, instance=doc, request=request) form = RemoveModalForm(request.POST or None, instance=doc, request=request)
return form.process_request( return form.process_request(
request=request, request=request,
msg_success=DOCUMENT_REMOVED_TEMPLATE.format(title) msg_success=DOCUMENT_REMOVED_TEMPLATE.format(title),
) )

View File

@ -45,11 +45,12 @@ class Mailer:
auth_password=self.auth_password auth_password=self.auth_password
) )
def send_mail_shared_access_removed(self, obj_identifier, user): def send_mail_shared_access_removed(self, obj_identifier, obj_title, user):
""" Send a mail if user has no access to the object anymore """ Send a mail if user has no access to the object anymore
Args: Args:
obj_identifier (str): The object identifier obj_identifier (str): The object identifier
obj_title (str): The object title
Returns: Returns:
@ -57,6 +58,7 @@ class Mailer:
context = { context = {
"user": user, "user": user,
"obj_identifier": obj_identifier, "obj_identifier": obj_identifier,
"obj_title": obj_title,
"EMAIL_REPLY_TO": EMAIL_REPLY_TO, "EMAIL_REPLY_TO": EMAIL_REPLY_TO,
} }
msg = render_to_string("email/sharing/shared_access_removed.html", context) msg = render_to_string("email/sharing/shared_access_removed.html", context)
@ -67,7 +69,7 @@ class Mailer:
msg msg
) )
def send_mail_shared_access_given(self, obj_identifier, user): def send_mail_shared_access_given(self, obj_identifier, obj_title, user):
""" Send a mail if user just got access to the object """ Send a mail if user just got access to the object
Args: Args:
@ -79,6 +81,7 @@ class Mailer:
context = { context = {
"user": user, "user": user,
"obj_identifier": obj_identifier, "obj_identifier": obj_identifier,
"obj_title": obj_title,
"EMAIL_REPLY_TO": EMAIL_REPLY_TO, "EMAIL_REPLY_TO": EMAIL_REPLY_TO,
} }
msg = render_to_string("email/sharing/shared_access_given.html", context) msg = render_to_string("email/sharing/shared_access_given.html", context)
@ -89,7 +92,7 @@ class Mailer:
msg msg
) )
def send_mail_shared_data_recorded(self, obj_identifier, user): def send_mail_shared_data_recorded(self, obj_identifier, obj_title, user):
""" Send a mail if the user's shared data has just been unrecorded """ Send a mail if the user's shared data has just been unrecorded
Args: Args:
@ -101,6 +104,7 @@ class Mailer:
context = { context = {
"user": user, "user": user,
"obj_identifier": obj_identifier, "obj_identifier": obj_identifier,
"obj_title": obj_title,
"EMAIL_REPLY_TO": EMAIL_REPLY_TO, "EMAIL_REPLY_TO": EMAIL_REPLY_TO,
} }
msg = render_to_string("email/recording/shared_data_recorded.html", context) msg = render_to_string("email/recording/shared_data_recorded.html", context)
@ -111,7 +115,7 @@ class Mailer:
msg msg
) )
def send_mail_shared_data_unrecorded(self, obj_identifier, user): def send_mail_shared_data_unrecorded(self, obj_identifier, obj_title, user):
""" Send a mail if the user's shared data has just been unrecorded """ Send a mail if the user's shared data has just been unrecorded
Args: Args:
@ -123,6 +127,7 @@ class Mailer:
context = { context = {
"user": user, "user": user,
"obj_identifier": obj_identifier, "obj_identifier": obj_identifier,
"obj_title": obj_title,
"EMAIL_REPLY_TO": EMAIL_REPLY_TO, "EMAIL_REPLY_TO": EMAIL_REPLY_TO,
} }
msg = render_to_string("email/recording/shared_data_unrecorded.html", context) msg = render_to_string("email/recording/shared_data_unrecorded.html", context)
@ -133,7 +138,7 @@ class Mailer:
msg msg
) )
def send_mail_shared_data_deleted(self, obj_identifier, user): def send_mail_shared_data_deleted(self, obj_identifier, obj_title, user):
""" Send a mail if shared data has just been deleted """ Send a mail if shared data has just been deleted
Args: Args:
@ -145,6 +150,7 @@ class Mailer:
context = { context = {
"user": user, "user": user,
"obj_identifier": obj_identifier, "obj_identifier": obj_identifier,
"obj_title": obj_title,
"EMAIL_REPLY_TO": EMAIL_REPLY_TO, "EMAIL_REPLY_TO": EMAIL_REPLY_TO,
} }
msg = render_to_string("email/deleting/shared_data_deleted.html", context) msg = render_to_string("email/deleting/shared_data_deleted.html", context)
@ -155,7 +161,7 @@ class Mailer:
msg msg
) )
def send_mail_shared_data_checked(self, obj_identifier, user): def send_mail_shared_data_checked(self, obj_identifier, obj_title, user):
""" Send a mail if shared data just has been checked """ Send a mail if shared data just has been checked
Args: Args:
@ -167,6 +173,7 @@ class Mailer:
context = { context = {
"user": user, "user": user,
"obj_identifier": obj_identifier, "obj_identifier": obj_identifier,
"obj_title": obj_title,
"EMAIL_REPLY_TO": EMAIL_REPLY_TO, "EMAIL_REPLY_TO": EMAIL_REPLY_TO,
} }
msg = render_to_string("email/checking/shared_data_checked.html", context) msg = render_to_string("email/checking/shared_data_checked.html", context)

View File

@ -18,34 +18,61 @@ MISSING_GROUP_PERMISSION = _("You need to be part of another user group.")
CHECKED_RECORDED_RESET = _("Status of Checked and Recorded reseted") CHECKED_RECORDED_RESET = _("Status of Checked and Recorded reseted")
# FILES
FILE_TYPE_UNSUPPORTED = _("Unsupported file type")
FILE_SIZE_TOO_LARGE = _("File too large")
# ECO ACCOUNT # ECO ACCOUNT
CANCEL_ACC_RECORDED_OR_DEDUCTED = _("Action canceled. Eco account is recorded or deductions exist. Only conservation office member can perform this action.") CANCEL_ACC_RECORDED_OR_DEDUCTED = _("Action canceled. Eco account is recorded or deductions exist. Only conservation office member can perform this action.")
# COMPENSATION # COMPENSATION
COMPENSATION_ADDED_TEMPLATE = _("Compensation {} added") COMPENSATION_ADDED_TEMPLATE = _("Compensation {} added")
COMPENSATION_REMOVED_TEMPLATE = _("Compensation {} removed") COMPENSATION_REMOVED_TEMPLATE = _("Compensation {} removed")
COMPENSATION_EDITED_TEMPLATE = _("Compensation {} edited")
ADDED_COMPENSATION_ACTION = _("Added compensation action")
ADDED_COMPENSATION_STATE = _("Added compensation state")
# COMPENSATION STATE
COMPENSATION_STATE_REMOVED = _("State removed")
COMPENSATION_STATE_EDITED = _("State edited")
COMPENSATION_STATE_ADDED = _("State added")
# COMPENSATION ACTION
COMPENSATION_ACTION_ADDED = _("Action added")
COMPENSATION_ACTION_EDITED = _("Action edited")
COMPENSATION_ACTION_REMOVED = _("Action removed")
# DEDUCTIONS # DEDUCTIONS
DEDUCTION_ADDED = _("Deduction added") DEDUCTION_ADDED = _("Deduction added")
DEDUCTION_EDITED = _("Deduction edited")
DEDUCTION_REMOVED = _("Deduction removed") DEDUCTION_REMOVED = _("Deduction removed")
# DEADLINE
DEADLINE_ADDED = _("Deadline added")
DEADLINE_EDITED = _("Deadline edited")
DEADLINE_REMOVED = _("Deadline removed")
# PAYMENTS # PAYMENTS
PAYMENT_ADDED = _("Payment added") PAYMENT_ADDED = _("Payment added")
PAYMENT_EDITED = _("Payment edited")
PAYMENT_REMOVED = _("Payment removed") PAYMENT_REMOVED = _("Payment removed")
# REVOCATIONS # REVOCATIONS
REVOCATION_ADDED = _("Revocation added") REVOCATION_ADDED = _("Revocation added")
REVOCATION_EDITED = _("Revocation edited")
REVOCATION_REMOVED = _("Revocation removed") REVOCATION_REMOVED = _("Revocation removed")
# DOCUMENTS # DOCUMENTS
DOCUMENT_REMOVED_TEMPLATE = _("Document '{}' deleted") DOCUMENT_REMOVED_TEMPLATE = _("Document '{}' deleted")
DOCUMENT_ADDED = _("Document added") DOCUMENT_ADDED = _("Document added")
DOCUMENT_EDITED = _("Document edited")
# Edited # Edited
EDITED_GENERAL_DATA = _("Edited general data") EDITED_GENERAL_DATA = _("Edited general data")
ADDED_COMPENSATION_STATE = _("Added compensation state")
ADDED_DEADLINE = _("Added deadline") ADDED_DEADLINE = _("Added deadline")
ADDED_COMPENSATION_ACTION = _("Added compensation action")
# Geometry conflicts # Geometry conflicts
GEOMETRY_CONFLICT_WITH_TEMPLATE = _("Geometry conflict detected with {}") GEOMETRY_CONFLICT_WITH_TEMPLATE = _("Geometry conflict detected with {}")
# INTERVENTION
INTERVENTION_HAS_REVOCATIONS_TEMPLATE = _("This intervention has {} revocations")

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -11,6 +11,8 @@
<br> <br>
<strong>{{obj_identifier}}</strong> <strong>{{obj_identifier}}</strong>
<br> <br>
<strong>{{obj_title}}</strong>
<br>
{% trans 'This means, the responsible registration office just confirmed the correctness of this dataset.' %} {% trans 'This means, the responsible registration office just confirmed the correctness of this dataset.' %}
<br> <br>
<br> <br>

View File

@ -11,6 +11,8 @@
<br> <br>
<strong>{{obj_identifier}}</strong> <strong>{{obj_identifier}}</strong>
<br> <br>
<strong>"{{obj_title}}"</strong>
<br>
{% trans 'If this should not have been happened, please contact us. See the signature for details.' %} {% trans 'If this should not have been happened, please contact us. See the signature for details.' %}
<br> <br>
<br> <br>

View File

@ -11,6 +11,8 @@
<br> <br>
<strong>{{obj_identifier}}</strong> <strong>{{obj_identifier}}</strong>
<br> <br>
<strong>"{{obj_title}}"</strong>
<br>
{% trans 'This means the data is now publicly available, e.g. in LANIS' %} {% trans 'This means the data is now publicly available, e.g. in LANIS' %}
<br> <br>
<br> <br>

View File

@ -11,6 +11,8 @@
<br> <br>
<strong>{{obj_identifier}}</strong> <strong>{{obj_identifier}}</strong>
<br> <br>
<strong>"{{obj_title}}"</strong>
<br>
{% trans 'This means the data is no longer publicly available.' %} {% trans 'This means the data is no longer publicly available.' %}
<br> <br>
<br> <br>

View File

@ -11,6 +11,8 @@
<br> <br>
<strong>{{obj_identifier}}</strong> <strong>{{obj_identifier}}</strong>
<br> <br>
<strong>"{{obj_title}}"</strong>
<br>
{% trans 'This means you can now edit this dataset.' %} {% trans 'This means you can now edit this dataset.' %}
{% trans 'The shared dataset appears now by default on your overview for this dataset type.' %} {% trans 'The shared dataset appears now by default on your overview for this dataset type.' %}
<br> <br>

View File

@ -11,6 +11,8 @@
<br> <br>
<strong>{{obj_identifier}}</strong> <strong>{{obj_identifier}}</strong>
<br> <br>
<strong>"{{obj_title}}"</strong>
<br>
{% trans 'However, you are still able to view the dataset content.' %} {% trans 'However, you are still able to view the dataset content.' %}
{% trans 'Please use the provided search filter on the dataset`s overview pages to find them.' %} {% trans 'Please use the provided search filter on the dataset`s overview pages to find them.' %}
<br> <br>

View File

@ -0,0 +1,9 @@
{% load i18n fontawesome_5 %}
{% for entry in entries %}
<span class="badge pill-badge rlp-r">{{entry}}</span>
{% empty %}
<span class="text-info" title="{% trans 'If the geometry is not empty, the parcels are currently recalculated. Please refresh this page in a few moments.' %}">
{% fa5_icon 'hourglass-half' %}
</span>
{% endfor %}

View File

@ -0,0 +1,14 @@
{% load i18n fontawesome_5 %}
{% if has_revocations %}
<strong>
<a href="{{url}}" title="{% trans 'Revocations exists' %}">
{% fa5_icon 'ban' %}
{{content}}
</a>
</strong>
{% else %}
<a href="{{url}}" title="{{tooltip}}">
{{content}}
</a>
{% endif %}

View File

@ -60,11 +60,12 @@ class User(AbstractUser):
name=ETS_GROUP name=ETS_GROUP
).exists() ).exists()
def send_mail_shared_access_removed(self, obj_identifier): def send_mail_shared_access_removed(self, obj_identifier, obj_title):
""" Sends a mail to the user in case of removed shared access """ Sends a mail to the user in case of removed shared access
Args: Args:
obj_identifier (): obj_identifier ():
obj_title ():
Returns: Returns:
@ -72,9 +73,9 @@ class User(AbstractUser):
notification_set = self.is_notification_setting_set(UserNotificationEnum.NOTIFY_ON_SHARED_ACCESS_REMOVED) notification_set = self.is_notification_setting_set(UserNotificationEnum.NOTIFY_ON_SHARED_ACCESS_REMOVED)
if notification_set: if notification_set:
mailer = Mailer() mailer = Mailer()
mailer.send_mail_shared_access_removed(obj_identifier, self) mailer.send_mail_shared_access_removed(obj_identifier, obj_title, self)
def send_mail_shared_access_given(self, obj_identifier): def send_mail_shared_access_given(self, obj_identifier, obj_title):
""" Sends a mail to the user in case of given shared access """ Sends a mail to the user in case of given shared access
Args: Args:
@ -86,9 +87,9 @@ class User(AbstractUser):
notification_set = self.is_notification_setting_set(UserNotificationEnum.NOTIFY_ON_SHARED_ACCESS_GAINED) notification_set = self.is_notification_setting_set(UserNotificationEnum.NOTIFY_ON_SHARED_ACCESS_GAINED)
if notification_set: if notification_set:
mailer = Mailer() mailer = Mailer()
mailer.send_mail_shared_access_given(obj_identifier, self) mailer.send_mail_shared_access_given(obj_identifier, obj_title, self)
def send_mail_shared_data_recorded(self, obj_identifier): def send_mail_shared_data_recorded(self, obj_identifier, obj_title):
""" Sends a mail to the user in case of shared data has been recorded """ Sends a mail to the user in case of shared data has been recorded
Args: Args:
@ -100,9 +101,9 @@ class User(AbstractUser):
notification_set = self.is_notification_setting_set(UserNotificationEnum.NOTIFY_ON_SHARED_DATA_RECORDED) notification_set = self.is_notification_setting_set(UserNotificationEnum.NOTIFY_ON_SHARED_DATA_RECORDED)
if notification_set: if notification_set:
mailer = Mailer() mailer = Mailer()
mailer.send_mail_shared_data_recorded(obj_identifier, self) mailer.send_mail_shared_data_recorded(obj_identifier, obj_title, self)
def send_mail_shared_data_unrecorded(self, obj_identifier): def send_mail_shared_data_unrecorded(self, obj_identifier, obj_title):
""" Sends a mail to the user in case of shared data has been unrecorded """ Sends a mail to the user in case of shared data has been unrecorded
Args: Args:
@ -114,9 +115,9 @@ class User(AbstractUser):
notification_set = self.is_notification_setting_set(UserNotificationEnum.NOTIFY_ON_SHARED_DATA_RECORDED) notification_set = self.is_notification_setting_set(UserNotificationEnum.NOTIFY_ON_SHARED_DATA_RECORDED)
if notification_set: if notification_set:
mailer = Mailer() mailer = Mailer()
mailer.send_mail_shared_data_unrecorded(obj_identifier, self) mailer.send_mail_shared_data_unrecorded(obj_identifier, obj_title, self)
def send_mail_shared_data_deleted(self, obj_identifier): def send_mail_shared_data_deleted(self, obj_identifier, obj_title):
""" Sends a mail to the user in case of shared data has been deleted """ Sends a mail to the user in case of shared data has been deleted
Args: Args:
@ -128,9 +129,9 @@ class User(AbstractUser):
notification_set = self.is_notification_setting_set(UserNotificationEnum.NOTIFY_ON_SHARED_DATA_DELETED) notification_set = self.is_notification_setting_set(UserNotificationEnum.NOTIFY_ON_SHARED_DATA_DELETED)
if notification_set: if notification_set:
mailer = Mailer() mailer = Mailer()
mailer.send_mail_shared_data_deleted(obj_identifier, self) mailer.send_mail_shared_data_deleted(obj_identifier, obj_title, self)
def send_mail_shared_data_checked(self, obj_identifier): def send_mail_shared_data_checked(self, obj_identifier, obj_title):
""" Sends a mail to the user in case of shared data has been deleted """ Sends a mail to the user in case of shared data has been deleted
Args: Args:
@ -142,7 +143,7 @@ class User(AbstractUser):
notification_set = self.is_notification_setting_set(UserNotificationEnum.NOTIFY_ON_SHARED_DATA_CHECKED) notification_set = self.is_notification_setting_set(UserNotificationEnum.NOTIFY_ON_SHARED_DATA_CHECKED)
if notification_set: if notification_set:
mailer = Mailer() mailer = Mailer()
mailer.send_mail_shared_data_checked(obj_identifier, self) mailer.send_mail_shared_data_checked(obj_identifier, obj_title, self)
def get_API_token(self): def get_API_token(self):
""" Getter for an API token """ Getter for an API token