diff --git a/api/tests/v1/share/test_api_sharing.py b/api/tests/v1/share/test_api_sharing.py index 9e7c9eec..1da0ce1b 100644 --- a/api/tests/v1/share/test_api_sharing.py +++ b/api/tests/v1/share/test_api_sharing.py @@ -12,15 +12,17 @@ class BaseAPIV1TestCase(BaseTestCase): def setUpTestData(cls): super().setUpTestData() - cls.superuser.get_API_token() - cls.superuser.api_token.is_active = True - cls.superuser.api_token.save() - default_group = cls.groups.get(name=DEFAULT_GROUP) - cls.superuser.groups.add(default_group) + def setUp(self) -> None: + super().setUp() + self.superuser.get_API_token() + self.superuser.api_token.is_active = True + self.superuser.api_token.save() + default_group = self.groups.get(name=DEFAULT_GROUP) + self.superuser.groups.add(default_group) - cls.header_data = { - "HTTP_ksptoken": cls.superuser.api_token.token, - "HTTP_kspuser": cls.superuser.username, + self.header_data = { + "HTTP_ksptoken": self.superuser.api_token.token, + "HTTP_kspuser": self.superuser.username, } diff --git a/compensation/forms/modalForms.py b/compensation/forms/modalForms.py index 9331f60a..804ebad4 100644 --- a/compensation/forms/modalForms.py +++ b/compensation/forms/modalForms.py @@ -18,10 +18,10 @@ from codelist.settings import CODELIST_BIOTOPES_ID, CODELIST_COMPENSATION_ACTION CODELIST_COMPENSATION_ACTION_DETAIL_ID from compensation.models import CompensationDocument, EcoAccountDocument 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.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): @@ -100,10 +100,52 @@ class NewPaymentForm(BaseModalForm): def save(self): pay = self.instance.add_payment(self) - self.instance.mark_as_edited(self.user, self.request, edit_comment=PAYMENT_ADDED) 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): """ Form handling state related input @@ -219,6 +261,63 @@ class NewStateModalForm(BaseModalForm): 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): """ Form handling deadline related input @@ -271,7 +370,29 @@ class NewDeadlineModalForm(BaseModalForm): def save(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 @@ -368,9 +489,36 @@ class NewActionModalForm(BaseModalForm): 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 -class NewEcoAccountDocumentForm(NewDocumentForm): +class NewEcoAccountDocumentModalForm(NewDocumentModalForm): document_model = EcoAccountDocument \ No newline at end of file diff --git a/compensation/models/action.py b/compensation/models/action.py index 087f48be..a5579159 100644 --- a/compensation/models/action.py +++ b/compensation/models/action.py @@ -12,6 +12,7 @@ from codelist.models import KonovaCode from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_COMPENSATION_ACTION_DETAIL_ID from compensation.managers import CompensationActionManager from konova.models import BaseResource +from konova.utils.message_templates import COMPENSATION_ACTION_REMOVED class UnitChoices(models.TextChoices): diff --git a/compensation/models/compensation.py b/compensation/models/compensation.py index f50553f0..4dd2b4c2 100644 --- a/compensation/models/compensation.py +++ b/compensation/models/compensation.py @@ -21,7 +21,8 @@ from konova.models import BaseObject, AbstractDocument, Deadline, generate_docum GeoReferencedMixin from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_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 @@ -61,7 +62,6 @@ class AbstractCompensation(BaseObject, GeoReferencedMixin): user = form.user with transaction.atomic(): created_action = UserActionLogEntry.get_created_action(user) - edited_action = UserActionLogEntry.get_edited_action(user, _("Added deadline")) deadline = Deadline.objects.create( type=form_data["type"], @@ -70,12 +70,26 @@ class AbstractCompensation(BaseObject, GeoReferencedMixin): created=created_action, ) - self.modified = edited_action self.save() - self.log.add(edited_action) self.deadlines.add(deadline) + self.mark_as_edited(user, edit_comment=ADDED_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: """ Adds a new action to the compensation @@ -101,6 +115,21 @@ class AbstractCompensation(BaseObject, GeoReferencedMixin): self.actions.add(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: """ Adds a new compensation state to the compensation @@ -125,6 +154,21 @@ class AbstractCompensation(BaseObject, GeoReferencedMixin): self.after_states.add(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: """ Calculates the compensation's/account's surface @@ -287,28 +331,6 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin): """ 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: """ Getter for all documents of a compensation @@ -332,7 +354,9 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin): 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: """ Not inherited by RecordableObjectMixin @@ -344,6 +368,26 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin): """ 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): """ diff --git a/compensation/models/eco_account.py b/compensation/models/eco_account.py index af660615..6d95b399 100644 --- a/compensation/models/eco_account.py +++ b/compensation/models/eco_account.py @@ -123,28 +123,6 @@ class EcoAccount(AbstractCompensation, ShareableObjectMixin, RecordableObjectMix return ret_val_total, ret_val_relative - def get_LANIS_link(self) -> str: - """ Generates a link for LANIS depending on the geometry - - Returns: - - """ - try: - geom = self.geometry.geom.transform(DEFAULT_SRID_RLP, clone=True) - x = geom.centroid.x - y = geom.centroid.y - zoom_lvl = 16 - except AttributeError: - # If no geometry has been added, yet. - x = 1 - y = 1 - zoom_lvl = 6 - return LANIS_LINK_TEMPLATE.format( - zoom_lvl, - x, - y, - ) - def quality_check(self) -> EcoAccountQualityChecker: """ Quality check @@ -273,5 +251,5 @@ class EcoAccountDeduction(BaseResource): def delete(self, user=None, *args, **kwargs): if user is not None: 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) \ No newline at end of file diff --git a/compensation/models/payment.py b/compensation/models/payment.py index 48eec3fc..6f3f4c24 100644 --- a/compensation/models/payment.py +++ b/compensation/models/payment.py @@ -37,8 +37,3 @@ class Payment(BaseResource): ordering = [ "-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) diff --git a/compensation/models/state.py b/compensation/models/state.py index ce0fc699..5cb8376a 100644 --- a/compensation/models/state.py +++ b/compensation/models/state.py @@ -6,11 +6,13 @@ Created on: 16.11.21 """ from django.db import models +from django.db.models import Q from codelist.models import KonovaCode from codelist.settings import CODELIST_BIOTOPES_ID, CODELIST_BIOTOPES_EXTRA_CODES_ID from compensation.managers import CompensationStateManager from konova.models import UuidModel +from konova.utils.message_templates import COMPENSATION_STATE_REMOVED class CompensationState(UuidModel): diff --git a/compensation/tables.py b/compensation/tables.py index b78b5aa9..96888cc1 100644 --- a/compensation/tables.py +++ b/compensation/tables.py @@ -31,6 +31,11 @@ class CompensationTable(BaseTable, TableRenderMixin): orderable=True, accessor="title", ) + d = tables.Column( + verbose_name=_("Parcel gmrkng"), + orderable=True, + accessor="geometry", + ) c = tables.Column( verbose_name=_("Checked"), orderable=True, @@ -80,14 +85,17 @@ class CompensationTable(BaseTable, TableRenderMixin): Returns: """ - html = "" - html += self.render_link( - tooltip=_("Open {}").format(_("Compensation")), - href=reverse("compensation:detail", args=(record.id,)), - txt=value, - new_tab=False, + context = { + "tooltip": _("Open {}").format(_("Intervention")), + "content": value, + "url": reverse("compensation:detail", args=(record.id,)), + "has_revocations": record.intervention.legal.revocations.exists() + } + html = render_to_string( + "table/revocation_warning_col.html", + context ) - return format_html(html) + return html def render_c(self, value, record: Compensation): """ Renders the checked column for a compensation @@ -115,6 +123,28 @@ class CompensationTable(BaseTable, TableRenderMixin): ) 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): """ Renders the registered column for a compensation @@ -173,10 +203,20 @@ class EcoAccountTable(BaseTable, TableRenderMixin): orderable=True, accessor="title", ) + d = tables.Column( + verbose_name=_("Parcel gmrkng"), + orderable=True, + accessor="geometry", + ) av = tables.Column( verbose_name=_("Available"), orderable=True, empty_values=[], + attrs={ + "th": { + "class": "w-20", + } + } ) r = tables.Column( verbose_name=_("Recorded"), @@ -244,6 +284,28 @@ class EcoAccountTable(BaseTable, TableRenderMixin): html = render_to_string("konova/widgets/progressbar.html", {"value": value_relative}) 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): """ Renders the recorded column for an eco account diff --git a/compensation/templates/compensation/detail/compensation/includes/actions.html b/compensation/templates/compensation/detail/compensation/includes/actions.html index d82d8c08..33037ecf 100644 --- a/compensation/templates/compensation/detail/compensation/includes/actions.html +++ b/compensation/templates/compensation/detail/compensation/includes/actions.html @@ -35,7 +35,7 @@ {% trans 'Comment' %} {% if is_default_member and has_access %} - + {% trans 'Action' %} @@ -61,9 +61,12 @@ {{ action.comment }} - + {% if is_default_member and has_access %} - + {% endif %} diff --git a/compensation/templates/compensation/detail/compensation/includes/deadlines.html b/compensation/templates/compensation/detail/compensation/includes/deadlines.html index da4e21d6..7f445657 100644 --- a/compensation/templates/compensation/detail/compensation/includes/deadlines.html +++ b/compensation/templates/compensation/detail/compensation/includes/deadlines.html @@ -34,7 +34,7 @@ {% trans 'Comment' %} {% if is_default_member and has_access %} - + {% trans 'Action' %} @@ -54,9 +54,12 @@ {{ deadline.comment }} - + {% if is_default_member and has_access %} - + {% endif %} diff --git a/compensation/templates/compensation/detail/compensation/includes/documents.html b/compensation/templates/compensation/detail/compensation/includes/documents.html index 0615d4be..573fd865 100644 --- a/compensation/templates/compensation/detail/compensation/includes/documents.html +++ b/compensation/templates/compensation/detail/compensation/includes/documents.html @@ -27,11 +27,14 @@ {% trans 'Title' %} + + {% trans 'Created on' %} + {% trans 'Comment' %} {% if is_default_member and has_access %} - + {% trans 'Action' %} @@ -43,18 +46,24 @@ {% for doc in obj.documents.all %} - + {{ doc.title }} + + {{ doc.date_of_creation }} +
{{ doc.comment }}
- + {% if is_default_member and has_access %} - + {% endif %} diff --git a/compensation/templates/compensation/detail/compensation/includes/states-after.html b/compensation/templates/compensation/detail/compensation/includes/states-after.html index b3e86a26..2c95ca1a 100644 --- a/compensation/templates/compensation/detail/compensation/includes/states-after.html +++ b/compensation/templates/compensation/detail/compensation/includes/states-after.html @@ -36,7 +36,7 @@ {% trans 'Surface' %} {% if is_default_member and has_access %} - + {% trans 'Action' %} @@ -57,9 +57,12 @@ {% endif %} {{ state.surface|floatformat:2 }} m² - + {% if is_default_member and has_access %} - + {% endif %} diff --git a/compensation/templates/compensation/detail/compensation/includes/states-before.html b/compensation/templates/compensation/detail/compensation/includes/states-before.html index c3c7e0f1..d2ba3697 100644 --- a/compensation/templates/compensation/detail/compensation/includes/states-before.html +++ b/compensation/templates/compensation/detail/compensation/includes/states-before.html @@ -36,7 +36,7 @@ {% trans 'Surface' %} {% if is_default_member and has_access %} - + {% trans 'Action' %} @@ -57,9 +57,12 @@ {% endif %} {{ state.surface|floatformat:2 }} m² - + {% if is_default_member and has_access %} - + {% endif %} diff --git a/compensation/templates/compensation/detail/eco_account/includes/actions.html b/compensation/templates/compensation/detail/eco_account/includes/actions.html index d1275c78..add698e0 100644 --- a/compensation/templates/compensation/detail/eco_account/includes/actions.html +++ b/compensation/templates/compensation/detail/eco_account/includes/actions.html @@ -34,7 +34,7 @@ {% trans 'Comment' %} {% if is_default_member and has_access %} - + {% trans 'Action' %} @@ -60,9 +60,12 @@ {{ action.comment }} - + {% if is_default_member and has_access %} - + {% endif %} diff --git a/compensation/templates/compensation/detail/eco_account/includes/deadlines.html b/compensation/templates/compensation/detail/eco_account/includes/deadlines.html index ff8a5291..beaecfda 100644 --- a/compensation/templates/compensation/detail/eco_account/includes/deadlines.html +++ b/compensation/templates/compensation/detail/eco_account/includes/deadlines.html @@ -33,7 +33,7 @@ {% trans 'Comment' %} - + {% trans 'Action' %} @@ -52,9 +52,12 @@ {{ deadline.comment }} - + {% if is_default_member and has_access %} - + {% endif %} diff --git a/compensation/templates/compensation/detail/eco_account/includes/deductions.html b/compensation/templates/compensation/detail/eco_account/includes/deductions.html index e72ab2ab..10f177ea 100644 --- a/compensation/templates/compensation/detail/eco_account/includes/deductions.html +++ b/compensation/templates/compensation/detail/eco_account/includes/deductions.html @@ -36,7 +36,7 @@ {% trans 'Created' %} - + {% trans 'Action' %} @@ -60,9 +60,12 @@ {{ deduction.surface|floatformat:2|intcomma }} m² {{ deduction.created.timestamp|default_if_none:""|naturalday}} - + {% if is_default_member and has_access %} - + {% endif %} diff --git a/compensation/templates/compensation/detail/eco_account/includes/documents.html b/compensation/templates/compensation/detail/eco_account/includes/documents.html index b3895eac..67881c90 100644 --- a/compensation/templates/compensation/detail/eco_account/includes/documents.html +++ b/compensation/templates/compensation/detail/eco_account/includes/documents.html @@ -28,9 +28,12 @@ {% trans 'Title' %} - {% trans 'Comment' %} + {% trans 'Created on' %} + {% trans 'Comment' %} + + {% trans 'Action' %} @@ -41,18 +44,24 @@ {% for doc in obj.documents.all %} - + {{ doc.title }} + + {{ doc.date_of_creation }} +
{{ doc.comment }}
- + {% if is_default_member and has_access %} - + {% endif %} diff --git a/compensation/templates/compensation/detail/eco_account/includes/states-after.html b/compensation/templates/compensation/detail/eco_account/includes/states-after.html index ea696b31..bead6f2b 100644 --- a/compensation/templates/compensation/detail/eco_account/includes/states-after.html +++ b/compensation/templates/compensation/detail/eco_account/includes/states-after.html @@ -36,7 +36,7 @@ {% trans 'Surface' %} {% if is_default_member and has_access %} - + {% trans 'Action' %} @@ -57,9 +57,12 @@ {% endif %} {{ state.surface|floatformat:2 }} m² - + {% if is_default_member and has_access %} - + {% endif %} diff --git a/compensation/templates/compensation/detail/eco_account/includes/states-before.html b/compensation/templates/compensation/detail/eco_account/includes/states-before.html index af3042ea..c19b4049 100644 --- a/compensation/templates/compensation/detail/eco_account/includes/states-before.html +++ b/compensation/templates/compensation/detail/eco_account/includes/states-before.html @@ -36,7 +36,7 @@ {% trans 'Surface' %} {% if is_default_member and has_access %} - + {% trans 'Action' %} @@ -57,9 +57,12 @@ {% endif %} {{ state.surface|floatformat:2 }} m² - + {% if is_default_member and has_access %} - + {% endif %} diff --git a/compensation/tests/compensation/__init__.py b/compensation/tests/compensation/__init__.py new file mode 100644 index 00000000..7cf7973a --- /dev/null +++ b/compensation/tests/compensation/__init__.py @@ -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 + +""" diff --git a/compensation/tests/compensation/test_views.py b/compensation/tests/compensation/test_views.py new file mode 100644 index 00000000..27218f2a --- /dev/null +++ b/compensation/tests/compensation/test_views.py @@ -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) + diff --git a/compensation/tests/test_workflow.py b/compensation/tests/compensation/test_workflow.py similarity index 78% rename from compensation/tests/test_workflow.py rename to compensation/tests/compensation/test_workflow.py index 7f4864d6..7b73be89 100644 --- a/compensation/tests/test_workflow.py +++ b/compensation/tests/compensation/test_workflow.py @@ -2,7 +2,7 @@ Author: Michel Peltriaux Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany Contact: michel.peltriaux@sgdnord.rlp.de -Created on: 11.11.21 +Created on: 07.02.22 """ import datetime @@ -11,7 +11,7 @@ from django.contrib.gis.geos import MultiPolygon from django.urls import reverse 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 user.models import UserAction @@ -21,17 +21,18 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase): def setUpTestData(cls): 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: 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 Compensation.objects.all().delete() @@ -55,6 +56,7 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase): "geom": test_geom.geojson, "intervention": self.intervention.id, } + pre_creation_intervention_log_count = self.intervention.log.count() # Preserve the current number of intervention's compensations num_compensations = self.intervention.compensations.count() @@ -66,6 +68,13 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase): self.assertEqual(new_compensation.identifier, test_id) self.assertEqual(new_compensation.title, test_title) 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): """ Test the creation of a compensation from a given intervention @@ -83,6 +92,7 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase): "title": test_title, "geom": test_geom.geojson, } + pre_creation_intervention_log_count = self.intervention.log.count() # Preserve the current number of intervention's compensations num_compensations = self.intervention.compensations.count() @@ -95,6 +105,12 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase): self.assertEqual(new_compensation.title, test_title) 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): """ Checks that the editing of a compensation works @@ -103,6 +119,7 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase): """ url = reverse("compensation:edit", args=(self.compensation.id,)) self.compensation = self.fill_out_compensation(self.compensation) + pre_edit_log_count = self.compensation.log.count() new_title = 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) + # 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): """ 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 self.superuser.groups.add(self.groups.get(name=ZB_GROUP)) + pre_check_log_count = self.compensation.log.count() + # Prepare url and form data url = reverse("intervention:check", args=(self.intervention.id,)) post_data = { @@ -186,6 +209,7 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase): # Expect the user action to be in the log self.assertIn(checked, self.compensation.log.all()) + self.assertEqual(pre_check_log_count + 1, self.compensation.log.count()) def test_recordability(self): """ @@ -200,6 +224,7 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase): """ # Add proper privilege for the user self.superuser.groups.add(self.groups.get(name=ETS_GROUP)) + pre_record_log_count = self.compensation.log.count() # Prepare url and form data 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 self.assertIn(recorded, self.compensation.log.all()) - - -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) - + self.assertEqual(pre_record_log_count + 1, self.compensation.log.count()) diff --git a/compensation/tests/ecoaccount/__init__.py b/compensation/tests/ecoaccount/__init__.py new file mode 100644 index 00000000..7cf7973a --- /dev/null +++ b/compensation/tests/ecoaccount/__init__.py @@ -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 + +""" diff --git a/compensation/tests/ecoaccount/test_views.py b/compensation/tests/ecoaccount/test_views.py new file mode 100644 index 00000000..617f7437 --- /dev/null +++ b/compensation/tests/ecoaccount/test_views.py @@ -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) + diff --git a/compensation/tests/ecoaccount/test_workflow.py b/compensation/tests/ecoaccount/test_workflow.py new file mode 100644 index 00000000..1cdb0308 --- /dev/null +++ b/compensation/tests/ecoaccount/test_workflow.py @@ -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) diff --git a/compensation/tests/payment/__init__.py b/compensation/tests/payment/__init__.py new file mode 100644 index 00000000..b90ce206 --- /dev/null +++ b/compensation/tests/payment/__init__.py @@ -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 + +""" diff --git a/compensation/tests/payment/test_views.py b/compensation/tests/payment/test_views.py new file mode 100644 index 00000000..b1eca5ae --- /dev/null +++ b/compensation/tests/payment/test_views.py @@ -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) diff --git a/compensation/tests/payment/test_workflow.py b/compensation/tests/payment/test_workflow.py new file mode 100644 index 00000000..790fb619 --- /dev/null +++ b/compensation/tests/payment/test_workflow.py @@ -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) + diff --git a/compensation/tests/test_views.py b/compensation/tests/test_views.py deleted file mode 100644 index 8d668220..00000000 --- a/compensation/tests/test_views.py +++ /dev/null @@ -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) - diff --git a/compensation/urls/compensation.py b/compensation/urls/compensation.py index 9c23117b..e1a41ff2 100644 --- a/compensation/urls/compensation.py +++ b/compensation/urls/compensation.py @@ -18,17 +18,24 @@ urlpatterns = [ path('/log', log_view, name='log'), path('/edit', edit_view, name='edit'), path('/remove', remove_view, name='remove'), + path('/state/new', state_new_view, name='new-state'), - path('/action/new', action_new_view, name='new-action'), + path('/state//edit', state_edit_view, name='state-edit'), path('/state//remove', state_remove_view, name='state-remove'), + + path('/action/new', action_new_view, name='new-action'), + path('/action//edit', action_edit_view, name='action-edit'), path('/action//remove', action_remove_view, name='action-remove'), + path('/deadline/new', deadline_new_view, name="new-deadline"), + path('/deadline//edit', deadline_edit_view, name='deadline-edit'), path('/deadline//remove', deadline_remove_view, name='deadline-remove'), path('/report', report_view, name='report'), # Documents path('/document/new/', new_document_view, name='new-doc'), - path('document/', get_document_view, name='get-doc'), - path('document//remove/', remove_document_view, name='remove-doc'), + path('/document/', get_document_view, name='get-doc'), + path('/document//remove/', remove_document_view, name='remove-doc'), + path('/document//edit/', edit_document_view, name='edit-doc'), ] \ No newline at end of file diff --git a/compensation/urls/eco_account.py b/compensation/urls/eco_account.py index 7deb48f7..a3d1aa38 100644 --- a/compensation/urls/eco_account.py +++ b/compensation/urls/eco_account.py @@ -19,22 +19,31 @@ urlpatterns = [ path('/report', report_view, name='report'), path('/edit', edit_view, name='edit'), path('/remove', remove_view, name='remove'), + path('/state/new', state_new_view, name='new-state'), - path('/action/new', action_new_view, name='new-action'), + path('/state//edit', state_edit_view, name='state-edit'), path('/state//remove', state_remove_view, name='state-remove'), + + path('/action/new', action_new_view, name='new-action'), + path('/action//edit', action_edit_view, name='action-edit'), path('/action//remove', action_remove_view, name='action-remove'), - path('/deadline//remove', deadline_remove_view, name='deadline-remove'), + path('/deadline/new', deadline_new_view, name="new-deadline"), + path('/deadline//edit', deadline_edit_view, name='deadline-edit'), + path('/deadline//remove', deadline_remove_view, name='deadline-remove'), + path('/share/', share_view, name='share'), path('/share', create_share_view, name='share-create'), # Documents path('/document/new/', new_document_view, name='new-doc'), - path('document/', get_document_view, name='get-doc'), - path('document//remove/', remove_document_view, name='remove-doc'), + path('/document/', get_document_view, name='get-doc'), + path('/document//edit', edit_document_view, name='edit-doc'), + path('/document//remove/', remove_document_view, name='remove-doc'), # Eco-account deductions - path('/remove/', deduction_remove_view, name='remove-deduction'), + path('/deduction//remove', deduction_remove_view, name='remove-deduction'), + path('/deduction//edit', deduction_edit_view, name='edit-deduction'), path('/deduct/new', new_deduction_view, name='new-deduction'), ] \ No newline at end of file diff --git a/compensation/urls/payment.py b/compensation/urls/payment.py index 2c8e5d4b..b51384dd 100644 --- a/compensation/urls/payment.py +++ b/compensation/urls/payment.py @@ -10,6 +10,7 @@ from compensation.views.payment import * app_name = "pay" urlpatterns = [ - path('/new', new_payment_view, name='new'), - path('/remove', payment_remove_view, name='remove'), + path('/new', new_payment_view, name='new'), + path('/remove/', payment_remove_view, name='remove'), + path('/edit/', payment_edit_view, name='edit'), ] diff --git a/compensation/views/compensation.py b/compensation/views/compensation.py index f6176e03..6dcf442b 100644 --- a/compensation/views/compensation.py +++ b/compensation/views/compensation.py @@ -6,19 +6,23 @@ from django.utils.translation import gettext_lazy as _ from compensation.forms.forms import NewCompensationForm, EditCompensationForm 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.tables import CompensationTable from intervention.models import Intervention from konova.contexts import BaseContext 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.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.utils.documents import get_document, remove_document from konova.utils.generators import generate_qr_code from konova.utils.message_templates import FORM_INVALID, IDENTIFIER_REPLACED, DATA_UNSHARED_EXPLANATION, \ - 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 @@ -274,7 +278,7 @@ def new_document_view(request: HttpRequest, id: str): """ 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( request, msg_success=DOCUMENT_ADDED, @@ -284,45 +288,42 @@ def new_document_view(request: HttpRequest, id: str): @login_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 Wraps the generic document fetcher function 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) - 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) @login_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 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) return remove_document( 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 @default_group_required @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) return form.process_request( request, - msg_success=_("State added"), + msg_success=COMPENSATION_STATE_ADDED, 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) return form.process_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" ) @@ -391,7 +442,31 @@ def deadline_new_view(request: HttpRequest, id: str): form = NewDeadlineModalForm(request.POST or None, instance=comp, request=request) return form.process_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" ) @@ -410,11 +485,12 @@ def deadline_remove_view(request: HttpRequest, id: str, deadline_id: str): Returns: """ + comp = get_object_or_404(Compensation, id=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( request, - msg_success=_("Deadline removed"), + msg_success=DEADLINE_REMOVED, 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: """ + comp = get_object_or_404(Compensation, id=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( 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" ) @@ -456,11 +557,12 @@ def action_remove_view(request: HttpRequest, id: str, action_id: str): Returns: """ + comp = get_object_or_404(Compensation, id=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( request, - msg_success=_("Action removed"), + msg_success=COMPENSATION_ACTION_REMOVED, redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data" ) diff --git a/compensation/views/eco_account.py b/compensation/views/eco_account.py index d04359ef..cf5f3288 100644 --- a/compensation/views/eco_account.py +++ b/compensation/views/eco_account.py @@ -16,21 +16,26 @@ from django.shortcuts import render, get_object_or_404, redirect from compensation.forms.forms import NewEcoAccountForm, EditEcoAccountForm from compensation.forms.modalForms import NewStateModalForm, NewActionModalForm, NewDeadlineModalForm, \ - NewEcoAccountDocumentForm + NewEcoAccountDocumentModalForm, RemoveCompensationActionModalForm, RemoveCompensationStateModalForm, \ + EditCompensationStateModalForm, EditCompensationActionModalForm, EditDeadlineModalForm from compensation.models import EcoAccount, EcoAccountDocument, CompensationState, CompensationAction 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.decorators import any_group_check, default_group_required, conservation_office_group_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.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.utils.documents import get_document, remove_document from konova.utils.generators import generate_qr_code 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 @@ -285,7 +290,7 @@ def deduction_remove_view(request: HttpRequest, id: str, deduction_id: str): except ObjectDoesNotExist: 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( request=request, 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 @default_group_required @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) return form.process_request( request, - msg_success=_("State added"), + msg_success=COMPENSATION_STATE_ADDED, 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) return form.process_request( request, - msg_success=_("Action added"), + msg_success=COMPENSATION_ACTION_ADDED, 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: """ + acc = get_object_or_404(EcoAccount, id=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( 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" ) @@ -423,11 +481,60 @@ def action_remove_view(request: HttpRequest, id: str, action_id: str): Returns: """ + acc = get_object_or_404(EcoAccount, id=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( 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" ) @@ -446,11 +553,12 @@ def deadline_remove_view(request: HttpRequest, id: str, deadline_id: str): Returns: """ + comp = get_object_or_404(EcoAccount, id=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( request, - msg_success=_("Deadline removed"), + msg_success=DEADLINE_REMOVED, 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) return form.process_request( request, - msg_success=_("Deadline added"), + msg_success=DEADLINE_ADDED, 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) - 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( request, msg_success=DOCUMENT_ADDED, @@ -500,46 +608,68 @@ def new_document_view(request: HttpRequest, id: str): @login_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 Wraps the generic document fetcher function 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) - 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) @login_required @default_group_required @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 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) + 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) return remove_document( request, diff --git a/compensation/views/payment.py b/compensation/views/payment.py index b715a3ae..2be5455e 100644 --- a/compensation/views/payment.py +++ b/compensation/views/payment.py @@ -11,52 +11,80 @@ from django.contrib.auth.decorators import login_required from django.http import HttpRequest 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 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.utils.message_templates import PAYMENT_ADDED, PAYMENT_REMOVED +from konova.utils.message_templates import PAYMENT_ADDED, PAYMENT_REMOVED, PAYMENT_EDITED @login_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 Args: 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: """ - 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) return form.process_request( request, 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 @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 Args: 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: """ - payment = get_object_or_404(Payment, id=id) - form = RemoveModalForm(request.POST or None, instance=payment, request=request) + intervention = get_object_or_404(Intervention, id=id) + 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( request=request, msg_success=PAYMENT_REMOVED, 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" + ) + diff --git a/ema/forms.py b/ema/forms.py index 5dc3bcfb..2f193605 100644 --- a/ema/forms.py +++ b/ema/forms.py @@ -15,7 +15,7 @@ from django.utils.translation import gettext_lazy as _ from compensation.forms.forms import AbstractCompensationForm, CompensationResponsibleFormMixin from ema.models import Ema, EmaDocument from intervention.models import Responsibility -from konova.forms import SimpleGeomForm, NewDocumentForm +from konova.forms import SimpleGeomForm, NewDocumentModalForm from user.models import UserActionLogEntry @@ -150,5 +150,5 @@ class EditEmaForm(NewEmaForm): return self.instance -class NewEmaDocumentForm(NewDocumentForm): +class NewEmaDocumentModalForm(NewDocumentModalForm): document_model = EmaDocument \ No newline at end of file diff --git a/ema/models/ema.py b/ema/models/ema.py index b145acba..983bdbd7 100644 --- a/ema/models/ema.py +++ b/ema/models/ema.py @@ -51,28 +51,6 @@ class Ema(AbstractCompensation, ShareableObjectMixin, RecordableObjectMixin): self.identifier = new_id 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: """ Quality check diff --git a/ema/tables.py b/ema/tables.py index d30f3e36..f9689517 100644 --- a/ema/tables.py +++ b/ema/tables.py @@ -6,6 +6,7 @@ Created on: 19.08.21 """ from django.http import HttpRequest +from django.template.loader import render_to_string from django.utils.html import format_html from django.utils.timezone import localtime from django.utils.translation import gettext_lazy as _ @@ -34,6 +35,11 @@ class EmaTable(BaseTable, TableRenderMixin): orderable=True, accessor="title", ) + d = tables.Column( + verbose_name=_("Parcel gmrkng"), + orderable=True, + accessor="geometry", + ) r = tables.Column( verbose_name=_("Recorded"), orderable=True, @@ -87,6 +93,29 @@ class EmaTable(BaseTable, TableRenderMixin): ) 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): """ Renders the registered column for a EMA diff --git a/ema/templates/ema/detail/includes/actions.html b/ema/templates/ema/detail/includes/actions.html index 74f0564a..02772b36 100644 --- a/ema/templates/ema/detail/includes/actions.html +++ b/ema/templates/ema/detail/includes/actions.html @@ -33,7 +33,7 @@ {% trans 'Comment' %} - + {% trans 'Action' %} @@ -58,9 +58,12 @@ {{ action.comment }} - + {% if is_default_member and has_access %} - + {% endif %} diff --git a/ema/templates/ema/detail/includes/deadlines.html b/ema/templates/ema/detail/includes/deadlines.html index 02efaf8b..761ce067 100644 --- a/ema/templates/ema/detail/includes/deadlines.html +++ b/ema/templates/ema/detail/includes/deadlines.html @@ -33,7 +33,7 @@ {% trans 'Comment' %} - + {% trans 'Action' %} @@ -52,9 +52,12 @@ {{ deadline.comment }} - + {% if is_default_member and has_access %} - + {% endif %} diff --git a/ema/templates/ema/detail/includes/documents.html b/ema/templates/ema/detail/includes/documents.html index dbc8927b..b38d5499 100644 --- a/ema/templates/ema/detail/includes/documents.html +++ b/ema/templates/ema/detail/includes/documents.html @@ -28,9 +28,12 @@ {% trans 'Title' %} - {% trans 'Comment' %} + {% trans 'Created on' %} + {% trans 'Comment' %} + + {% trans 'Action' %} @@ -41,18 +44,24 @@ {% for doc in obj.documents.all %} - + {{ doc.title }} + + {{ doc.date_of_creation }} +
{{ doc.comment }}
- + {% if is_default_member and has_access %} - + {% endif %} diff --git a/ema/templates/ema/detail/includes/states-after.html b/ema/templates/ema/detail/includes/states-after.html index f0d3dfc4..e09f4ba5 100644 --- a/ema/templates/ema/detail/includes/states-after.html +++ b/ema/templates/ema/detail/includes/states-after.html @@ -35,7 +35,7 @@ {% trans 'Surface' %} - + {% trans 'Action' %} @@ -55,9 +55,12 @@ {% endif %} {{ state.surface|floatformat:2 }} m² - + {% if is_default_member and has_access %} - + {% endif %} diff --git a/ema/templates/ema/detail/includes/states-before.html b/ema/templates/ema/detail/includes/states-before.html index f479bd8d..1369829b 100644 --- a/ema/templates/ema/detail/includes/states-before.html +++ b/ema/templates/ema/detail/includes/states-before.html @@ -35,7 +35,7 @@ {% trans 'Surface' %} - + {% trans 'Action' %} @@ -55,9 +55,12 @@ {% endif %} {{ state.surface|floatformat:2 }} m² - + {% if is_default_member and has_access %} - + {% endif %} diff --git a/ema/tests/test_views.py b/ema/tests/test_views.py index 3d853e7b..9654e8f3 100644 --- a/ema/tests/test_views.py +++ b/ema/tests/test_views.py @@ -5,14 +5,15 @@ Contact: michel.peltriaux@sgdnord.rlp.de Created on: 26.10.21 """ + from django.db.models import Q from django.urls import reverse 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 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 user.models import UserActionLogEntry @@ -30,42 +31,57 @@ class EmaViewTestCase(CompensationViewTestCase): def setUpTestData(cls) -> None: super().setUpTestData() + def setUp(self) -> None: + super().setUp() # Create dummy data and related objects, like states or actions - cls.create_dummy_data() - state = cls.create_dummy_states() - action = cls.create_dummy_action() - cls.ema.before_states.set([state]) - cls.ema.after_states.set([state]) - cls.ema.actions.set([action]) + self.create_dummy_data() + state = self.create_dummy_states() + action = self.create_dummy_action() + self.ema.before_states.set([state]) + self.ema.after_states.set([state]) + self.ema.actions.set([action]) # Prepare urls - cls.index_url = reverse("ema:index", args=()) - cls.new_url = reverse("ema:new", args=()) - cls.new_id_url = reverse("ema:new-id", args=()) - cls.detail_url = reverse("ema:detail", args=(cls.ema.id,)) - cls.log_url = reverse("ema:log", args=(cls.ema.id,)) - cls.edit_url = reverse("ema:edit", args=(cls.ema.id,)) - cls.remove_url = reverse("ema:remove", args=(cls.ema.id,)) - cls.share_url = reverse("ema:share", args=(cls.ema.id, cls.ema.access_token,)) - cls.share_create_url = reverse("ema:share-create", args=(cls.ema.id,)) - cls.record_url = reverse("ema:record", args=(cls.ema.id,)) - cls.report_url = reverse("ema:report", args=(cls.ema.id,)) - cls.new_doc_url = reverse("ema:new-doc", args=(cls.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,)) + self.index_url = reverse("ema:index", args=()) + self.new_url = reverse("ema:new", args=()) + self.new_id_url = reverse("ema:new-id", args=()) + self.detail_url = reverse("ema:detail", args=(self.ema.id,)) + self.log_url = reverse("ema:log", args=(self.ema.id,)) + self.edit_url = reverse("ema:edit", args=(self.ema.id,)) + self.remove_url = reverse("ema:remove", args=(self.ema.id,)) + self.share_url = reverse("ema:share", args=(self.ema.id, self.ema.access_token,)) + self.share_create_url = reverse("ema:share-create", args=(self.ema.id,)) + self.record_url = reverse("ema:record", args=(self.ema.id,)) + self.report_url = reverse("ema:report", args=(self.ema.id,)) + self.new_doc_url = reverse("ema:new-doc", args=(self.ema.id,)) - @classmethod - def create_dummy_data(cls): + self.state_new_url = reverse("ema:new-state", args=(self.ema.id,)) + 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 log entry - action = UserActionLogEntry.get_created_action(cls.superuser) + action = UserActionLogEntry.get_created_action(self.superuser) # Create responsible data object responsibility_data = Responsibility.objects.create() geometry = Geometry.objects.create() - cls.ema = Ema.objects.create( + self.ema = Ema.objects.create( identifier="TEST", title="Test_title", created=action, @@ -106,10 +122,14 @@ class EmaViewTestCase(CompensationViewTestCase): self.new_id_url, self.edit_url, self.state_new_url, - self.action_new_url, - self.deadline_new_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_new_url, self.new_doc_url, self.log_url, self.remove_url, @@ -152,10 +172,14 @@ class EmaViewTestCase(CompensationViewTestCase): self.new_id_url, self.edit_url, self.state_new_url, - self.action_new_url, - self.deadline_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.log_url, self.remove_url, @@ -189,9 +213,13 @@ class EmaViewTestCase(CompensationViewTestCase): self.new_id_url, self.edit_url, self.state_new_url, - self.action_new_url, - self.deadline_new_url, + self.state_edit_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.new_doc_url, self.log_url, @@ -227,9 +255,13 @@ class EmaViewTestCase(CompensationViewTestCase): fail_urls = [ self.edit_url, self.state_new_url, - self.action_new_url, - self.deadline_new_url, + self.state_edit_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.new_doc_url, self.log_url, diff --git a/ema/tests/test_workflow.py b/ema/tests/test_workflow.py new file mode 100644 index 00000000..3306a21f --- /dev/null +++ b/ema/tests/test_workflow.py @@ -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()) diff --git a/ema/urls.py b/ema/urls.py index 1aa52780..90cafb66 100644 --- a/ema/urls.py +++ b/ema/urls.py @@ -19,18 +19,26 @@ urlpatterns = [ path('/remove', remove_view, name='remove'), path('/record', record_view, name='record'), path('/report', report_view, name='report'), + path('/state/new', state_new_view, name='new-state'), - path('/action/new', action_new_view, name='new-action'), path('/state//remove', state_remove_view, name='state-remove'), + path('/state//edit', state_edit_view, name='state-edit'), + + path('/action/new', action_new_view, name='new-action'), + path('/action//edit', action_edit_view, name='action-edit'), path('/action//remove', action_remove_view, name='action-remove'), - path('/deadline//remove', deadline_remove_view, name='deadline-remove'), + path('/deadline/new', deadline_new_view, name="new-deadline"), + path('/deadline//edit', deadline_edit_view, name='deadline-edit'), + path('/deadline//remove', deadline_remove_view, name='deadline-remove'), + path('/share/', share_view, name='share'), path('/share', create_share_view, name='share-create'), # Documents path('/document/new/', document_new_view, name='new-doc'), - path('document/', get_document_view, name='get-doc'), - path('document//remove/', remove_document_view, name='remove-doc'), + path('/document/', get_document_view, name='get-doc'), + path('/document//edit/', edit_document_view, name='edit-doc'), + path('/document//remove/', remove_document_view, name='remove-doc'), ] \ No newline at end of file diff --git a/ema/views.py b/ema/views.py index ff372170..e9d0acb6 100644 --- a/ema/views.py +++ b/ema/views.py @@ -6,22 +6,27 @@ from django.shortcuts import render, get_object_or_404, redirect from django.urls import reverse 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 ema.forms import NewEmaForm, EditEmaForm, NewEmaDocumentForm +from ema.forms import NewEmaForm, EditEmaForm, NewEmaDocumentModalForm from ema.tables import EmaTable from intervention.forms.modalForms import ShareModalForm from konova.contexts import BaseContext from konova.decorators import conservation_office_group_required, shared_access_required from ema.models import Ema, EmaDocument -from konova.forms import RemoveModalForm, SimpleGeomForm, RecordModalForm +from konova.forms import RemoveModalForm, SimpleGeomForm, RecordModalForm, RemoveDeadlineModalForm, \ + EditDocumentModalForm from konova.models import Deadline from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.utils.documents import get_document, remove_document from konova.utils.generators import generate_qr_code 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 @@ -292,7 +297,7 @@ def state_new_view(request: HttpRequest, id: str): form = NewStateModalForm(request.POST or None, instance=ema, request=request) return form.process_request( request, - msg_success=_("State added"), + msg_success=COMPENSATION_STATE_ADDED, 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) return form.process_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" ) @@ -336,7 +365,7 @@ def deadline_new_view(request: HttpRequest, id: str): form = NewDeadlineModalForm(request.POST or None, instance=ema, request=request) return form.process_request( request, - msg_success=_("Deadline added"), + msg_success=DEADLINE_ADDED, 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) - 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( request, msg_success=DOCUMENT_ADDED, @@ -364,45 +393,68 @@ def document_new_view(request: HttpRequest, id: str): @login_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 Wraps the generic document fetcher function 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) - 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) @login_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 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) + 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) return remove_document( request, @@ -424,11 +476,36 @@ def state_remove_view(request: HttpRequest, id: str, state_id: str): Returns: """ + ema = get_object_or_404(Ema, id=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( 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" ) @@ -447,11 +524,12 @@ def action_remove_view(request: HttpRequest, id: str, action_id: str): Returns: """ + ema = get_object_or_404(Ema, id=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( request, - msg_success=_("Action removed"), + msg_success=COMPENSATION_ACTION_REMOVED, 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 @conservation_office_group_required @shared_access_required(Ema, "id") @@ -589,10 +691,11 @@ def deadline_remove_view(request: HttpRequest, id: str, deadline_id: str): Returns: """ + ema = get_object_or_404(Ema, id=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( request, - msg_success=_("Deadline removed"), + msg_success=DEADLINE_REMOVED, redirect_url=reverse("ema:detail", args=(id,)) + "#related_data" ) \ No newline at end of file diff --git a/intervention/forms/modalForms.py b/intervention/forms/modalForms.py index f8a419d2..a8fa98de 100644 --- a/intervention/forms/modalForms.py +++ b/intervention/forms/modalForms.py @@ -6,8 +6,11 @@ Created on: 27.09.21 """ 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 django.db import transaction from django import forms @@ -15,8 +18,8 @@ from django.utils.translation import gettext_lazy as _ from compensation.models import EcoAccount, EcoAccountDeduction from intervention.inputs import TextToClipboardInput -from intervention.models import Intervention, InterventionDocument -from konova.forms import BaseModalForm, NewDocumentForm +from intervention.models import Intervention, InterventionDocument, RevocationDocument +from konova.forms import BaseModalForm, NewDocumentModalForm, RemoveModalForm from konova.utils.general import format_german_float from konova.utils.user_checks import is_default_group_only @@ -157,6 +160,7 @@ class NewRevocationModalForm(BaseModalForm): } ) ) + document_model = RevocationDocument def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -172,6 +176,46 @@ class NewRevocationModalForm(BaseModalForm): return revocation +class EditRevocationModalForm(NewRevocationModalForm): + revocation = None + + def __init__(self, *args, **kwargs): + self.revocation = kwargs.pop("revocation", None) + super().__init__(*args, **kwargs) + try: + doc = self.revocation.document.file + except ObjectDoesNotExist: + doc = None + form_data = { + "date": str(self.revocation.date), + "file": doc, + "comment": self.revocation.comment, + } + self.load_initial_data(form_data) + + def save(self): + revocation = self.instance.edit_revocation(self) + self.instance.mark_as_edited(self.user, self.request, edit_comment=REVOCATION_EDITED) + return revocation + + +class RemoveRevocationModalForm(RemoveModalForm): + """ Removing modal form for Revocation + + Can be used for anything, where removing shall be confirmed by the user a second time. + + """ + revocation = None + + def __init__(self, *args, **kwargs): + revocation = kwargs.pop("revocation", None) + self.revocation = revocation + super().__init__(*args, **kwargs) + + def save(self): + self.instance.remove_revocation(self) + + class CheckModalForm(BaseModalForm): """ The modal form for running a check on interventions and their compensations @@ -332,6 +376,21 @@ class NewDeductionModalForm(BaseModalForm): else: raise NotImplementedError + def _get_available_surface(self, acc): + """ Calculates how much available surface is left on the account + + Args: + acc (EcoAccount): + + Returns: + + """ + # Calculate valid surface + deductable_surface = acc.deductable_surface + sum_surface_deductions = acc.get_deductions_surface() + rest_surface = deductable_surface - sum_surface_deductions + return rest_surface + def is_valid(self): """ Custom validity check @@ -350,10 +409,7 @@ class NewDeductionModalForm(BaseModalForm): ) return False - # Calculate valid surface - deductable_surface = acc.deductable_surface - sum_surface_deductions = acc.get_deductions_surface() - rest_surface = deductable_surface - sum_surface_deductions + rest_surface = self._get_available_surface(acc) form_surface = float(self.cleaned_data["surface"]) is_valid_surface = form_surface <= rest_surface if not is_valid_surface: @@ -386,9 +442,80 @@ class NewDeductionModalForm(BaseModalForm): def save(self): deduction = self.__create_deduction() self.cleaned_data["intervention"].mark_as_edited(self.user, edit_comment=DEDUCTION_ADDED) - self.cleaned_data["account"].mark_as_edited(self.user, edit_comment=DEDUCTION_ADDED, reset_recorded=False) + self.cleaned_data["account"].mark_as_edited(self.user, edit_comment=DEDUCTION_ADDED) 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 diff --git a/intervention/models/intervention.py b/intervention/models/intervention.py index 06b9174d..167c27af 100644 --- a/intervention/models/intervention.py +++ b/intervention/models/intervention.py @@ -8,6 +8,8 @@ Created on: 15.11.21 import shutil 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.utils import timezone @@ -16,7 +18,6 @@ from django.db import models, transaction from django.db.models import QuerySet from django.http import HttpRequest -from compensation.models import EcoAccountDeduction from intervention.managers import InterventionManager from intervention.models.legal import Legal from intervention.models.responsibility import Responsibility @@ -26,7 +27,8 @@ from konova.models import generate_document_file_upload_path, AbstractDocument, ShareableObjectMixin, \ RecordableObjectMixin, CheckableObjectMixin, GeoReferencedMixin 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 @@ -100,34 +102,6 @@ class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, Chec checker.run_check() 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): """ Getter for all documents of an intervention @@ -196,6 +170,7 @@ class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, Chec comment=form_data.get("comment", None), intervention=self, ) + self.mark_as_edited(user, form.request, edit_comment=PAYMENT_ADDED) return pay def add_revocation(self, form): @@ -229,6 +204,56 @@ class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, Chec ) 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): """ In case the object or a related object changed, internal processes need to be started, such as unrecord and uncheck @@ -242,7 +267,9 @@ class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, Chec 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: self.set_unchecked() return action @@ -258,6 +285,13 @@ class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, Chec Returns: 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): messages.info(request, DATA_UNSHARED_EXPLANATION) 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)) + 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): """ diff --git a/intervention/models/revocation.py b/intervention/models/revocation.py index da39b001..3f55bbf4 100644 --- a/intervention/models/revocation.py +++ b/intervention/models/revocation.py @@ -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") 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 try: self.document.delete(*args, **kwargs) @@ -31,9 +31,6 @@ class Revocation(BaseResource): # No file to delete pass - if user is not None: - self.legal.intervention.mark_as_edited(user, edit_comment=REVOCATION_REMOVED) - super().delete() @property diff --git a/intervention/tables.py b/intervention/tables.py index 0ed003fa..f535039d 100644 --- a/intervention/tables.py +++ b/intervention/tables.py @@ -6,6 +6,7 @@ Created on: 01.12.20 """ from django.http import HttpRequest +from django.template.loader import render_to_string from django.urls import reverse from django.utils.html import format_html from django.utils.timezone import localtime @@ -29,6 +30,11 @@ class InterventionTable(BaseTable, TableRenderMixin): orderable=True, accessor="title", ) + d = tables.Column( + verbose_name=_("Parcel gmrkng"), + orderable=True, + accessor="geometry", + ) c = tables.Column( verbose_name=_("Checked"), orderable=True, @@ -41,12 +47,6 @@ class InterventionTable(BaseTable, TableRenderMixin): empty_values=[], accessor="recorded", ) - rev = tables.Column( - verbose_name=_("Revocation"), - orderable=True, - empty_values=[], - accessor="legal__revocation", - ) e = tables.Column( verbose_name=_("Editable"), orderable=True, @@ -84,14 +84,17 @@ class InterventionTable(BaseTable, TableRenderMixin): Returns: """ - html = "" - html += self.render_link( - tooltip=_("Open {}").format(_("Intervention")), - href=reverse("intervention:detail", args=(record.id,)), - txt=value, - new_tab=False, + context = { + "tooltip": _("Open {}").format(_("Intervention")), + "content": value, + "url": reverse("intervention:detail", args=(record.id,)), + "has_revocations": record.legal.revocations.exists() + } + html = render_to_string( + "table/revocation_warning_col.html", + context ) - return format_html(html) + return html def render_c(self, value, record: Intervention): """ Renders the checked column for an intervention @@ -117,6 +120,28 @@ class InterventionTable(BaseTable, TableRenderMixin): ) 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): """ Renders the recorded column for an intervention @@ -162,28 +187,3 @@ class InterventionTable(BaseTable, TableRenderMixin): ) 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) - diff --git a/intervention/templates/intervention/detail/includes/compensations.html b/intervention/templates/intervention/detail/includes/compensations.html index b7a546dd..c572a344 100644 --- a/intervention/templates/intervention/detail/includes/compensations.html +++ b/intervention/templates/intervention/detail/includes/compensations.html @@ -33,7 +33,7 @@ {% trans 'Title' %} {% if is_default_member and has_access %} - + {% trans 'Action' %} diff --git a/intervention/templates/intervention/detail/includes/deductions.html b/intervention/templates/intervention/detail/includes/deductions.html index 99f11cb8..04110b8b 100644 --- a/intervention/templates/intervention/detail/includes/deductions.html +++ b/intervention/templates/intervention/detail/includes/deductions.html @@ -34,7 +34,7 @@ {% trans 'Created' %} {% if is_default_member and has_access %} - + {% trans 'Action' %} @@ -55,9 +55,12 @@ {{ deduction.surface|floatformat:2|intcomma }} m² {{ deduction.created.timestamp|default_if_none:""|naturalday}} - + {% if is_default_member and has_access %} - + {% endif %} diff --git a/intervention/templates/intervention/detail/includes/documents.html b/intervention/templates/intervention/detail/includes/documents.html index f536f154..9fccc4b7 100644 --- a/intervention/templates/intervention/detail/includes/documents.html +++ b/intervention/templates/intervention/detail/includes/documents.html @@ -27,11 +27,14 @@ {% trans 'Title' %} - + + {% trans 'Created on' %} + + {% trans 'Comment' %} {% if is_default_member and has_access %} - + {% trans 'Action' %} @@ -43,18 +46,26 @@ {% for doc in obj.documents.all %} - + {{ doc.title }} + +
+ {{ doc.date_of_creation }} +
+
{{ doc.comment }}
- + {% if is_default_member and has_access %} - + {% endif %} diff --git a/intervention/templates/intervention/detail/includes/payments.html b/intervention/templates/intervention/detail/includes/payments.html index 53a17f8f..65408e47 100644 --- a/intervention/templates/intervention/detail/includes/payments.html +++ b/intervention/templates/intervention/detail/includes/payments.html @@ -34,7 +34,7 @@ {% trans 'Comment' %} {% if is_default_member and has_access %} - + {% trans 'Action' %} @@ -54,9 +54,12 @@ {{ pay.comment }} - + {% if is_default_member and has_access %} - + {% endif %} diff --git a/intervention/templates/intervention/detail/includes/revocation.html b/intervention/templates/intervention/detail/includes/revocation.html index 9fb2989d..39738d24 100644 --- a/intervention/templates/intervention/detail/includes/revocation.html +++ b/intervention/templates/intervention/detail/includes/revocation.html @@ -37,7 +37,7 @@ {% trans 'Comment' %} {% if is_default_member and has_access %} - + {% trans 'Action' %} @@ -63,9 +63,12 @@ {{ rev.comment }} - + {% if is_default_member and has_access %} - + {% endif %} diff --git a/intervention/tests/test_views.py b/intervention/tests/test_views.py index 23da913a..f12ee7a3 100644 --- a/intervention/tests/test_views.py +++ b/intervention/tests/test_views.py @@ -10,6 +10,7 @@ from django.test import Client from django.contrib.auth.models import Group from django.urls import reverse +from intervention.models import Revocation from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP from konova.tests.test_views import BaseViewTestCase @@ -20,19 +21,34 @@ class InterventionViewTestCase(BaseViewTestCase): def setUpTestData(cls) -> None: super().setUpTestData() + def setUp(self) -> None: + super().setUp() # Prepare urls - cls.index_url = reverse("intervention:index", args=()) - cls.new_url = reverse("intervention:new", args=()) - cls.new_id_url = reverse("intervention:new-id", args=()) - cls.detail_url = reverse("intervention:detail", args=(cls.intervention.id,)) - cls.log_url = reverse("intervention:log", args=(cls.intervention.id,)) - cls.edit_url = reverse("intervention:edit", args=(cls.intervention.id,)) - cls.remove_url = reverse("intervention:remove", args=(cls.intervention.id,)) - cls.share_url = reverse("intervention:share", args=(cls.intervention.id, cls.intervention.access_token,)) - cls.share_create_url = reverse("intervention:share-create", args=(cls.intervention.id,)) - cls.run_check_url = reverse("intervention:check", args=(cls.intervention.id,)) - cls.record_url = reverse("intervention:record", args=(cls.intervention.id,)) - cls.report_url = reverse("intervention:report", args=(cls.intervention.id,)) + self.index_url = reverse("intervention:index", args=()) + self.new_url = reverse("intervention:new", args=()) + self.new_id_url = reverse("intervention:new-id", args=()) + self.detail_url = reverse("intervention:detail", args=(self.intervention.id,)) + self.log_url = reverse("intervention:log", args=(self.intervention.id,)) + self.edit_url = reverse("intervention:edit", args=(self.intervention.id,)) + self.remove_url = reverse("intervention:remove", args=(self.intervention.id,)) + self.share_url = reverse("intervention:share", args=(self.intervention.id, self.intervention.access_token,)) + self.share_create_url = reverse("intervention:share-create", args=(self.intervention.id,)) + self.run_check_url = reverse("intervention:check", args=(self.intervention.id,)) + self.record_url = reverse("intervention:record", args=(self.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): """ 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.run_check_url: f"{login_redirect_base}{self.run_check_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) @@ -96,6 +118,12 @@ class InterventionViewTestCase(BaseViewTestCase): self.share_create_url, self.run_check_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) @@ -128,6 +156,12 @@ class InterventionViewTestCase(BaseViewTestCase): self.edit_url, self.remove_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 = [ self.run_check_url, @@ -172,6 +206,12 @@ class InterventionViewTestCase(BaseViewTestCase): self.remove_url, self.share_create_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 = { self.share_url: self.detail_url @@ -212,6 +252,12 @@ class InterventionViewTestCase(BaseViewTestCase): self.remove_url, self.share_create_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 = { self.share_url: self.detail_url @@ -252,6 +298,12 @@ class InterventionViewTestCase(BaseViewTestCase): self.share_create_url, self.record_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 = { self.share_url: self.detail_url @@ -292,6 +344,12 @@ class InterventionViewTestCase(BaseViewTestCase): self.remove_url, self.share_create_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 = { self.share_url: self.detail_url @@ -332,6 +390,12 @@ class InterventionViewTestCase(BaseViewTestCase): self.remove_url, self.share_create_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 success_urls_redirect = { diff --git a/intervention/tests/test_workflow.py b/intervention/tests/test_workflow.py index fbc2f81f..69f606f0 100644 --- a/intervention/tests/test_workflow.py +++ b/intervention/tests/test_workflow.py @@ -74,6 +74,9 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase): self.assertEqual(obj.identifier, test_id) self.assertEqual(obj.title, test_title) 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: # Fail if there is no such object self.fail() @@ -215,6 +218,8 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase): # Make sure there are no payments on the intervention, yet self.assertEqual(0, self.intervention.payments.count()) + pre_payment_logs_count = self.intervention.log.count() + # Create form data to be sent to the url test_amount = 10.00 test_due = "2021-01-01" @@ -239,6 +244,10 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase): self.assertEqual(payment.amount, test_amount) self.assertEqual(payment.due_on, datetime.date.fromisoformat(test_due)) 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 def subtest_delete_payment(self, payment: Payment): @@ -250,8 +259,10 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase): Returns: """ + pre_payment_logs_count = self.intervention.log.count() + # 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 = { "confirm": True, } @@ -266,6 +277,11 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase): # Now make sure the intervention has no payments anymore 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): """ Checks a 'normal' case of adding a payment. @@ -353,6 +369,8 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase): Returns: """ + pre_deduction_logs_count = self.intervention.log.count() + # Prepare the account for a working situation (enough deductable surface, recorded and shared) self.eco_account.deductable_surface = 10000.00 if self.eco_account.recorded is None: @@ -376,6 +394,11 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase): ) 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 @@ -414,6 +437,8 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase): Returns: """ + pre_delete_logs_count = self.intervention.log.count() + # Prepare url for deleting of this deduction delete_url = reverse("compensation:acc:remove-deduction", args=(self.eco_account.id, deduction.id,)) post_data = { @@ -433,6 +458,11 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase): # Expect the deduction to be totally gone 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): """ Checks a 'normal case of adding a deduction. diff --git a/intervention/urls.py b/intervention/urls.py index 1c663124..2a5e6d38 100644 --- a/intervention/urls.py +++ b/intervention/urls.py @@ -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, \ 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, \ - remove_deduction_view, remove_compensation_view + remove_deduction_view, remove_compensation_view, edit_deduction_view, edit_revocation_view, edit_document_view app_name = "intervention" urlpatterns = [ @@ -28,19 +28,22 @@ urlpatterns = [ path('/report', report_view, name='report'), # Compensations - path('/remove/', remove_compensation_view, name='remove-compensation'), + path('/compensation//remove', remove_compensation_view, name='remove-compensation'), # Documents path('/document/new/', new_document_view, name='new-doc'), - path('document/', get_document_view, name='get-doc'), - path('document//remove/', remove_document_view, name='remove-doc'), + path('/document/', get_document_view, name='get-doc'), + path('/document//remove/', remove_document_view, name='remove-doc'), + path('/document//edit/', edit_document_view, name='edit-doc'), # Deductions path('/deduction/new', new_deduction_view, name='new-deduction'), - path('/remove/', remove_deduction_view, name='remove-deduction'), + path('/deduction//edit', edit_deduction_view, name='edit-deduction'), + path('/deduction//remove', remove_deduction_view, name='remove-deduction'), # Revocation routes path('/revocation/new', new_revocation_view, name='new-revocation'), - path('revocation//remove', remove_revocation_view, name='remove-revocation'), + path('/revocation//edit', edit_revocation_view, name='edit-revocation'), + path('/revocation//remove', remove_revocation_view, name='remove-revocation'), path('revocation/', get_revocation_view, name='get-doc-revocation'), ] \ No newline at end of file diff --git a/intervention/views.py b/intervention/views.py index a518a9da..00440bb8 100644 --- a/intervention/views.py +++ b/intervention/views.py @@ -6,18 +6,19 @@ from django.shortcuts import render from intervention.forms.forms import NewInterventionForm, EditInterventionForm 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.tables import InterventionTable from konova.contexts import BaseContext 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.utils.documents import remove_document, get_document from konova.utils.generators import generate_qr_code from konova.utils.message_templates import INTERVENTION_INVALID, FORM_INVALID, IDENTIFIER_REPLACED, \ 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 @@ -128,7 +129,7 @@ def new_document_view(request: HttpRequest, id: str): """ 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( request, 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 get_document(doc) - @login_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 Wraps the generic document fetcher function 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) - 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) @login_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 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) return remove_document( 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 @any_group_check def detail_view(request: HttpRequest, id: str): @@ -244,14 +267,6 @@ def detail_view(request: HttpRequest, id: str): 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 = { "obj": intervention, "compensations": compensations, @@ -340,23 +355,51 @@ def remove_view(request: HttpRequest, id: str): @login_required @default_group_required -def remove_revocation_view(request: HttpRequest, id: str): - """ Renders a remove view for a revocation +@shared_access_required(Intervention, "id") +def edit_revocation_view(request: HttpRequest, id: str, revocation_id: str): + """ Renders a edit view for a revocation Args: 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: """ - 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( request, 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: 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( request=request, 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 @conservation_office_group_required @shared_access_required(Intervention, "id") diff --git a/konova/autocompletes.py b/konova/autocompletes.py index 3c12b36d..3a79ab68 100644 --- a/konova/autocompletes.py +++ b/konova/autocompletes.py @@ -35,8 +35,9 @@ class EcoAccountAutocomplete(Select2QuerySetView): ) if self.q: qs = qs.filter( - identifier__icontains=self.q - ) + Q(identifier__icontains=self.q) | + Q(title__icontains=self.q) + ).distinct() return qs @@ -57,8 +58,9 @@ class InterventionAutocomplete(Select2QuerySetView): ) if self.q: qs = qs.filter( - identifier__icontains=self.q - ) + Q(identifier__icontains=self.q) | + Q(title__icontains=self.q) + ).distinct() return qs @@ -81,8 +83,9 @@ class ShareUserAutocomplete(Select2QuerySetView): if self.q: # Due to privacy concerns only a full username match will return the proper user entry qs = qs.filter( - username=self.q - ) + Q(username=self.q) | + Q(email=self.q) + ).distinct() return qs diff --git a/konova/forms.py b/konova/forms.py index ab55cb75..57562f71 100644 --- a/konova/forms.py +++ b/konova/forms.py @@ -12,6 +12,8 @@ from bootstrap_modal_forms.forms import BSModalForm from bootstrap_modal_forms.utils import is_ajax from django import forms from django.contrib import messages +from django.db.models.fields.files import FieldFile + from user.models import User from django.contrib.gis.forms import OSMWidget, MultiPolygonField 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 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.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 @@ -87,7 +89,7 @@ class BaseForm(forms.Form): """ 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 Inserts instance data into form and disables form fields @@ -99,8 +101,9 @@ class BaseForm(forms.Form): return for k, v in form_data.items(): self.initialize_form_field(k, v) - for field in disabled_fields: - self.disable_form_field(field) + if disabled_fields: + for field in disabled_fields: + self.disable_form_field(field) def add_widget_html_class(self, field: str, cls: str): """ 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) else: # 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 """ @@ -402,18 +422,22 @@ class NewDocumentForm(BaseModalForm): _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) if not mime_type_valid: self.add_error( "file", - _("Unsupported file type") + FILE_TYPE_UNSUPPORTED ) file_size_valid = self.document_model.is_file_size_valid(_file) if not file_size_valid: self.add_error( "file", - _("File too large") + FILE_SIZE_TOO_LARGE ) file_valid = mime_type_valid and file_size_valid @@ -440,6 +464,39 @@ class NewDocumentForm(BaseModalForm): 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): """ Modal form for recording data diff --git a/konova/migrations/0003_auto_20220208_1801.py b/konova/migrations/0003_auto_20220208_1801.py new file mode 100644 index 00000000..d1d9b5a0 --- /dev/null +++ b/konova/migrations/0003_auto_20220208_1801.py @@ -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', + ), + ] diff --git a/konova/migrations/0004_auto_20220209_0839.py b/konova/migrations/0004_auto_20220209_0839.py new file mode 100644 index 00000000..fe41eada --- /dev/null +++ b/konova/migrations/0004_auto_20220209_0839.py @@ -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', + ), + ] diff --git a/konova/models/document.py b/konova/models/document.py index 72b124d3..b465d70c 100644 --- a/konova/models/document.py +++ b/konova/models/document.py @@ -101,3 +101,19 @@ class AbstractDocument(BaseResource): def is_file_size_valid(cls, _file): max_size = cls._maximum_file_size * pow(1000, 2) 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() \ No newline at end of file diff --git a/konova/models/geometry.py b/konova/models/geometry.py index 0a380b48..bec89c39 100644 --- a/konova/models/geometry.py +++ b/konova/models/geometry.py @@ -99,7 +99,7 @@ class Geometry(BaseResource): Returns: """ - from konova.models import Parcel, District + from konova.models import Parcel, District, ParcelIntersection parcel_fetcher = ParcelWFSFetcher( geometry_id=self.id, ) @@ -107,6 +107,7 @@ class Geometry(BaseResource): fetched_parcels = parcel_fetcher.get_features( typename ) + _now = timezone.now() underlying_parcels = [] for result in fetched_parcels: fetched_parcel = result[typename] @@ -125,19 +126,35 @@ class Geometry(BaseResource): krs=fetched_parcel["ave:kreis"], )[0] parcel_obj.district = district - parcel_obj.updated_on = timezone.now() + parcel_obj.updated_on = _now parcel_obj.save() underlying_parcels.append(parcel_obj) + # Update the linked 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): """ Getter for related parcels and their districts Returns: 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" ).order_by( "gmrkng", diff --git a/konova/models/object.py b/konova/models/object.py index 9eba8c1d..a6164f5a 100644 --- a/konova/models/object.py +++ b/konova/models/object.py @@ -12,6 +12,7 @@ from abc import abstractmethod from django.contrib import messages 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, \ celery_send_mail_shared_data_recorded, celery_send_mail_shared_data_unrecorded, \ celery_send_mail_shared_data_deleted, celery_send_mail_shared_data_checked @@ -128,10 +129,27 @@ class BaseObject(BaseResource): # Send mail shared_users = self.shared_users.values_list("id", flat=True) 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() + 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): """ Wraps adding of UserActionLogEntry to log @@ -200,6 +218,10 @@ class BaseObject(BaseResource): _str = "{}{}-{}".format(curr_month, curr_year, rand_str) return definitions[self.__class__]["template"].format(_str) + @abstractmethod + def get_detail_url(self): + raise NotImplementedError() + class RecordableObjectMixin(models.Model): """ Wraps record related fields and functionality @@ -236,7 +258,7 @@ class RecordableObjectMixin(models.Model): shared_users = self.users.all().values_list("id", flat=True) 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 @@ -258,29 +280,22 @@ class RecordableObjectMixin(models.Model): shared_users = self.users.all().values_list("id", flat=True) 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 - 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 - unrecord and uncheck + def unrecord(self, performing_user: User, request: HttpRequest = None): + """ Unrecords a dataset 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 - reset_recorded (bool): Whether the record-state of the object should be reset Returns: """ - edit_action = UserActionLogEntry.get_edited_action(performing_user, edit_comment) - self.modified = edit_action - self.log.add(edit_action) - self.save() - - if self.recorded and reset_recorded: + action = None + if self.recorded: action = self.set_unrecorded(performing_user) self.log.add(action) if request: @@ -288,7 +303,7 @@ class RecordableObjectMixin(models.Model): request, CHECKED_RECORDED_RESET ) - return edit_action + return action @abstractmethod def is_ready_for_publish(self) -> bool: @@ -350,7 +365,7 @@ class CheckableObjectMixin(models.Model): # Send mail shared_users = self.users.all().values_list("id", flat=True) 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) return action @@ -464,9 +479,9 @@ class ShareableObjectMixin(models.Model): # Send mails 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: - 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 self.share_with_list(users) @@ -530,3 +545,31 @@ class GeoReferencedMixin(models.Model): message_str = GEOMETRY_CONFLICT_WITH_TEMPLATE.format(instance_identifiers) messages.info(request, message_str) 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, + ) \ No newline at end of file diff --git a/konova/models/parcel.py b/konova/models/parcel.py index 487225e6..9c887f1a 100644 --- a/konova/models/parcel.py +++ b/konova/models/parcel.py @@ -22,7 +22,7 @@ class Parcel(UuidModel): 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") gmrkng = models.CharField( max_length=1000, @@ -77,3 +77,22 @@ class District(UuidModel): def __str__(self): 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) diff --git a/konova/static/css/konova.css b/konova/static/css/konova.css index a10e124b..709d3eb3 100644 --- a/konova/static/css/konova.css +++ b/konova/static/css/konova.css @@ -219,6 +219,13 @@ Overwrites bootstrap .btn:focus box shadow color overflow: auto; } +.w-20{ + width: 20%; +} +.w-10{ + width: 20%; +} + /* Extends css for django autocomplete light (dal) No other approach worked to get the autocomplete fields to full width of parent containers diff --git a/konova/tasks.py b/konova/tasks.py index a463374c..c74a2bd7 100644 --- a/konova/tasks.py +++ b/konova/tasks.py @@ -4,13 +4,19 @@ from celery import shared_task from django.core.exceptions import ObjectDoesNotExist - @shared_task def celery_update_parcels(geometry_id: str, recheck: bool = True): - from konova.models import Geometry + from konova.models import Geometry, ParcelIntersection try: 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() except ObjectDoesNotExist: if recheck: @@ -19,42 +25,42 @@ def celery_update_parcels(geometry_id: str, recheck: bool = True): @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 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 -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 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 -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 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 -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 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 -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 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 -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 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) diff --git a/konova/tests/test_views.py b/konova/tests/test_views.py index 1bf33325..6218a6c0 100644 --- a/konova/tests/test_views.py +++ b/konova/tests/test_views.py @@ -7,6 +7,7 @@ Created on: 26.10.21 """ import datetime +from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID from ema.models import Ema from user.models import User 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.urls import reverse -from codelist.models import KonovaCode +from codelist.models import KonovaCode, KonovaCodeList from compensation.models import Compensation, CompensationState, CompensationAction, EcoAccount, EcoAccountDeduction from intervention.models import Legal, Responsibility, Intervention from konova.management.commands.setup_data import GROUPS_DATA @@ -46,43 +47,58 @@ class BaseTestCase(TestCase): class Meta: abstract = True - @classmethod - def setUpTestData(cls): - 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() + def setUp(self) -> None: + """ Setup data before each test run - @classmethod - def create_users(cls): + Returns: + + """ + 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 - cls.superuser = User.objects.create_superuser( + self.superuser = User.objects.create_superuser( username="root", 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", 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 for group_data in GROUPS_DATA: name = group_data.get("name") Group.objects.get_or_create( name=name, ) - cls.groups = Group.objects.all() + self.groups = Group.objects.all() @staticmethod def create_dummy_string(prefix: str = ""): @@ -93,8 +109,7 @@ class BaseTestCase(TestCase): """ return f"{prefix}{generate_random_string(3, True)}" - @classmethod - def create_dummy_intervention(cls): + def create_dummy_intervention(self): """ Creates an intervention which can be used for tests Returns: @@ -102,7 +117,7 @@ class BaseTestCase(TestCase): """ # Create dummy data # 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) legal_data = Legal.objects.create() # Create responsible data object @@ -121,32 +136,30 @@ class BaseTestCase(TestCase): intervention.generate_access_token(make_unique=True) return intervention - @classmethod - def create_dummy_compensation(cls): + def create_dummy_compensation(self): """ Creates a compensation which can be used for tests Returns: """ - if cls.intervention is None: - cls.intervention = cls.create_dummy_intervention() + if self.intervention is None: + self.intervention = self.create_dummy_intervention() # Create dummy data # Create log entry - action = UserActionLogEntry.get_created_action(cls.superuser) + action = UserActionLogEntry.get_created_action(self.superuser) geometry = Geometry.objects.create() # Finally create main object, holding the other objects compensation = Compensation.objects.create( identifier="TEST", title="Test_title", - intervention=cls.intervention, + intervention=self.intervention, created=action, geometry=geometry, comment="Test", ) return compensation - @classmethod - def create_dummy_eco_account(cls): + def create_dummy_eco_account(self): """ Creates an eco account which can be used for tests Returns: @@ -154,7 +167,7 @@ class BaseTestCase(TestCase): """ # Create dummy data # Create log entry - action = UserActionLogEntry.get_created_action(cls.superuser) + action = UserActionLogEntry.get_created_action(self.superuser) geometry = Geometry.objects.create() # Create responsible data object lega_data = Legal.objects.create() @@ -171,8 +184,7 @@ class BaseTestCase(TestCase): ) return eco_account - @classmethod - def create_dummy_ema(cls): + def create_dummy_ema(self): """ Creates an ema which can be used for tests Returns: @@ -180,7 +192,7 @@ class BaseTestCase(TestCase): """ # Create dummy data # Create log entry - action = UserActionLogEntry.get_created_action(cls.superuser) + action = UserActionLogEntry.get_created_action(self.superuser) geometry = Geometry.objects.create() # Create responsible data object responsible_data = Responsibility.objects.create() @@ -195,51 +207,47 @@ class BaseTestCase(TestCase): ) return ema - @classmethod - def create_dummy_deduction(cls): + def create_dummy_deduction(self): return EcoAccountDeduction.objects.create( - account=cls.create_dummy_eco_account(), - intervention=cls.create_dummy_intervention(), + account=self.create_dummy_eco_account(), + intervention=self.create_dummy_intervention(), surface=100, ) - @classmethod - def create_dummy_states(cls): + def create_dummy_states(self): """ Creates an intervention which can be used for tests Returns: """ - cls.comp_state = CompensationState.objects.create( + self.comp_state = CompensationState.objects.create( surface=10.00, biotope_type=None, ) - return cls.comp_state + return self.comp_state - @classmethod - def create_dummy_action(cls): + def create_dummy_action(self): """ Creates an intervention which can be used for tests Returns: """ - cls.comp_action = CompensationAction.objects.create( + self.comp_action = CompensationAction.objects.create( amount=10 ) - return cls.comp_action + return self.comp_action - @classmethod - def create_dummy_codes(cls): + def create_dummy_codes(self): """ Creates some dummy KonovaCodes which can be used for testing Returns: """ codes = KonovaCode.objects.bulk_create([ - KonovaCode(id=1, is_selectable=True, long_name="Test1"), - KonovaCode(id=2, is_selectable=True, long_name="Test2"), - KonovaCode(id=3, is_selectable=True, long_name="Test3"), - KonovaCode(id=4, is_selectable=True, long_name="Test4"), + KonovaCode(id=1, is_selectable=True, is_archived=False, is_leaf=True, long_name="Test1"), + KonovaCode(id=2, is_selectable=True, is_archived=False, is_leaf=True, long_name="Test2"), + KonovaCode(id=3, is_selectable=True, is_archived=False, is_leaf=True, long_name="Test3"), + KonovaCode(id=4, is_selectable=True, is_archived=False, is_leaf=True, long_name="Test4"), ]) return codes @@ -255,8 +263,7 @@ class BaseTestCase(TestCase): polygon = polygon.transform(3857, clone=True) return MultiPolygon(polygon, srid=3857) # 3857 is the default srid used for MultiPolygonField in the form - @classmethod - def fill_out_intervention(cls, intervention: Intervention) -> Intervention: + def fill_out_intervention(self, intervention: Intervention) -> Intervention: """ Adds all required (dummy) data to an intervention Args: @@ -276,13 +283,12 @@ class BaseTestCase(TestCase): intervention.legal.process_type = KonovaCode.objects.get(id=3) intervention.legal.save() 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.save() return intervention - @classmethod - def fill_out_compensation(cls, compensation: Compensation) -> Compensation: + def fill_out_compensation(self, compensation: Compensation) -> Compensation: """ Adds all required (dummy) data to a compensation Args: @@ -291,13 +297,62 @@ class BaseTestCase(TestCase): Returns: compensation (Compensation): The modified compensation """ - compensation.after_states.add(cls.comp_state) - compensation.before_states.add(cls.comp_state) - compensation.actions.add(cls.comp_action) - compensation.geometry.geom = cls.create_dummy_geometry() + compensation.after_states.add(self.comp_state) + compensation.before_states.add(self.comp_state) + compensation.actions.add(self.comp_action) + compensation.geometry.geom = self.create_dummy_geometry() compensation.geometry.save() 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): """ Assert for geometries to be equal @@ -337,7 +392,10 @@ class BaseViewTestCase(BaseTestCase): @classmethod def setUpTestData(cls) -> None: 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): """ Assert for all given urls a direct 200 response @@ -496,21 +554,6 @@ class BaseWorkflowTestCase(BaseTestCase): def setUpTestData(cls): 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): """ Provides a quick check whether an object has been removed from the database or not diff --git a/konova/utils/documents.py b/konova/utils/documents.py index 63a7d328..f9b15160 100644 --- a/konova/utils/documents.py +++ b/konova/utils/documents.py @@ -50,5 +50,5 @@ def remove_document(request: HttpRequest, doc: AbstractDocument): form = RemoveModalForm(request.POST or None, instance=doc, request=request) return form.process_request( request=request, - msg_success=DOCUMENT_REMOVED_TEMPLATE.format(title) + msg_success=DOCUMENT_REMOVED_TEMPLATE.format(title), ) \ No newline at end of file diff --git a/konova/utils/mailer.py b/konova/utils/mailer.py index e174ae56..33907f3c 100644 --- a/konova/utils/mailer.py +++ b/konova/utils/mailer.py @@ -45,11 +45,12 @@ class Mailer: 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 Args: obj_identifier (str): The object identifier + obj_title (str): The object title Returns: @@ -57,6 +58,7 @@ class Mailer: context = { "user": user, "obj_identifier": obj_identifier, + "obj_title": obj_title, "EMAIL_REPLY_TO": EMAIL_REPLY_TO, } msg = render_to_string("email/sharing/shared_access_removed.html", context) @@ -67,7 +69,7 @@ class Mailer: 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 Args: @@ -79,6 +81,7 @@ class Mailer: context = { "user": user, "obj_identifier": obj_identifier, + "obj_title": obj_title, "EMAIL_REPLY_TO": EMAIL_REPLY_TO, } msg = render_to_string("email/sharing/shared_access_given.html", context) @@ -89,7 +92,7 @@ class Mailer: 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 Args: @@ -101,6 +104,7 @@ class Mailer: context = { "user": user, "obj_identifier": obj_identifier, + "obj_title": obj_title, "EMAIL_REPLY_TO": EMAIL_REPLY_TO, } msg = render_to_string("email/recording/shared_data_recorded.html", context) @@ -111,7 +115,7 @@ class Mailer: 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 Args: @@ -123,6 +127,7 @@ class Mailer: context = { "user": user, "obj_identifier": obj_identifier, + "obj_title": obj_title, "EMAIL_REPLY_TO": EMAIL_REPLY_TO, } msg = render_to_string("email/recording/shared_data_unrecorded.html", context) @@ -133,7 +138,7 @@ class Mailer: 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 Args: @@ -145,6 +150,7 @@ class Mailer: context = { "user": user, "obj_identifier": obj_identifier, + "obj_title": obj_title, "EMAIL_REPLY_TO": EMAIL_REPLY_TO, } msg = render_to_string("email/deleting/shared_data_deleted.html", context) @@ -155,7 +161,7 @@ class Mailer: 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 Args: @@ -167,6 +173,7 @@ class Mailer: context = { "user": user, "obj_identifier": obj_identifier, + "obj_title": obj_title, "EMAIL_REPLY_TO": EMAIL_REPLY_TO, } msg = render_to_string("email/checking/shared_data_checked.html", context) diff --git a/konova/utils/message_templates.py b/konova/utils/message_templates.py index 79a190ca..c7a2be00 100644 --- a/konova/utils/message_templates.py +++ b/konova/utils/message_templates.py @@ -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") +# FILES +FILE_TYPE_UNSUPPORTED = _("Unsupported file type") +FILE_SIZE_TOO_LARGE = _("File too large") + # ECO ACCOUNT CANCEL_ACC_RECORDED_OR_DEDUCTED = _("Action canceled. Eco account is recorded or deductions exist. Only conservation office member can perform this action.") # COMPENSATION COMPENSATION_ADDED_TEMPLATE = _("Compensation {} added") 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 DEDUCTION_ADDED = _("Deduction added") +DEDUCTION_EDITED = _("Deduction edited") DEDUCTION_REMOVED = _("Deduction removed") +# DEADLINE +DEADLINE_ADDED = _("Deadline added") +DEADLINE_EDITED = _("Deadline edited") +DEADLINE_REMOVED = _("Deadline removed") + # PAYMENTS PAYMENT_ADDED = _("Payment added") +PAYMENT_EDITED = _("Payment edited") PAYMENT_REMOVED = _("Payment removed") # REVOCATIONS REVOCATION_ADDED = _("Revocation added") +REVOCATION_EDITED = _("Revocation edited") REVOCATION_REMOVED = _("Revocation removed") # DOCUMENTS DOCUMENT_REMOVED_TEMPLATE = _("Document '{}' deleted") DOCUMENT_ADDED = _("Document added") +DOCUMENT_EDITED = _("Document edited") # Edited EDITED_GENERAL_DATA = _("Edited general data") -ADDED_COMPENSATION_STATE = _("Added compensation state") ADDED_DEADLINE = _("Added deadline") -ADDED_COMPENSATION_ACTION = _("Added compensation action") # Geometry conflicts GEOMETRY_CONFLICT_WITH_TEMPLATE = _("Geometry conflict detected with {}") + +# INTERVENTION +INTERVENTION_HAS_REVOCATIONS_TEMPLATE = _("This intervention has {} revocations") diff --git a/locale/de/LC_MESSAGES/django.mo b/locale/de/LC_MESSAGES/django.mo index 23892cf8..f2e51b00 100644 Binary files a/locale/de/LC_MESSAGES/django.mo and b/locale/de/LC_MESSAGES/django.mo differ diff --git a/locale/de/LC_MESSAGES/django.po b/locale/de/LC_MESSAGES/django.po index 65808d43..488b3d4c 100644 --- a/locale/de/LC_MESSAGES/django.po +++ b/locale/de/LC_MESSAGES/django.po @@ -5,10 +5,10 @@ # #: compensation/filters.py:122 compensation/forms/modalForms.py:35 #: compensation/forms/modalForms.py:46 compensation/forms/modalForms.py:62 -#: compensation/forms/modalForms.py:256 compensation/forms/modalForms.py:350 +#: compensation/forms/modalForms.py:332 compensation/forms/modalForms.py:425 #: intervention/forms/forms.py:54 intervention/forms/forms.py:156 -#: intervention/forms/forms.py:168 intervention/forms/modalForms.py:124 -#: intervention/forms/modalForms.py:137 intervention/forms/modalForms.py:150 +#: intervention/forms/forms.py:168 intervention/forms/modalForms.py:127 +#: intervention/forms/modalForms.py:140 intervention/forms/modalForms.py:153 #: konova/filters/mixins.py:53 konova/filters/mixins.py:54 #: konova/filters/mixins.py:81 konova/filters/mixins.py:82 #: konova/filters/mixins.py:94 konova/filters/mixins.py:95 @@ -18,15 +18,15 @@ #: konova/filters/mixins.py:270 konova/filters/mixins.py:315 #: konova/filters/mixins.py:353 konova/filters/mixins.py:354 #: konova/filters/mixins.py:385 konova/filters/mixins.py:386 -#: konova/forms.py:140 konova/forms.py:241 konova/forms.py:312 -#: konova/forms.py:339 konova/forms.py:349 konova/forms.py:362 -#: konova/forms.py:374 konova/forms.py:392 user/forms.py:42 +#: konova/forms.py:143 konova/forms.py:244 konova/forms.py:315 +#: konova/forms.py:359 konova/forms.py:369 konova/forms.py:382 +#: konova/forms.py:394 konova/forms.py:412 user/forms.py:42 #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-02-03 13:37+0100\n" +"POT-Creation-Date: 2022-02-10 10:17+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -63,7 +63,7 @@ msgstr "Verantwortliche Stelle" #: analysis/forms.py:58 compensation/forms/forms.py:88 #: compensation/forms/forms.py:165 intervention/forms/forms.py:64 #: intervention/forms/forms.py:81 intervention/forms/forms.py:97 -#: intervention/forms/forms.py:113 intervention/forms/modalForms.py:46 +#: intervention/forms/forms.py:113 intervention/forms/modalForms.py:49 msgid "Click for selection" msgstr "Auswählen..." @@ -75,7 +75,7 @@ msgstr "Bericht generieren" msgid "Select a timespan and the desired conservation office" msgstr "Wählen Sie die Zeitspanne und die gewünschte Eintragungsstelle" -#: analysis/forms.py:69 konova/forms.py:188 +#: analysis/forms.py:69 konova/forms.py:191 msgid "Continue" msgstr "Weiter" @@ -95,7 +95,7 @@ msgstr "" #: analysis/templates/analysis/reports/includes/eco_account/amount.html:3 #: analysis/templates/analysis/reports/includes/intervention/amount.html:3 #: analysis/templates/analysis/reports/includes/old_data/amount.html:3 -#: compensation/forms/modalForms.py:334 +#: compensation/forms/modalForms.py:409 #: compensation/templates/compensation/detail/eco_account/includes/deductions.html:34 #: intervention/templates/intervention/detail/includes/deductions.html:31 msgid "Amount" @@ -136,9 +136,9 @@ msgstr "Zuständigkeitsbereich" #: analysis/templates/analysis/reports/includes/intervention/amount.html:17 #: analysis/templates/analysis/reports/includes/intervention/compensated_by.html:8 #: analysis/templates/analysis/reports/includes/intervention/laws.html:17 -#: compensation/tables.py:35 +#: compensation/tables.py:40 #: compensation/templates/compensation/detail/compensation/view.html:63 -#: intervention/tables.py:33 +#: intervention/tables.py:39 #: intervention/templates/intervention/detail/view.html:68 #: user/models/user_action.py:20 msgid "Checked" @@ -152,12 +152,12 @@ msgstr "Geprüft" #: analysis/templates/analysis/reports/includes/intervention/compensated_by.html:9 #: analysis/templates/analysis/reports/includes/intervention/laws.html:20 #: analysis/templates/analysis/reports/includes/old_data/amount.html:18 -#: compensation/tables.py:41 compensation/tables.py:182 +#: compensation/tables.py:46 compensation/tables.py:222 #: compensation/templates/compensation/detail/compensation/view.html:77 #: compensation/templates/compensation/detail/eco_account/includes/deductions.html:31 #: compensation/templates/compensation/detail/eco_account/view.html:44 -#: ema/tables.py:38 ema/templates/ema/detail/view.html:35 -#: intervention/tables.py:39 +#: ema/tables.py:44 ema/templates/ema/detail/view.html:35 +#: intervention/tables.py:45 #: intervention/templates/intervention/detail/view.html:82 #: user/models/user_action.py:21 msgid "Recorded" @@ -196,7 +196,7 @@ msgid "Other registration office" msgstr "Andere Zulassungsbehörden" #: analysis/templates/analysis/reports/includes/compensation/card_compensation.html:11 -#: compensation/tables.py:62 +#: compensation/tables.py:67 #: intervention/templates/intervention/detail/includes/compensations.html:8 #: intervention/templates/intervention/report/report.html:49 msgid "Compensations" @@ -213,14 +213,14 @@ msgstr "Abbuchungen" #: analysis/templates/analysis/reports/includes/eco_account/deductions.html:9 #: analysis/templates/analysis/reports/includes/eco_account/deductions.html:11 -#: compensation/forms/modalForms.py:151 +#: compensation/forms/modalForms.py:193 #: compensation/templates/compensation/detail/compensation/includes/states-after.html:36 #: compensation/templates/compensation/detail/compensation/includes/states-before.html:36 #: compensation/templates/compensation/detail/eco_account/includes/states-after.html:36 #: compensation/templates/compensation/detail/eco_account/includes/states-before.html:36 #: ema/templates/ema/detail/includes/states-after.html:36 #: ema/templates/ema/detail/includes/states-before.html:36 -#: intervention/forms/modalForms.py:294 +#: intervention/forms/modalForms.py:338 msgid "Surface" msgstr "Fläche" @@ -239,7 +239,6 @@ msgstr "Kompensationsart" #: analysis/templates/analysis/reports/includes/intervention/compensated_by.html:15 #: analysis/templates/analysis/reports/includes/old_data/amount.html:29 -#: compensation/tables.py:85 #: compensation/templates/compensation/detail/compensation/view.html:19 #: konova/templates/konova/includes/quickstart/compensations.html:4 #: templates/navbars/navbar.html:28 @@ -284,8 +283,8 @@ msgid "Type" msgstr "Typ" #: analysis/templates/analysis/reports/includes/old_data/amount.html:24 -#: intervention/forms/modalForms.py:305 intervention/forms/modalForms.py:312 -#: intervention/tables.py:89 +#: compensation/tables.py:89 intervention/forms/modalForms.py:349 +#: intervention/forms/modalForms.py:356 intervention/tables.py:88 #: intervention/templates/intervention/detail/view.html:19 #: konova/templates/konova/includes/quickstart/interventions.html:4 #: templates/navbars/navbar.html:22 @@ -293,9 +292,9 @@ msgid "Intervention" msgstr "Eingriff" #: analysis/templates/analysis/reports/includes/old_data/amount.html:34 -#: compensation/tables.py:226 +#: compensation/tables.py:266 #: compensation/templates/compensation/detail/eco_account/view.html:19 -#: intervention/forms/modalForms.py:278 intervention/forms/modalForms.py:285 +#: intervention/forms/modalForms.py:322 intervention/forms/modalForms.py:329 #: konova/templates/konova/includes/quickstart/ecoaccounts.html:4 #: templates/navbars/navbar.html:34 msgid "Eco-account" @@ -314,8 +313,8 @@ msgid "Show only unrecorded" msgstr "Nur unverzeichnete anzeigen" #: compensation/forms/forms.py:32 compensation/tables.py:25 -#: compensation/tables.py:167 ema/tables.py:28 intervention/forms/forms.py:28 -#: intervention/tables.py:23 +#: compensation/tables.py:197 ema/tables.py:29 intervention/forms/forms.py:28 +#: intervention/tables.py:24 #: intervention/templates/intervention/detail/includes/compensations.html:30 msgid "Identifier" msgstr "Kennung" @@ -326,22 +325,22 @@ msgid "Generated automatically" msgstr "Automatisch generiert" #: compensation/forms/forms.py:44 compensation/tables.py:30 -#: compensation/tables.py:172 +#: compensation/tables.py:202 #: compensation/templates/compensation/detail/compensation/includes/documents.html:28 #: compensation/templates/compensation/detail/compensation/view.html:31 #: compensation/templates/compensation/detail/eco_account/includes/documents.html:28 #: compensation/templates/compensation/detail/eco_account/view.html:31 #: compensation/templates/compensation/report/compensation/report.html:12 #: compensation/templates/compensation/report/eco_account/report.html:12 -#: ema/tables.py:33 ema/templates/ema/detail/includes/documents.html:28 +#: ema/tables.py:34 ema/templates/ema/detail/includes/documents.html:28 #: ema/templates/ema/detail/view.html:31 #: ema/templates/ema/report/report.html:12 intervention/forms/forms.py:40 -#: intervention/tables.py:28 +#: intervention/tables.py:29 #: intervention/templates/intervention/detail/includes/compensations.html:33 #: intervention/templates/intervention/detail/includes/documents.html:28 #: intervention/templates/intervention/detail/view.html:31 #: intervention/templates/intervention/report/report.html:12 -#: konova/forms.py:338 +#: konova/forms.py:358 msgid "Title" msgstr "Bezeichnung" @@ -354,7 +353,7 @@ msgid "Compensation XY; Location ABC" msgstr "Kompensation XY; Flur ABC" #: compensation/forms/forms.py:57 compensation/forms/modalForms.py:61 -#: compensation/forms/modalForms.py:255 compensation/forms/modalForms.py:349 +#: compensation/forms/modalForms.py:331 compensation/forms/modalForms.py:424 #: compensation/templates/compensation/detail/compensation/includes/actions.html:35 #: compensation/templates/compensation/detail/compensation/includes/deadlines.html:34 #: compensation/templates/compensation/detail/compensation/includes/documents.html:31 @@ -364,15 +363,15 @@ msgstr "Kompensation XY; Flur ABC" #: ema/templates/ema/detail/includes/actions.html:34 #: ema/templates/ema/detail/includes/deadlines.html:34 #: ema/templates/ema/detail/includes/documents.html:31 -#: intervention/forms/forms.py:180 intervention/forms/modalForms.py:149 -#: intervention/templates/intervention/detail/includes/documents.html:31 +#: intervention/forms/forms.py:180 intervention/forms/modalForms.py:152 +#: intervention/templates/intervention/detail/includes/documents.html:34 #: intervention/templates/intervention/detail/includes/payments.html:34 #: intervention/templates/intervention/detail/includes/revocation.html:38 -#: konova/forms.py:373 konova/templates/konova/includes/comment_card.html:16 +#: konova/forms.py:393 konova/templates/konova/includes/comment_card.html:16 msgid "Comment" msgstr "Kommentar" -#: compensation/forms/forms.py:59 compensation/forms/modalForms.py:351 +#: compensation/forms/forms.py:59 compensation/forms/modalForms.py:426 #: intervention/forms/forms.py:182 msgid "Additional comment" msgstr "Zusätzlicher Kommentar" @@ -432,7 +431,7 @@ msgstr "kompensiert Eingriff" msgid "Select the intervention for which this compensation compensates" msgstr "Wählen Sie den Eingriff, für den diese Kompensation bestimmt ist" -#: compensation/forms/forms.py:184 compensation/views/compensation.py:92 +#: compensation/forms/forms.py:184 compensation/views/compensation.py:94 msgid "New compensation" msgstr "Neue Kompensation" @@ -458,7 +457,7 @@ msgstr "Vereinbarungsdatum" msgid "When did the parties agree on this?" msgstr "Wann wurde dieses Ökokonto offiziell vereinbart?" -#: compensation/forms/forms.py:354 compensation/views/eco_account.py:102 +#: compensation/forms/forms.py:354 compensation/views/eco_account.py:105 msgid "New Eco-Account" msgstr "Neues Ökokonto" @@ -483,8 +482,8 @@ msgstr "Fällig am" msgid "Due on which date" msgstr "Zahlung wird an diesem Datum erwartet" -#: compensation/forms/modalForms.py:63 compensation/forms/modalForms.py:257 -#: intervention/forms/modalForms.py:151 konova/forms.py:375 +#: compensation/forms/modalForms.py:63 compensation/forms/modalForms.py:333 +#: intervention/forms/modalForms.py:154 konova/forms.py:395 msgid "Additional comment, maximum {} letters" msgstr "Zusätzlicher Kommentar, maximal {} Zeichen" @@ -496,75 +495,75 @@ msgstr "Neue Ersatzzahlung zu Eingriff '{}' hinzufügen" msgid "If there is no date you can enter, please explain why." msgstr "Falls Sie kein Datum angeben können, erklären Sie bitte weshalb." -#: compensation/forms/modalForms.py:115 compensation/forms/modalForms.py:127 +#: compensation/forms/modalForms.py:157 compensation/forms/modalForms.py:169 msgid "Biotope Type" msgstr "Biotoptyp" -#: compensation/forms/modalForms.py:118 +#: compensation/forms/modalForms.py:160 msgid "Select the biotope type" msgstr "Biotoptyp wählen" -#: compensation/forms/modalForms.py:132 compensation/forms/modalForms.py:144 +#: compensation/forms/modalForms.py:174 compensation/forms/modalForms.py:186 msgid "Biotope additional type" msgstr "Zusatzbezeichnung" -#: compensation/forms/modalForms.py:135 +#: compensation/forms/modalForms.py:177 msgid "Select an additional biotope type" msgstr "Zusatzbezeichnung wählen" -#: compensation/forms/modalForms.py:154 intervention/forms/modalForms.py:296 +#: compensation/forms/modalForms.py:196 intervention/forms/modalForms.py:340 msgid "in m²" msgstr "" -#: compensation/forms/modalForms.py:165 +#: compensation/forms/modalForms.py:207 msgid "New state" msgstr "Neuer Zustand" -#: compensation/forms/modalForms.py:166 +#: compensation/forms/modalForms.py:208 msgid "Insert data for the new state" msgstr "Geben Sie die Daten des neuen Zustandes ein" -#: compensation/forms/modalForms.py:173 konova/forms.py:190 +#: compensation/forms/modalForms.py:215 konova/forms.py:193 msgid "Object removed" msgstr "Objekt entfernt" -#: compensation/forms/modalForms.py:227 +#: compensation/forms/modalForms.py:303 msgid "Deadline Type" msgstr "Fristart" -#: compensation/forms/modalForms.py:230 +#: compensation/forms/modalForms.py:306 msgid "Select the deadline type" msgstr "Fristart wählen" -#: compensation/forms/modalForms.py:239 +#: compensation/forms/modalForms.py:315 #: compensation/templates/compensation/detail/compensation/includes/deadlines.html:31 #: compensation/templates/compensation/detail/eco_account/includes/deadlines.html:31 #: ema/templates/ema/detail/includes/deadlines.html:31 -#: intervention/forms/modalForms.py:123 +#: intervention/forms/modalForms.py:126 msgid "Date" msgstr "Datum" -#: compensation/forms/modalForms.py:242 +#: compensation/forms/modalForms.py:318 msgid "Select date" msgstr "Datum wählen" -#: compensation/forms/modalForms.py:269 +#: compensation/forms/modalForms.py:345 msgid "New deadline" msgstr "Neue Frist" -#: compensation/forms/modalForms.py:270 +#: compensation/forms/modalForms.py:346 msgid "Insert data for the new deadline" msgstr "Geben Sie die Daten der neuen Frist ein" -#: compensation/forms/modalForms.py:288 +#: compensation/forms/modalForms.py:363 msgid "Action Type" msgstr "Maßnahmentyp" -#: compensation/forms/modalForms.py:291 +#: compensation/forms/modalForms.py:366 msgid "Select the action type" msgstr "Maßnahmentyp wählen" -#: compensation/forms/modalForms.py:300 +#: compensation/forms/modalForms.py:375 #: compensation/templates/compensation/detail/compensation/includes/actions.html:40 #: compensation/templates/compensation/detail/compensation/includes/deadlines.html:39 #: compensation/templates/compensation/detail/compensation/includes/documents.html:36 @@ -583,69 +582,65 @@ msgstr "Maßnahmentyp wählen" #: ema/templates/ema/detail/includes/states-before.html:40 #: intervention/templates/intervention/detail/includes/compensations.html:38 #: intervention/templates/intervention/detail/includes/deductions.html:39 -#: intervention/templates/intervention/detail/includes/documents.html:36 +#: intervention/templates/intervention/detail/includes/documents.html:39 #: intervention/templates/intervention/detail/includes/payments.html:39 #: intervention/templates/intervention/detail/includes/revocation.html:43 #: templates/log.html:10 msgid "Action" msgstr "Aktionen" -#: compensation/forms/modalForms.py:305 compensation/forms/modalForms.py:317 +#: compensation/forms/modalForms.py:380 compensation/forms/modalForms.py:392 msgid "Action Type detail" msgstr "Zusatzmerkmal" -#: compensation/forms/modalForms.py:308 +#: compensation/forms/modalForms.py:383 msgid "Select the action type detail" msgstr "Zusatzmerkmal wählen" -#: compensation/forms/modalForms.py:322 +#: compensation/forms/modalForms.py:397 msgid "Unit" msgstr "Einheit" -#: compensation/forms/modalForms.py:325 +#: compensation/forms/modalForms.py:400 msgid "Select the unit" msgstr "Einheit wählen" -#: compensation/forms/modalForms.py:337 +#: compensation/forms/modalForms.py:412 msgid "Insert the amount" msgstr "Menge eingeben" -#: compensation/forms/modalForms.py:362 +#: compensation/forms/modalForms.py:437 msgid "New action" msgstr "Neue Maßnahme" -#: compensation/forms/modalForms.py:363 +#: compensation/forms/modalForms.py:438 msgid "Insert data for the new action" msgstr "Geben Sie die Daten der neuen Maßnahme ein" -#: compensation/models/action.py:21 +#: compensation/models/action.py:22 msgid "cm" msgstr "" -#: compensation/models/action.py:22 +#: compensation/models/action.py:23 msgid "m" msgstr "" -#: compensation/models/action.py:23 +#: compensation/models/action.py:24 msgid "km" msgstr "" -#: compensation/models/action.py:24 +#: compensation/models/action.py:25 msgid "m²" msgstr "" -#: compensation/models/action.py:25 +#: compensation/models/action.py:26 msgid "ha" msgstr "" -#: compensation/models/action.py:26 +#: compensation/models/action.py:27 msgid "Pieces" msgstr "Stück" -#: compensation/models/compensation.py:63 konova/utils/message_templates.py:43 -msgid "Added deadline" -msgstr "Frist/Termin hinzugefügt" - #: compensation/models/eco_account.py:56 msgid "" "Deductable surface can not be larger than existing surfaces in after states" @@ -661,65 +656,70 @@ msgstr "" "Es wurde bereits mehr Fläche abgebucht, als Sie nun als abbuchbar einstellen " "wollen. Kontaktieren Sie die für die Abbuchungen verantwortlichen Nutzer!" -#: compensation/tables.py:47 compensation/tables.py:188 ema/tables.py:44 +#: compensation/tables.py:35 compensation/tables.py:207 ema/tables.py:39 +#: intervention/tables.py:34 konova/filters/mixins.py:98 +msgid "Parcel gmrkng" +msgstr "Gemarkung" + +#: compensation/tables.py:52 compensation/tables.py:228 ema/tables.py:50 #: intervention/tables.py:51 msgid "Editable" msgstr "Freigegeben" -#: compensation/tables.py:53 compensation/tables.py:194 ema/tables.py:50 +#: compensation/tables.py:58 compensation/tables.py:234 ema/tables.py:56 #: intervention/tables.py:57 msgid "Last edit" msgstr "Zuletzt bearbeitet" -#: compensation/tables.py:85 compensation/tables.py:226 ema/tables.py:83 -#: intervention/tables.py:89 +#: compensation/tables.py:89 compensation/tables.py:266 ema/tables.py:89 +#: intervention/tables.py:88 msgid "Open {}" msgstr "Öffne {}" -#: compensation/tables.py:106 intervention/tables.py:108 +#: compensation/tables.py:114 intervention/tables.py:111 msgid "Not checked yet" msgstr "Noch nicht geprüft" -#: compensation/tables.py:111 intervention/tables.py:113 +#: compensation/tables.py:119 intervention/tables.py:116 msgid "Checked on {} by {}" msgstr "Am {} von {} geprüft worden" -#: compensation/tables.py:130 +#: compensation/tables.py:160 #: compensation/templates/compensation/detail/compensation/view.html:80 #: compensation/templates/compensation/detail/eco_account/includes/deductions.html:58 #: compensation/templates/compensation/detail/eco_account/view.html:47 -#: ema/tables.py:102 ema/templates/ema/detail/view.html:38 -#: intervention/tables.py:132 +#: ema/tables.py:131 ema/templates/ema/detail/view.html:38 +#: intervention/tables.py:157 #: intervention/templates/intervention/detail/view.html:85 msgid "Not recorded yet" msgstr "Noch nicht verzeichnet" -#: compensation/tables.py:135 compensation/tables.py:264 ema/tables.py:107 -#: intervention/tables.py:137 +#: compensation/tables.py:165 compensation/tables.py:326 ema/tables.py:136 +#: intervention/tables.py:162 msgid "Recorded on {} by {}" msgstr "Am {} von {} verzeichnet worden" -#: compensation/tables.py:159 compensation/tables.py:286 ema/tables.py:130 -#: intervention/tables.py:160 +#: compensation/tables.py:189 compensation/tables.py:348 ema/tables.py:159 +#: intervention/tables.py:185 msgid "Full access granted" msgstr "Für Sie freigegeben - Datensatz kann bearbeitet werden" -#: compensation/tables.py:159 compensation/tables.py:286 ema/tables.py:130 -#: intervention/tables.py:160 +#: compensation/tables.py:189 compensation/tables.py:348 ema/tables.py:159 +#: intervention/tables.py:185 msgid "Access not granted" msgstr "Nicht freigegeben - Datensatz nur lesbar" -#: compensation/tables.py:177 +#: compensation/tables.py:212 #: compensation/templates/compensation/detail/eco_account/view.html:35 #: konova/templates/konova/widgets/progressbar.html:3 msgid "Available" msgstr "Verfügbar" -#: compensation/tables.py:203 +#: compensation/tables.py:243 msgid "Eco Accounts" msgstr "Ökokonten" -#: compensation/tables.py:259 +#: compensation/tables.py:321 msgid "Not recorded yet. Can not be used for deductions, yet." msgstr "" "Noch nicht verzeichnet. Kann noch nicht für Abbuchungen genutzt werden." @@ -821,14 +821,14 @@ msgstr "Dokumente" #: compensation/templates/compensation/detail/eco_account/includes/documents.html:14 #: ema/templates/ema/detail/includes/documents.html:14 #: intervention/templates/intervention/detail/includes/documents.html:14 -#: konova/forms.py:391 +#: konova/forms.py:411 msgid "Add new document" msgstr "Neues Dokument hinzufügen" #: compensation/templates/compensation/detail/compensation/includes/documents.html:57 #: compensation/templates/compensation/detail/eco_account/includes/documents.html:55 #: ema/templates/ema/detail/includes/documents.html:55 -#: intervention/templates/intervention/detail/includes/documents.html:57 +#: intervention/templates/intervention/detail/includes/documents.html:68 msgid "Remove document" msgstr "Dokument löschen" @@ -943,14 +943,14 @@ msgstr "Zuletzt bearbeitet" #: compensation/templates/compensation/detail/compensation/view.html:99 #: compensation/templates/compensation/detail/eco_account/view.html:82 -#: ema/templates/ema/detail/view.html:76 intervention/forms/modalForms.py:53 +#: ema/templates/ema/detail/view.html:76 intervention/forms/modalForms.py:56 #: intervention/templates/intervention/detail/view.html:116 msgid "Shared with" msgstr "Freigegeben für" #: compensation/templates/compensation/detail/eco_account/includes/controls.html:15 #: ema/templates/ema/detail/includes/controls.html:15 -#: intervention/forms/modalForms.py:67 +#: intervention/forms/modalForms.py:70 #: intervention/templates/intervention/detail/includes/controls.html:15 msgid "Share" msgstr "Freigabe" @@ -993,6 +993,11 @@ msgstr "Verzeichnet am" #: compensation/templates/compensation/detail/eco_account/includes/deductions.html:65 #: intervention/templates/intervention/detail/includes/deductions.html:60 +msgid "Edit Deduction" +msgstr "Abbuchung bearbeiten" + +#: compensation/templates/compensation/detail/eco_account/includes/deductions.html:68 +#: intervention/templates/intervention/detail/includes/deductions.html:63 msgid "Remove Deduction" msgstr "Abbuchung entfernen" @@ -1076,111 +1081,76 @@ msgstr "" msgid "Responsible data" msgstr "Daten zu den verantwortlichen Stellen" -#: compensation/views/compensation.py:48 +#: compensation/views/compensation.py:50 msgid "Compensations - Overview" msgstr "Kompensationen - Übersicht" -#: compensation/views/compensation.py:147 +#: compensation/views/compensation.py:149 konova/utils/message_templates.py:31 msgid "Compensation {} edited" msgstr "Kompensation {} bearbeitet" -#: compensation/views/compensation.py:157 compensation/views/eco_account.py:160 -#: ema/views.py:227 intervention/views.py:311 +#: compensation/views/compensation.py:159 compensation/views/eco_account.py:163 +#: ema/views.py:230 intervention/views.py:337 msgid "Edit {}" msgstr "Bearbeite {}" -#: compensation/views/compensation.py:236 compensation/views/eco_account.py:316 -#: ema/views.py:188 intervention/views.py:487 +#: compensation/views/compensation.py:238 compensation/views/eco_account.py:347 +#: ema/views.py:191 intervention/views.py:541 msgid "Log" msgstr "Log" -#: compensation/views/compensation.py:280 compensation/views/eco_account.py:496 -#: ema/views.py:359 intervention/views.py:133 -msgid "Document added" -msgstr "Dokument hinzugefügt" - -#: compensation/views/compensation.py:350 compensation/views/eco_account.py:362 -#: ema/views.py:294 -msgid "State added" -msgstr "Zustand hinzugefügt" - -#: compensation/views/compensation.py:372 compensation/views/eco_account.py:384 -#: ema/views.py:316 -msgid "Action added" -msgstr "Maßnahme hinzugefügt" - -#: compensation/views/compensation.py:394 compensation/views/eco_account.py:475 -#: ema/views.py:338 -msgid "Deadline added" -msgstr "Frist/Termin hinzugefügt" - -#: compensation/views/compensation.py:417 compensation/views/eco_account.py:453 -#: ema/views.py:595 -msgid "Deadline removed" -msgstr "Frist gelöscht" - -#: compensation/views/compensation.py:440 compensation/views/eco_account.py:407 -#: ema/views.py:430 -msgid "State removed" -msgstr "Zustand gelöscht" - -#: compensation/views/compensation.py:463 compensation/views/eco_account.py:430 -#: ema/views.py:453 -msgid "Action removed" -msgstr "Maßnahme entfernt" - -#: compensation/views/compensation.py:482 compensation/views/eco_account.py:586 -#: ema/views.py:472 intervention/views.py:580 +#: compensation/views/compensation.py:487 compensation/views/eco_account.py:620 +#: ema/views.py:477 intervention/views.py:687 msgid "Report {}" msgstr "Bericht {}" -#: compensation/views/eco_account.py:59 +#: compensation/views/eco_account.py:62 msgid "Eco-account - Overview" msgstr "Ökokonten - Übersicht" -#: compensation/views/eco_account.py:92 +#: compensation/views/eco_account.py:95 msgid "Eco-Account {} added" msgstr "Ökokonto {} hinzugefügt" -#: compensation/views/eco_account.py:150 +#: compensation/views/eco_account.py:153 msgid "Eco-Account {} edited" msgstr "Ökokonto {} bearbeitet" -#: compensation/views/eco_account.py:263 +#: compensation/views/eco_account.py:266 msgid "Eco-account removed" msgstr "Ökokonto entfernt" -#: compensation/views/eco_account.py:337 ema/views.py:269 -#: intervention/views.py:558 +#: compensation/views/eco_account.py:368 ema/views.py:272 +#: intervention/views.py:640 msgid "{} unrecorded" msgstr "{} entzeichnet" -#: compensation/views/eco_account.py:337 ema/views.py:269 -#: intervention/views.py:558 +#: compensation/views/eco_account.py:368 ema/views.py:272 +#: intervention/views.py:640 msgid "{} recorded" msgstr "{} verzeichnet" -#: compensation/views/eco_account.py:659 ema/views.py:538 -#: intervention/views.py:384 +#: compensation/views/eco_account.py:693 ema/views.py:543 +#: intervention/views.py:438 msgid "{} has already been shared with you" msgstr "{} wurde bereits für Sie freigegeben" -#: compensation/views/eco_account.py:664 ema/views.py:543 -#: intervention/views.py:389 +#: compensation/views/eco_account.py:698 ema/views.py:548 +#: intervention/views.py:443 msgid "{} has been shared with you" msgstr "{} ist nun für Sie freigegeben" -#: compensation/views/eco_account.py:671 ema/views.py:550 -#: intervention/views.py:396 +#: compensation/views/eco_account.py:705 ema/views.py:555 +#: intervention/views.py:450 msgid "Share link invalid" msgstr "Freigabelink ungültig" -#: compensation/views/eco_account.py:694 ema/views.py:573 -#: intervention/views.py:419 +#: compensation/views/eco_account.py:728 ema/views.py:578 +#: intervention/views.py:473 msgid "Share settings updated" msgstr "Freigabe Einstellungen aktualisiert" -#: ema/forms.py:40 ema/views.py:92 +#: ema/forms.py:40 ema/views.py:95 msgid "New EMA" msgstr "Neue EMA hinzufügen" @@ -1188,11 +1158,11 @@ msgstr "Neue EMA hinzufügen" msgid "Edit EMA" msgstr "Bearbeite EMA" -#: ema/tables.py:59 templates/navbars/navbar.html:43 +#: ema/tables.py:65 templates/navbars/navbar.html:43 msgid "Payment funded compensations" msgstr "Ersatzzahlungsmaßnahmen (EMA)" -#: ema/tables.py:60 +#: ema/tables.py:66 msgid "EMA explanation" msgstr "" "EMA sind Kompensationen, die durch Ersatzzahlungen finanziert wurden. " @@ -1200,7 +1170,7 @@ msgstr "" "Maßnahmen aus Ersatzzahlungen, die nach 2015 rechtskräftig wurden, werden " "durch die Stiftung Natur und Umwelt verwaltet." -#: ema/tables.py:83 templates/navbars/navbar.html:43 +#: ema/tables.py:89 templates/navbars/navbar.html:43 msgid "EMA" msgstr "" @@ -1208,19 +1178,19 @@ msgstr "" msgid "Payment funded compensation" msgstr "Ersatzzahlungsmaßnahme" -#: ema/views.py:49 +#: ema/views.py:52 msgid "EMAs - Overview" msgstr "EMAs - Übersicht" -#: ema/views.py:82 +#: ema/views.py:85 msgid "EMA {} added" msgstr "EMA {} hinzugefügt" -#: ema/views.py:217 +#: ema/views.py:220 msgid "EMA {} edited" msgstr "EMA {} bearbeitet" -#: ema/views.py:250 +#: ema/views.py:253 msgid "EMA removed" msgstr "EMA entfernt" @@ -1281,7 +1251,7 @@ msgstr "Datum Zulassung bzw. Satzungsbeschluss" msgid "Binding on" msgstr "Datum Bestandskraft" -#: intervention/forms/forms.py:193 intervention/views.py:92 +#: intervention/forms/forms.py:193 intervention/views.py:94 msgid "New intervention" msgstr "Neuer Eingriff" @@ -1289,19 +1259,19 @@ msgstr "Neuer Eingriff" msgid "Edit intervention" msgstr "Eingriff bearbeiten" -#: intervention/forms/modalForms.py:26 +#: intervention/forms/modalForms.py:29 msgid "Share link" msgstr "Freigabelink" -#: intervention/forms/modalForms.py:28 +#: intervention/forms/modalForms.py:31 msgid "Send this link to users who you want to have writing access on the data" msgstr "Andere Nutzer erhalten über diesen Link Zugriff auf die Daten" -#: intervention/forms/modalForms.py:38 +#: intervention/forms/modalForms.py:41 msgid "Add user to share with" msgstr "Nutzer direkt hinzufügen" -#: intervention/forms/modalForms.py:40 +#: intervention/forms/modalForms.py:43 msgid "" "Multiple selection possible - You can only select users which do not already " "have access. Enter the full username." @@ -1309,46 +1279,46 @@ msgstr "" "Mehrfachauswahl möglich - Sie können nur Nutzer wählen, für die der Eintrag " "noch nicht freigegeben wurde. Geben Sie den ganzen Nutzernamen an." -#: intervention/forms/modalForms.py:56 +#: intervention/forms/modalForms.py:59 msgid "Remove check to remove access for this user" msgstr "Wählen Sie die Nutzer ab, die keinen Zugriff mehr haben sollen" -#: intervention/forms/modalForms.py:68 +#: intervention/forms/modalForms.py:71 msgid "Share settings for {}" msgstr "Freigabe Einstellungen für {}" -#: intervention/forms/modalForms.py:125 +#: intervention/forms/modalForms.py:128 msgid "Date of revocation" msgstr "Datum des Widerspruchs" -#: intervention/forms/modalForms.py:136 +#: intervention/forms/modalForms.py:139 #: intervention/templates/intervention/detail/includes/revocation.html:35 msgid "Document" msgstr "Dokument" -#: intervention/forms/modalForms.py:139 +#: intervention/forms/modalForms.py:142 msgid "Must be smaller than 15 Mb" msgstr "Muss kleiner als 15 Mb sein" -#: intervention/forms/modalForms.py:163 +#: intervention/forms/modalForms.py:167 #: intervention/templates/intervention/detail/includes/revocation.html:18 msgid "Add revocation" msgstr "Widerspruch hinzufügen" -#: intervention/forms/modalForms.py:180 +#: intervention/forms/modalForms.py:224 msgid "Checked intervention data" msgstr "Eingriffsdaten geprüft" -#: intervention/forms/modalForms.py:186 +#: intervention/forms/modalForms.py:230 msgid "Checked compensations data and payments" msgstr "Kompensationen und Zahlungen geprüft" -#: intervention/forms/modalForms.py:195 +#: intervention/forms/modalForms.py:239 #: intervention/templates/intervention/detail/includes/controls.html:19 msgid "Run check" msgstr "Prüfung vornehmen" -#: intervention/forms/modalForms.py:196 konova/forms.py:457 +#: intervention/forms/modalForms.py:240 konova/forms.py:514 msgid "" "I, {} {}, confirm that all necessary control steps have been performed by " "myself." @@ -1356,23 +1326,23 @@ msgstr "" "Ich, {} {}, bestätige, dass die notwendigen Kontrollschritte durchgeführt " "wurden:" -#: intervention/forms/modalForms.py:280 +#: intervention/forms/modalForms.py:324 msgid "Only recorded accounts can be selected for deductions" msgstr "Nur verzeichnete Ökokonten können für Abbuchungen verwendet werden." -#: intervention/forms/modalForms.py:307 +#: intervention/forms/modalForms.py:351 msgid "Only shared interventions can be selected" msgstr "Nur freigegebene Eingriffe können gewählt werden" -#: intervention/forms/modalForms.py:320 +#: intervention/forms/modalForms.py:364 msgid "New Deduction" msgstr "Neue Abbuchung" -#: intervention/forms/modalForms.py:321 +#: intervention/forms/modalForms.py:365 msgid "Enter the information for a new deduction from a chosen eco-account" msgstr "Geben Sie die Informationen für eine neue Abbuchung ein." -#: intervention/forms/modalForms.py:349 +#: intervention/forms/modalForms.py:408 msgid "" "Eco-account {} is not recorded yet. You can only deduct from recorded " "accounts." @@ -1380,7 +1350,7 @@ msgstr "" "Ökokonto {} ist noch nicht verzeichnet. Abbuchungen können nur von " "verzeichneten Ökokonten erfolgen." -#: intervention/forms/modalForms.py:362 +#: intervention/forms/modalForms.py:418 msgid "" "The account {} has not enough surface for a deduction of {} m². There are " "only {} m² left" @@ -1388,19 +1358,6 @@ msgstr "" "Das Ökokonto {} hat für eine Abbuchung von {} m² nicht ausreichend " "Restfläche. Es stehen noch {} m² zur Verfügung." -#: intervention/tables.py:45 -#: intervention/templates/intervention/detail/includes/revocation.html:58 -msgid "Revocation" -msgstr "Widerspruch" - -#: intervention/tables.py:177 -msgid "No revocation" -msgstr "Kein Widerspruch" - -#: intervention/tables.py:183 -msgid "Revocation from {}, added on {} by {}" -msgstr "Widerspruch vom {}, am {} von {} hinzugefügt" - #: intervention/templates/intervention/detail/includes/compensations.html:14 msgid "Add new compensation" msgstr "Neue Kompensation hinzufügen" @@ -1421,6 +1378,15 @@ msgstr "Ökokonto gelöscht! Abbuchung ungültig!" msgid "Eco-account not recorded! Deduction invalid!" msgstr "Ökokonto nicht verzeichnet! Abbuchung ungültig!" +#: intervention/templates/intervention/detail/includes/documents.html:31 +#: konova/forms.py:368 +msgid "Created on" +msgstr "Erstellt" + +#: intervention/templates/intervention/detail/includes/documents.html:65 +msgid "Edit document" +msgstr "Dokument bearbeitet" + #: intervention/templates/intervention/detail/includes/payments.html:8 #: intervention/templates/intervention/report/report.html:73 msgid "Payments" @@ -1436,6 +1402,10 @@ msgid "Amount" msgstr "Betrag" #: intervention/templates/intervention/detail/includes/payments.html:59 +msgid "Edit payment" +msgstr "Zahlung bearbeitet" + +#: intervention/templates/intervention/detail/includes/payments.html:62 msgid "Remove payment" msgstr "Zahlung entfernen" @@ -1449,7 +1419,15 @@ msgctxt "Revocation" msgid "From" msgstr "Vom" +#: intervention/templates/intervention/detail/includes/revocation.html:58 +msgid "Revocation" +msgstr "Widerspruch" + #: intervention/templates/intervention/detail/includes/revocation.html:69 +msgid "Edit revocation" +msgstr "Widerspruch bearbeiten" + +#: intervention/templates/intervention/detail/includes/revocation.html:72 msgid "Remove revocation" msgstr "Widerspruch entfernen" @@ -1466,6 +1444,7 @@ msgid "Exist" msgstr "Vorhanden" #: intervention/utils/quality.py:70 +#: templates/table/revocation_warning_col.html:5 msgid "Revocations exists" msgstr "Widersprüche liegen vor" @@ -1477,37 +1456,33 @@ msgstr "Datum Bestandskraft" msgid "Laws" msgstr "Gesetze" -#: intervention/utils/quality.py:98 +#: intervention/utils/quality.py:101 msgid "No compensation of any type found (Compensation, Payment, Deduction)" msgstr "" "Kein Ausgleich jeglicher Art gefunden (Kompensation, Ersatzzahlung, " "Abbuchung)" -#: intervention/views.py:49 +#: intervention/views.py:51 msgid "Interventions - Overview" msgstr "Eingriffe - Übersicht" -#: intervention/views.py:82 +#: intervention/views.py:84 msgid "Intervention {} added" msgstr "Eingriff {} hinzugefügt" -#: intervention/views.py:250 -msgid "This intervention has {} revocations" -msgstr "Dem Eingriff liegen {} Widersprüche vor" - -#: intervention/views.py:299 +#: intervention/views.py:325 msgid "Intervention {} edited" msgstr "Eingriff {} bearbeitet" -#: intervention/views.py:335 +#: intervention/views.py:361 msgid "{} removed" msgstr "{} entfernt" -#: intervention/views.py:440 +#: intervention/views.py:494 msgid "Check performed" msgstr "Prüfung durchgeführt" -#: intervention/views.py:563 +#: intervention/views.py:645 msgid "There are errors on this intervention:" msgstr "Es liegen Fehler in diesem Eingriff vor:" @@ -1544,10 +1519,6 @@ msgstr "Kreis" msgid "Search for district" msgstr "Nach Kreis suchen" -#: konova/filters/mixins.py:98 -msgid "Parcel gmrkng" -msgstr "Gemarkung" - #: konova/filters/mixins.py:99 msgid "Search for parcel gmrkng" msgstr "Nach Gemarkung suchen" @@ -1595,81 +1566,69 @@ msgstr "Nach Zulassungsbehörde suchen" msgid "Search for conservation office" msgstr "Nch Eintragungsstelle suchen" -#: konova/forms.py:37 templates/form/collapsable/form.html:62 +#: konova/forms.py:39 templates/form/collapsable/form.html:62 msgid "Save" msgstr "Speichern" -#: konova/forms.py:69 +#: konova/forms.py:71 msgid "Not editable" msgstr "Nicht editierbar" -#: konova/forms.py:139 konova/forms.py:311 +#: konova/forms.py:142 konova/forms.py:314 msgid "Confirm" msgstr "Bestätige" -#: konova/forms.py:151 konova/forms.py:320 +#: konova/forms.py:154 konova/forms.py:323 msgid "Remove" msgstr "Löschen" -#: konova/forms.py:153 +#: konova/forms.py:156 msgid "You are about to remove {} {}" msgstr "Sie sind dabei {} {} zu löschen" -#: konova/forms.py:240 konova/utils/quality.py:44 konova/utils/quality.py:46 +#: konova/forms.py:243 konova/utils/quality.py:44 konova/utils/quality.py:46 #: templates/form/collapsable/form.html:45 msgid "Geometry" msgstr "Geometrie" -#: konova/forms.py:321 +#: konova/forms.py:324 msgid "Are you sure?" msgstr "Sind Sie sicher?" -#: konova/forms.py:348 -msgid "Created on" -msgstr "Erstellt" - -#: konova/forms.py:350 +#: konova/forms.py:370 msgid "When has this file been created? Important for photos." msgstr "Wann wurde diese Datei erstellt oder das Foto aufgenommen?" -#: konova/forms.py:361 +#: konova/forms.py:381 #: venv/lib/python3.7/site-packages/django/db/models/fields/files.py:231 msgid "File" msgstr "Datei" -#: konova/forms.py:363 +#: konova/forms.py:383 msgid "Allowed formats: pdf, jpg, png. Max size 15 MB." msgstr "Formate: pdf, jpg, png. Maximal 15 MB." -#: konova/forms.py:409 -msgid "Unsupported file type" -msgstr "Dateiformat nicht unterstützt" - -#: konova/forms.py:416 -msgid "File too large" -msgstr "Datei zu groß" - -#: konova/forms.py:425 +#: konova/forms.py:449 msgid "Added document" msgstr "Dokument hinzugefügt" -#: konova/forms.py:448 +#: konova/forms.py:505 msgid "Confirm record" msgstr "Verzeichnen bestätigen" -#: konova/forms.py:456 +#: konova/forms.py:513 msgid "Record data" msgstr "Daten verzeichnen" -#: konova/forms.py:463 +#: konova/forms.py:520 msgid "Confirm unrecord" msgstr "Entzeichnen bestätigen" -#: konova/forms.py:464 +#: konova/forms.py:521 msgid "Unrecord data" msgstr "Daten entzeichnen" -#: konova/forms.py:465 +#: konova/forms.py:522 msgid "I, {} {}, confirm that this data must be unrecorded." msgstr "" "Ich, {} {}, bestätige, dass diese Daten wieder entzeichnet werden müssen." @@ -1772,35 +1731,31 @@ msgstr "In Zwischenablage kopieren" msgid "Copied to clipboard" msgstr "In Zwischenablage kopiert" -#: konova/utils/documents.py:52 -msgid "Document '{}' deleted" -msgstr "Dokument '{}' gelöscht" - -#: konova/utils/mailer.py:66 +#: konova/utils/mailer.py:68 msgid "{} - Shared access removed" msgstr "{} - Zugriff entzogen" -#: konova/utils/mailer.py:88 +#: konova/utils/mailer.py:91 msgid "{} - Shared access given" msgstr "{} - Zugriff freigegeben" -#: konova/utils/mailer.py:110 +#: konova/utils/mailer.py:114 msgid "{} - Shared data recorded" msgstr "{} - Freigegebene Daten verzeichnet" -#: konova/utils/mailer.py:132 +#: konova/utils/mailer.py:137 msgid "{} - Shared data unrecorded" msgstr "{} - Freigegebene Daten entzeichnet" -#: konova/utils/mailer.py:154 +#: konova/utils/mailer.py:160 msgid "{} - Shared data deleted" msgstr "{} - Freigegebene Daten gelöscht" -#: konova/utils/mailer.py:176 +#: konova/utils/mailer.py:183 msgid "{} - Shared data checked" msgstr "{} - Freigegebene Daten geprüft" -#: konova/utils/mailer.py:197 templates/email/api/verify_token.html:4 +#: konova/utils/mailer.py:204 templates/email/api/verify_token.html:4 msgid "Request for new API token" msgstr "Anfrage für neuen API Token" @@ -1847,6 +1802,14 @@ msgid "Status of Checked and Recorded reseted" msgstr "'Geprüft'/'Verzeichnet' wurde zurückgesetzt" #: konova/utils/message_templates.py:22 +msgid "Unsupported file type" +msgstr "Dateiformat nicht unterstützt" + +#: konova/utils/message_templates.py:23 +msgid "File too large" +msgstr "Datei zu groß" + +#: konova/utils/message_templates.py:26 msgid "" "Action canceled. Eco account is recorded or deductions exist. Only " "conservation office member can perform this action." @@ -1854,54 +1817,122 @@ msgstr "" "Aktion abgebrochen. Ökokonto ist bereits verzeichnet oder Abbuchungen liegen " "vor. Nur Eintragungsstellennutzer können diese Aktion jetzt durchführen." -#: konova/utils/message_templates.py:25 +#: konova/utils/message_templates.py:29 msgid "Compensation {} added" msgstr "Kompensation {} hinzugefügt" -#: konova/utils/message_templates.py:26 +#: konova/utils/message_templates.py:30 msgid "Compensation {} removed" msgstr "Kompensation {} entfernt" -#: konova/utils/message_templates.py:29 -msgid "Deduction added" -msgstr "Abbuchung hinzugefügt" - -#: konova/utils/message_templates.py:30 -msgid "Deduction removed" -msgstr "Abbuchung entfernt" +#: konova/utils/message_templates.py:32 +msgid "Added compensation action" +msgstr "Maßnahme hinzugefügt" #: konova/utils/message_templates.py:33 -msgid "Payment added" -msgstr "Zahlung hinzugefügt" - -#: konova/utils/message_templates.py:34 -msgid "Payment removed" -msgstr "Zahlung gelöscht" - -#: konova/utils/message_templates.py:37 -msgid "Revocation added" -msgstr "Widerspruch hinzugefügt" - -#: konova/utils/message_templates.py:38 -msgid "Revocation removed" -msgstr "Widerspruch entfernt" - -#: konova/utils/message_templates.py:41 -msgid "Edited general data" -msgstr "Allgemeine Daten bearbeitet" - -#: konova/utils/message_templates.py:42 msgid "Added compensation state" msgstr "Zustand hinzugefügt" -#: konova/utils/message_templates.py:44 -msgid "Added compensation action" -msgstr "Maßnahme hinzufügen" +#: konova/utils/message_templates.py:36 +msgid "State removed" +msgstr "Zustand gelöscht" + +#: konova/utils/message_templates.py:37 +msgid "State edited" +msgstr "Zustand bearbeitet" + +#: konova/utils/message_templates.py:38 +msgid "State added" +msgstr "Zustand hinzugefügt" + +#: konova/utils/message_templates.py:41 +msgid "Action added" +msgstr "Maßnahme hinzugefügt" + +#: konova/utils/message_templates.py:42 +msgid "Action edited" +msgstr "Maßnahme bearbeitet" + +#: konova/utils/message_templates.py:43 +msgid "Action removed" +msgstr "Maßnahme entfernt" + +#: konova/utils/message_templates.py:46 +msgid "Deduction added" +msgstr "Abbuchung hinzugefügt" #: konova/utils/message_templates.py:47 +msgid "Deduction edited" +msgstr "Abbuchung bearbeitet" + +#: konova/utils/message_templates.py:48 +msgid "Deduction removed" +msgstr "Abbuchung entfernt" + +#: konova/utils/message_templates.py:51 +msgid "Deadline added" +msgstr "Frist/Termin hinzugefügt" + +#: konova/utils/message_templates.py:52 +msgid "Deadline edited" +msgstr "Frist/Termin bearbeitet" + +#: konova/utils/message_templates.py:53 +msgid "Deadline removed" +msgstr "Frist/Termin gelöscht" + +#: konova/utils/message_templates.py:56 +msgid "Payment added" +msgstr "Zahlung hinzugefügt" + +#: konova/utils/message_templates.py:57 +msgid "Payment edited" +msgstr "Zahlung bearbeitet" + +#: konova/utils/message_templates.py:58 +msgid "Payment removed" +msgstr "Zahlung gelöscht" + +#: konova/utils/message_templates.py:61 +msgid "Revocation added" +msgstr "Widerspruch hinzugefügt" + +#: konova/utils/message_templates.py:62 +msgid "Revocation edited" +msgstr "Widerspruch bearbeitet" + +#: konova/utils/message_templates.py:63 +msgid "Revocation removed" +msgstr "Widerspruch entfernt" + +#: konova/utils/message_templates.py:66 +msgid "Document '{}' deleted" +msgstr "Dokument '{}' gelöscht" + +#: konova/utils/message_templates.py:67 +msgid "Document added" +msgstr "Dokument hinzugefügt" + +#: konova/utils/message_templates.py:68 +msgid "Document edited" +msgstr "Dokument bearbeitet" + +#: konova/utils/message_templates.py:71 +msgid "Edited general data" +msgstr "Allgemeine Daten bearbeitet" + +#: konova/utils/message_templates.py:72 +msgid "Added deadline" +msgstr "Frist/Termin hinzugefügt" + +#: konova/utils/message_templates.py:75 msgid "Geometry conflict detected with {}" msgstr "Geometriekonflikt mit folgenden Einträgen erkannt: {}" +#: konova/utils/message_templates.py:78 +msgid "This intervention has {} revocations" +msgstr "Dem Eingriff liegen {} Widersprüche vor" + #: konova/utils/messenger.py:70 msgid "{} checked" msgstr "{} geprüft" @@ -1984,12 +2015,12 @@ msgstr "" "Admin Backend aktiviert worden ist." #: templates/email/api/verify_token.html:18 -#: templates/email/checking/shared_data_checked.html:17 -#: templates/email/deleting/shared_data_deleted.html:17 -#: templates/email/recording/shared_data_recorded.html:17 -#: templates/email/recording/shared_data_unrecorded.html:17 -#: templates/email/sharing/shared_access_given.html:18 -#: templates/email/sharing/shared_access_removed.html:18 +#: templates/email/checking/shared_data_checked.html:19 +#: templates/email/deleting/shared_data_deleted.html:19 +#: templates/email/recording/shared_data_recorded.html:19 +#: templates/email/recording/shared_data_unrecorded.html:19 +#: templates/email/sharing/shared_access_given.html:20 +#: templates/email/sharing/shared_access_removed.html:20 msgid "Best regards" msgstr "Beste Grüße" @@ -2010,7 +2041,7 @@ msgstr "Hallo " msgid "the following dataset has just been checked" msgstr "der folgende Datensatz wurde soeben geprüft " -#: templates/email/checking/shared_data_checked.html:14 +#: templates/email/checking/shared_data_checked.html:16 msgid "" "This means, the responsible registration office just confirmed the " "correctness of this dataset." @@ -2026,7 +2057,7 @@ msgstr "Freigegebene Daten gelöscht" msgid "the following dataset has just been deleted" msgstr "der folgende Datensatz wurde soeben gelöscht " -#: templates/email/deleting/shared_data_deleted.html:14 +#: templates/email/deleting/shared_data_deleted.html:16 msgid "" "If this should not have been happened, please contact us. See the signature " "for details." @@ -2042,12 +2073,12 @@ msgstr "Freigegebene Daten verzeichnet" msgid "the following dataset has just been recorded" msgstr "der folgende Datensatz wurde soeben verzeichnet " -#: templates/email/recording/shared_data_recorded.html:14 +#: templates/email/recording/shared_data_recorded.html:16 msgid "This means the data is now publicly available, e.g. in LANIS" msgstr "" "Das bedeutet, dass die Daten nun öffentlich verfügbar sind, z.B. im LANIS." -#: templates/email/recording/shared_data_recorded.html:24 +#: templates/email/recording/shared_data_recorded.html:26 msgid "" "Please note: Recorded intervention means the compensations are recorded as " "well." @@ -2063,11 +2094,11 @@ msgstr "Freigegebene Daten entzeichnet" msgid "the following dataset has just been unrecorded" msgstr "der folgende Datensatz wurde soeben entzeichnet " -#: templates/email/recording/shared_data_unrecorded.html:14 +#: templates/email/recording/shared_data_unrecorded.html:16 msgid "This means the data is no longer publicly available." msgstr "Das bedeutet, dass die Daten nicht länger öffentlich verfügbar sind." -#: templates/email/recording/shared_data_unrecorded.html:24 +#: templates/email/recording/shared_data_unrecorded.html:26 msgid "" "Please note: Unrecorded intervention means the compensations are unrecorded " "as well." @@ -2083,11 +2114,11 @@ msgstr "Zugriff freigegeben" msgid "the following dataset has just been shared with you" msgstr "der folgende Datensatz wurde soeben für Sie freigegeben " -#: templates/email/sharing/shared_access_given.html:14 +#: templates/email/sharing/shared_access_given.html:16 msgid "This means you can now edit this dataset." msgstr "Das bedeutet, dass Sie diesen Datensatz nun auch bearbeiten können." -#: templates/email/sharing/shared_access_given.html:15 +#: templates/email/sharing/shared_access_given.html:17 msgid "" "The shared dataset appears now by default on your overview for this dataset " "type." @@ -2095,7 +2126,7 @@ msgstr "" "Der freigegebene Datensatz ist nun standardmäßig in Ihrer Übersicht für den " "Datensatztyp im KSP gelistet." -#: templates/email/sharing/shared_access_given.html:25 +#: templates/email/sharing/shared_access_given.html:27 msgid "" "Please note: Shared access on an intervention means you automatically have " "editing access to related compensations." @@ -2115,11 +2146,11 @@ msgstr "" "Ihnen wurde soeben der bearbeitende Zugriff auf den folgenden Datensatz " "entzogen: " -#: templates/email/sharing/shared_access_removed.html:14 +#: templates/email/sharing/shared_access_removed.html:16 msgid "However, you are still able to view the dataset content." msgstr "Sie können den Datensatz aber immer noch im KSP einsehen." -#: templates/email/sharing/shared_access_removed.html:15 +#: templates/email/sharing/shared_access_removed.html:17 msgid "" "Please use the provided search filter on the dataset`s overview pages to " "find them." @@ -2270,6 +2301,14 @@ msgstr "" "wieder vorbei. \n" " " +#: templates/table/gmrkng_col.html:6 +msgid "" +"If the geometry is not empty, the parcels are currently recalculated. Please " +"refresh this page in a few moments." +msgstr "" +"Falls die Geometrie nicht leer ist, werden die Flurstücke aktuell berechnet. " +"Bitte laden Sie diese Seite in ein paar Augenblicken erneut..." + #: user/forms.py:27 msgid "Notifications" msgstr "Benachrichtigungen" @@ -3918,6 +3957,12 @@ msgstr "" msgid "Unable to connect to qpid with SASL mechanism %s" msgstr "" +#~ msgid "No revocation" +#~ msgstr "Kein Widerspruch" + +#~ msgid "Revocation from {}, added on {} by {}" +#~ msgstr "Widerspruch vom {}, am {} von {} hinzugefügt" + #~ msgid "General data edited" #~ msgstr "Allgemeine Daten bearbeitet" @@ -3942,9 +3987,6 @@ msgstr "" #~ msgid "No file given!" #~ msgstr "Keine Datei angegeben!" -#~ msgid "Added payment" -#~ msgstr "Zahlung hinzufügen" - #~ msgid "Added state" #~ msgstr "Zustand hinzugefügt" diff --git a/templates/email/checking/shared_data_checked.html b/templates/email/checking/shared_data_checked.html index 0707cfbc..0b67ecc7 100644 --- a/templates/email/checking/shared_data_checked.html +++ b/templates/email/checking/shared_data_checked.html @@ -11,6 +11,8 @@
{{obj_identifier}}
+ {{obj_title}} +
{% trans 'This means, the responsible registration office just confirmed the correctness of this dataset.' %}

diff --git a/templates/email/deleting/shared_data_deleted.html b/templates/email/deleting/shared_data_deleted.html index b920f1ec..272b0fde 100644 --- a/templates/email/deleting/shared_data_deleted.html +++ b/templates/email/deleting/shared_data_deleted.html @@ -11,6 +11,8 @@
{{obj_identifier}}
+ "{{obj_title}}" +
{% trans 'If this should not have been happened, please contact us. See the signature for details.' %}

diff --git a/templates/email/recording/shared_data_recorded.html b/templates/email/recording/shared_data_recorded.html index f8db9182..6805c928 100644 --- a/templates/email/recording/shared_data_recorded.html +++ b/templates/email/recording/shared_data_recorded.html @@ -11,6 +11,8 @@
{{obj_identifier}}
+ "{{obj_title}}" +
{% trans 'This means the data is now publicly available, e.g. in LANIS' %}

diff --git a/templates/email/recording/shared_data_unrecorded.html b/templates/email/recording/shared_data_unrecorded.html index d4639c9e..1e0310ae 100644 --- a/templates/email/recording/shared_data_unrecorded.html +++ b/templates/email/recording/shared_data_unrecorded.html @@ -11,6 +11,8 @@
{{obj_identifier}}
+ "{{obj_title}}" +
{% trans 'This means the data is no longer publicly available.' %}

diff --git a/templates/email/sharing/shared_access_given.html b/templates/email/sharing/shared_access_given.html index b8b26b7a..140e7a88 100644 --- a/templates/email/sharing/shared_access_given.html +++ b/templates/email/sharing/shared_access_given.html @@ -11,6 +11,8 @@
{{obj_identifier}}
+ "{{obj_title}}" +
{% trans 'This means you can now edit this dataset.' %} {% trans 'The shared dataset appears now by default on your overview for this dataset type.' %}
diff --git a/templates/email/sharing/shared_access_removed.html b/templates/email/sharing/shared_access_removed.html index 86d4fddd..d1cbc5bf 100644 --- a/templates/email/sharing/shared_access_removed.html +++ b/templates/email/sharing/shared_access_removed.html @@ -11,6 +11,8 @@
{{obj_identifier}}
+ "{{obj_title}}" +
{% 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.' %}
diff --git a/templates/table/gmrkng_col.html b/templates/table/gmrkng_col.html new file mode 100644 index 00000000..bd8878de --- /dev/null +++ b/templates/table/gmrkng_col.html @@ -0,0 +1,9 @@ +{% load i18n fontawesome_5 %} + +{% for entry in entries %} + {{entry}} +{% empty %} + + {% fa5_icon 'hourglass-half' %} + +{% endfor %} \ No newline at end of file diff --git a/templates/table/revocation_warning_col.html b/templates/table/revocation_warning_col.html new file mode 100644 index 00000000..2ed0922a --- /dev/null +++ b/templates/table/revocation_warning_col.html @@ -0,0 +1,14 @@ +{% load i18n fontawesome_5 %} + +{% if has_revocations %} + + + {% fa5_icon 'ban' %} + {{content}} + + +{% else %} + + {{content}} + +{% endif %} \ No newline at end of file diff --git a/user/models/user.py b/user/models/user.py index 6370f2d5..df63dd76 100644 --- a/user/models/user.py +++ b/user/models/user.py @@ -60,11 +60,12 @@ class User(AbstractUser): name=ETS_GROUP ).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 Args: obj_identifier (): + obj_title (): Returns: @@ -72,9 +73,9 @@ class User(AbstractUser): notification_set = self.is_notification_setting_set(UserNotificationEnum.NOTIFY_ON_SHARED_ACCESS_REMOVED) if notification_set: 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 Args: @@ -86,9 +87,9 @@ class User(AbstractUser): notification_set = self.is_notification_setting_set(UserNotificationEnum.NOTIFY_ON_SHARED_ACCESS_GAINED) if notification_set: 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 Args: @@ -100,9 +101,9 @@ class User(AbstractUser): notification_set = self.is_notification_setting_set(UserNotificationEnum.NOTIFY_ON_SHARED_DATA_RECORDED) if notification_set: 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 Args: @@ -114,9 +115,9 @@ class User(AbstractUser): notification_set = self.is_notification_setting_set(UserNotificationEnum.NOTIFY_ON_SHARED_DATA_RECORDED) if notification_set: 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 Args: @@ -128,9 +129,9 @@ class User(AbstractUser): notification_set = self.is_notification_setting_set(UserNotificationEnum.NOTIFY_ON_SHARED_DATA_DELETED) if notification_set: 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 Args: @@ -142,7 +143,7 @@ class User(AbstractUser): notification_set = self.is_notification_setting_set(UserNotificationEnum.NOTIFY_ON_SHARED_DATA_CHECKED) if notification_set: 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): """ Getter for an API token