Compare commits

...

48 Commits
v0.1 ... v0.2

Author SHA1 Message Date
68b8ff07e9 Translation update
* updates translations
2022-02-10 13:31:51 +01:00
f8dedc6df1 Merge pull request '86_User_suggestions_and_feedback' (#111) from 86_User_suggestions_and_feedback into master
Reviewed-on: SGD-Nord/konova#111
2022-02-10 13:31:55 +01:00
fe29b7874e #86 Edit deadlines EcoAccount
* adds support for editing of deadlines in EcoAccount
* adds buttons and urls
2022-02-10 12:49:30 +01:00
e3c7a1a274 #86 Edit deadline Compensation
* adds support for editing of deadlines
* adds buttons and urls
* adds w-10 as base css-class for all action columns
2022-02-10 12:42:41 +01:00
3f7a6d416d #86 Edit deadlines EMA
* adds support for editing of EMA deadlines
* adds buttons and urls
2022-02-10 12:33:22 +01:00
cba174b762 #86 Edit view tests
* extends view tests
2022-02-10 11:45:55 +01:00
8d573e7390 #86 Edit actions compensation
* adds support for editing of CompensationAction for compensation
* adds buttons and urls
2022-02-10 11:31:13 +01:00
06f81d89c4 #86 Edit actions EMA
* adds support for editing of CompensationAction
* adds buttons and urls for EMA
2022-02-10 11:24:20 +01:00
a16e0af751 #86 Edit states EMA/EcoAccount
* adds support for editing of states for EMA and EcoAccount
* adds buttons and urls
2022-02-10 11:15:01 +01:00
0479f54a4d #86 Edit states compensation
* adds support for editing of states
* adds buttons and urls for compensation
2022-02-10 11:02:30 +01:00
aa616db1f0 #86 Edit document EcoAccount
* adds support for editing of documents
* adds buttons and urls for ecoaccount
2022-02-10 10:51:52 +01:00
a9bd92c57c #86 Edit document Compensation
* adds support for editing of documents
* adds buttons and urls for compensation
* simplifies getter for all documents
2022-02-10 10:44:44 +01:00
fce85690b7 #86 Edit document EMA
* adds buttons and urls for ema
2022-02-10 10:28:41 +01:00
a385420c57 #86 Edit document
* adds support for editing of documents
* adds buttons for intervention
2022-02-10 10:21:18 +01:00
9915e6a450 #86 Revocation edit
* adds support for revocation edit
    * revocation document files will be replaced on an edit
2022-02-09 16:02:28 +01:00
59f28fbf12 #86 Edit deductions
* adds support for editing deductions
* adds tests
* improves major base test logic
2022-02-09 14:49:56 +01:00
78b4dce64d #86 Edit payment
* adds button for payment editing
* adds new edit form payment editing
* adds tests for views and workflow
2022-02-09 10:29:34 +01:00
cb6a2d4d91 #86 District column simplification
* simplifies the fetching of districts for district column
2022-02-09 09:30:37 +01:00
591e35a0e2 #86 Parcel-Geometry improvement
* improves the way parcel-geometry relations are stored on the DB
    * instead of a numerical sequence we switched to UUID, so no sequence will run out at anytime (new model: ParcelIntersection)
    * instead of dropping all M2M relations between parcel and geometry on each calculation, we keep the ones that still exist, drop the ones that do not exist and add new ones (if new ones exist)
2022-02-09 09:18:35 +01:00
e3fbe60fce # 86 LANIS link fix
* simplifies creation of LANIS link by refactoring into super class
2022-02-08 17:14:23 +01:00
83531c5f77 #86 Autocomplete enhancement
* adds support for title lookup on EcoAccounts
* adds support for title lookup on Interventions
* adds support for email lookup on User
2022-02-08 17:08:03 +01:00
0afb4d34c3 #86 Parcel district column for all
* adds parcel district column for all major data objects
* adds warning about intervention-revocation on index view of compensations
* adds warning about intervention-revocation on detail view of related compensations
2022-02-08 15:25:44 +01:00
a759eb2453 #86 Revocation rendering if needed
* renders revocation warning on the index view if a revocation exists
2022-02-08 15:07:05 +01:00
fd3fe17953 #86 Parcel districts instead of revocation
* drops revocation column in favour of a parcel district column
2022-02-08 14:51:53 +01:00
58e5b47b07 #86 Email enhancement
* adds object titles to email sending
2022-02-08 14:31:11 +01:00
a56f202e7f Remove form renaming
* renames new remove modal forms to match a more coherent style
2022-02-08 13:31:40 +01:00
51a1652baa Test enhancements
* adds more view tests to intervention tests
2022-02-08 13:27:42 +01:00
6cdf355063 #86 Log detail enhancements
* restructures removing of related data into separate sub-delete forms for easier logic handling
2022-02-08 13:16:20 +01:00
13fd3e1fcb Further tests
* adds tests for intervention workflow
2022-02-08 12:07:49 +01:00
00c1bb67ca Further tests ecoaccount
* adds ecoaccount workflow tests
2022-02-08 11:58:43 +01:00
8d47c9576b Further tests
* restructures compensation/tests into subtests for ecoaccount and compensation
* adds tests for ema workflow
* improved test data setup
2022-02-08 09:27:28 +01:00
a147626174 # 86 Deadline removal log entry
* adds log entries if deadline is removed
2022-02-07 09:56:37 +01:00
34d167a3eb #86 Logs
* adds log detail support for compensation state and action
2022-02-04 16:56:08 +01:00
80b78d3e0d Merge pull request '# 108' (#109) from 108_Deleted_compensation_checked into master
Reviewed-on: SGD-Nord/konova#109
2022-02-04 16:00:20 +01:00
6525f24121 # 108
* fixes bug
2022-02-04 15:59:53 +01:00
16505c79e7 Merge pull request '86_User_suggestions_and_feedback' (#106) from 86_User_suggestions_and_feedback into master
Reviewed-on: SGD-Nord/konova#106
2022-02-04 14:47:23 +01:00
fd04c314cd # 86 Minor html/css tweaks
* improves minor things like display related breakpoints for certain html elements
* improves css for select2 for better group-result distinction
2022-02-04 13:55:50 +01:00
fb8b338950 # 86 More log details Documents
* adds more log details on adding/removing documents
* fixes bug in admin backend where restoring of non-compensation entries led to an error
* fixes bug where deleting of revocation without an attached file would lead to an error
2022-02-04 09:18:46 +01:00
6c80480d0d #86 Userlogs Compensation
* adds log details for adding/removing of compensations for intervention
* adds handy restore-deleted function for admin backend for alls BaseObject derivatives
* adds/updates translations
2022-02-03 15:29:22 +01:00
e5cd5a2312 #86 Userlogs Revocation
* reworks user logs for adding/removing revocations with more detail on log history
* enhances css to display neat shadow on select2-results
2022-02-03 12:10:23 +01:00
1eff3687e8 CSS enhancement
* improves css on select2 width, which lead to strange viewport sizing on creation of new major datasets
2022-02-03 11:58:33 +01:00
a9215511ac # 86 Proper log detail
* adds support for payment adding/deleting to intervention log
* adds support for deduction adding/deleting to intervention/ecoaccount log
* improves code snippets
* drops add_deduction() methods for ecoaccount and intervention in favor of simpler creation in NewDeductionModalForm
* adds messages
* adds/updates translations
2022-02-02 15:16:25 +01:00
f224bbb5bd # 86 HTML simplification
* simplifies rendering of detail attributes for CompensationState and CompensationAction -> takes up less space
2022-02-02 14:26:39 +01:00
874b266352 # 86 Visual improvements
* moves message rendering directly below navigation menu for a more closed look
* reworks message rendering on before_states and after_states for all compensation related datatypes
* reworks layout of action column on all related data card tables
* resizes certain attribute layouts on related data card tables
* reworks layout of details on CompensationState and CompensationAction rendering from own column into subgrouped placement of main type info
* drops align-middle placement for all related data card table contents
2022-02-02 14:18:44 +01:00
77b59db56f # 86 Comment field length
* removes comment field length limit
* adds improvements for rendering large comments
2022-02-02 12:54:45 +01:00
d5e23b420e # 86 Viewport jump EcoAccount/EMA
* adds direct jump of viewport on related-data action (create/delete)
2022-02-02 11:26:02 +01:00
299923ef45 # 86 Viewport jump Compensation
* adds direct jump of viewport on related-data action (create/delete)
* adds comment field to log.html as 'details'
2022-02-02 10:17:59 +01:00
7028672b93 # 86 Viewport jump Intervention
* adds direct jump of viewport on related-data action (create/delete)
2022-02-02 09:32:34 +01:00
108 changed files with 4650 additions and 1973 deletions

View File

@@ -3,11 +3,11 @@
{% block body %}
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
<h3>{% trans 'Evaluation report' %} {{office.long_name}}</h3>
<h5>{% trans 'From' %} {{report.date_from.date}} {% trans 'to' %} {{report.date_to.date}}</h5>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
<div class="d-flex justify-content-end">
<div class="dropdown">
<div class="btn btn" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">

View File

@@ -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,
}

View File

@@ -3,6 +3,8 @@ from django.contrib import admin
from compensation.models import Compensation, CompensationAction, CompensationState, Payment, \
EcoAccountDeduction, EcoAccount
from konova.admin import BaseObjectAdmin, BaseResourceAdmin
from konova.utils.message_templates import COMPENSATION_REMOVED_TEMPLATE
from user.models import UserAction
class AbstractCompensationAdmin(BaseObjectAdmin):
@@ -42,6 +44,17 @@ class CompensationAdmin(AbstractCompensationAdmin):
"intervention",
]
def restore_deleted_data(self, request, queryset):
super().restore_deleted_data(request, queryset)
for entry in queryset:
# Remove delete log entry from related intervention log history
logs = entry.intervention.log.filter(
action=UserAction.EDITED,
comment=COMPENSATION_REMOVED_TEMPLATE.format(entry.identifier)
)
logs.delete()
class EcoAccountAdmin(AbstractCompensationAdmin):
list_display = [

View File

@@ -18,7 +18,7 @@ from compensation.models import Compensation, EcoAccount
from intervention.inputs import GenerateInput
from intervention.models import Intervention, Responsibility, Legal
from konova.forms import BaseForm, SimpleGeomForm
from konova.utils.message_templates import EDITED_GENERAL_DATA
from konova.utils.message_templates import EDITED_GENERAL_DATA, COMPENSATION_ADDED_TEMPLATE
from user.models import UserActionLogEntry
@@ -200,35 +200,49 @@ class NewCompensationForm(AbstractCompensationForm, CEFCompensationFormMixin, Co
self.initialize_form_field("identifier", identifier)
self.fields["identifier"].widget.attrs["url"] = reverse_lazy("compensation:new-id")
def __create_comp(self, user, geom_form) -> Compensation:
""" Creates the compensation from form data
Args:
user (User): The performing user
geom_form (SimpleGeomForm): The geometry form
Returns:
comp (Compensation): The compensation object
"""
# Fetch data from cleaned POST values
identifier = self.cleaned_data.get("identifier", None)
title = self.cleaned_data.get("title", None)
intervention = self.cleaned_data.get("intervention", None)
is_cef = self.cleaned_data.get("is_cef", None)
is_coherence_keeping = self.cleaned_data.get("is_coherence_keeping", None)
comment = self.cleaned_data.get("comment", None)
# Create log entry
action = UserActionLogEntry.get_created_action(user)
# Process the geometry form
geometry = geom_form.save(action)
# Finally create main object
comp = Compensation.objects.create(
identifier=identifier,
title=title,
intervention=intervention,
created=action,
is_cef=is_cef,
is_coherence_keeping=is_coherence_keeping,
geometry=geometry,
comment=comment,
)
# Add the log entry to the main objects log list
comp.log.add(action)
return comp
def save(self, user: User, geom_form: SimpleGeomForm):
with transaction.atomic():
# Fetch data from cleaned POST values
identifier = self.cleaned_data.get("identifier", None)
title = self.cleaned_data.get("title", None)
intervention = self.cleaned_data.get("intervention", None)
is_cef = self.cleaned_data.get("is_cef", None)
is_coherence_keeping = self.cleaned_data.get("is_coherence_keeping", None)
comment = self.cleaned_data.get("comment", None)
# Create log entry
action = UserActionLogEntry.get_created_action(user)
# Process the geometry form
geometry = geom_form.save(action)
# Finally create main object
comp = Compensation.objects.create(
identifier=identifier,
title=title,
intervention=intervention,
created=action,
is_cef=is_cef,
is_coherence_keeping=is_coherence_keeping,
geometry=geometry,
comment=comment,
)
# Add the log entry to the main objects log list
comp.log.add(action)
comp = self.__create_comp(user, geom_form)
comp.intervention.mark_as_edited(user, edit_comment=COMPENSATION_ADDED_TEMPLATE.format(comp.identifier))
return comp
@@ -339,13 +353,13 @@ class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMix
super().__init__(*args, **kwargs)
self.form_title = _("New Eco-Account")
self.action_url = reverse("compensation:acc-new")
self.cancel_redirect = reverse("compensation:acc-index")
self.action_url = reverse("compensation:acc:new")
self.cancel_redirect = reverse("compensation:acc:index")
tmp = EcoAccount()
identifier = tmp.generate_new_identifier()
self.initialize_form_field("identifier", identifier)
self.fields["identifier"].widget.attrs["url"] = reverse_lazy("compensation:acc-new-id")
self.fields["identifier"].widget.attrs["url"] = reverse_lazy("compensation:acc:new-id")
self.fields["title"].widget.attrs["placeholder"] = _("Eco-Account XY; Location ABC")
def save(self, user: User, geom_form: SimpleGeomForm):
@@ -402,8 +416,8 @@ class EditEcoAccountForm(NewEcoAccountForm):
super().__init__(*args, **kwargs)
self.form_title = _("Edit Eco-Account")
self.action_url = reverse("compensation:acc-edit", args=(self.instance.id,))
self.cancel_redirect = reverse("compensation:acc-detail", args=(self.instance.id,))
self.action_url = reverse("compensation:acc:edit", args=(self.instance.id,))
self.cancel_redirect = reverse("compensation:acc:detail", args=(self.instance.id,))
# Initialize form data
reg_date = self.instance.legal.registration_date

View File

@@ -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
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)
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
@@ -346,10 +467,9 @@ class NewActionModalForm(BaseModalForm):
)
comment = forms.CharField(
required=False,
max_length=200,
label=_("Comment"),
label_suffix=_(""),
help_text=_("Additional comment, maximum {} letters").format(200),
help_text=_("Additional comment"),
widget=forms.Textarea(
attrs={
"rows": 5,
@@ -369,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

View File

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

View File

@@ -20,7 +20,9 @@ from compensation.utils.quality import CompensationQualityChecker
from konova.models import BaseObject, AbstractDocument, Deadline, generate_document_file_upload_path, \
GeoReferencedMixin
from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE
from konova.utils.message_templates import DATA_UNSHARED_EXPLANATION
from konova.utils.message_templates import DATA_UNSHARED_EXPLANATION, COMPENSATION_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
@@ -60,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"],
@@ -69,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
@@ -100,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
@@ -124,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
@@ -235,6 +280,11 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin):
self.identifier = self.generate_new_identifier()
super().save(*args, **kwargs)
def mark_as_deleted(self, user, send_mail: bool = True):
super().mark_as_deleted(user, send_mail)
if user is not None:
self.intervention.mark_as_edited(user, edit_comment=COMPENSATION_REMOVED_TEMPLATE.format(self.identifier))
def is_shared_with(self, user: User):
""" Access check
@@ -281,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
@@ -326,7 +354,9 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin):
Returns:
"""
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
@@ -338,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):
"""
@@ -353,7 +403,7 @@ class CompensationDocument(AbstractDocument):
max_length=1000,
)
def delete(self, *args, **kwargs):
def delete(self, user=None, *args, **kwargs):
"""
Custom delete functionality for CompensationDocuments.
Removes the folder from the file system if there are no further documents for this entry.
@@ -375,6 +425,9 @@ class CompensationDocument(AbstractDocument):
folder_path = self.file.path.split("/")[:-1]
folder_path = "/".join(folder_path)
if user:
self.instance.mark_as_edited(user, edit_comment=DOCUMENT_REMOVED_TEMPLATE.format(self.title))
# Remove the file itself
super().delete(*args, **kwargs)

View File

@@ -7,10 +7,12 @@ Created on: 16.11.21
"""
import shutil
from user.models import User
from django.urls import reverse
from konova.utils.message_templates import DEDUCTION_REMOVED, DOCUMENT_REMOVED_TEMPLATE
from django.core.exceptions import ValidationError
from django.core.validators import MinValueValidator
from django.db import models, transaction
from django.db import models
from django.db.models import Sum, QuerySet
from django.utils.translation import gettext_lazy as _
@@ -20,7 +22,6 @@ from compensation.utils.quality import EcoAccountQualityChecker
from konova.models import ShareableObjectMixin, RecordableObjectMixin, AbstractDocument, BaseResource, \
generate_document_file_upload_path
from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE
from user.models import UserActionLogEntry
class EcoAccount(AbstractCompensation, ShareableObjectMixin, RecordableObjectMixin):
@@ -122,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
@@ -165,34 +144,6 @@ class EcoAccount(AbstractCompensation, ShareableObjectMixin, RecordableObjectMix
)
return docs
def add_deduction(self, form):
""" Adds a new deduction to the intervention
Args:
form (NewDeductionModalForm): The form holding the data
Returns:
"""
form_data = form.cleaned_data
user = form.user
with transaction.atomic():
# Create log entry
user_action_create = UserActionLogEntry.get_created_action(user)
user_action_edit = UserActionLogEntry.get_edited_action(user)
self.log.add(user_action_edit)
self.modified = user_action_edit
self.save()
deduction = EcoAccountDeduction.objects.create(
intervention=form_data["intervention"],
account=self,
surface=form_data["surface"],
created=user_action_create,
)
return deduction
def is_ready_for_publish(self) -> bool:
""" Checks whether the data passes all constraints for being publishable
@@ -203,6 +154,14 @@ class EcoAccount(AbstractCompensation, ShareableObjectMixin, RecordableObjectMix
is_ready = is_recorded
return is_ready
def get_share_link(self):
""" Returns the share url for the object
Returns:
"""
return reverse("compensation:acc:share", args=(self.id, self.access_token))
class EcoAccountDocument(AbstractDocument):
"""
@@ -218,7 +177,7 @@ class EcoAccountDocument(AbstractDocument):
max_length=1000,
)
def delete(self, *args, **kwargs):
def delete(self, user=None, *args, **kwargs):
"""
Custom delete functionality for EcoAccountDocuments.
Removes the folder from the file system if there are no further documents for this entry.
@@ -240,6 +199,9 @@ class EcoAccountDocument(AbstractDocument):
folder_path = self.file.path.split("/")[:-1]
folder_path = "/".join(folder_path)
if user:
self.instance.mark_as_edited(user, edit_comment=DOCUMENT_REMOVED_TEMPLATE.format(self.title))
# Remove the file itself
super().delete(*args, **kwargs)
@@ -285,3 +247,9 @@ class EcoAccountDeduction(BaseResource):
def __str__(self):
return "{} of {}".format(self.surface, self.account)
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)
super().delete(*args, **kwargs)

View File

@@ -10,6 +10,8 @@ from django.db import models
from intervention.models import Intervention
from konova.models import BaseResource
from konova.utils.message_templates import PAYMENT_REMOVED
from user.models import UserActionLogEntry
class Payment(BaseResource):

View File

@@ -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):

View File

@@ -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"),
@@ -201,7 +241,7 @@ class EcoAccountTable(BaseTable, TableRenderMixin):
def __init__(self, request: HttpRequest, *args, **kwargs):
self.title = _("Eco Accounts")
self.add_new_url = reverse("compensation:acc-new")
self.add_new_url = reverse("compensation:acc:new")
qs = kwargs.get("queryset", None)
self.filter = EcoAccountTableFilter(
user=request.user,
@@ -224,7 +264,7 @@ class EcoAccountTable(BaseTable, TableRenderMixin):
html = ""
html += self.render_link(
tooltip=_("Open {}").format(_("Eco-account")),
href=reverse("compensation:acc-detail", args=(record.id,)),
href=reverse("compensation:acc:detail", args=(record.id,)),
txt=value,
new_tab=False,
)
@@ -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

View File

@@ -1,4 +1,5 @@
{% load i18n l10n fontawesome_5 humanize %}
{% load i18n l10n fontawesome_5 humanize ksp_filters %}
<div id="actions" class="card">
<div class="card-header rlp-r">
<div class="row">
@@ -20,16 +21,13 @@
</div>
</div>
</div>
<div class="card-body scroll-300">
<div class="card-body scroll-300 p-2">
<table class="table table-hover">
<thead>
<tr>
<th class="w-25" scope="col">
<th class="" scope="col">
{% trans 'Action type' %}
</th>
<th class="w-25" scope="col">
{% trans 'Action type details' %}
</th>
<th scope="col">
{% trans 'Amount' context 'Compensation' %}
</th>
@@ -37,8 +35,10 @@
{% trans 'Comment' %}
</th>
{% if is_default_member and has_access %}
<th scope="col">
{% trans 'Action' %}
<th class="w-10" scope="col">
<span class="float-right">
{% trans 'Action' %}
</span>
</th>
{% endif %}
</tr>
@@ -46,18 +46,26 @@
<tbody>
{% for action in actions %}
<tr>
<td class="align-middle">
{{ action.action_type }}
<td class="">
<span>{{ action.action_type }}</span>
{% if action.action_type_details.count > 0 %}
<br>
{% for detail in action.action_type_details.all %}
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
{% endfor %}
{% endif %}
</td>
<td class="align-middle">
{% for detail in action.action_type_details.all %}
<div class="mb-2" title="{{detail}}">{{detail.long_name}}</div>
{% endfor %}
<td class="">{{ action.amount|floatformat:2|intcomma }} {{ action.unit_humanize }}</td>
<td class="">
<div class="scroll-150">
{{ action.comment }}
</div>
</td>
<td class="align-middle">{{ action.amount|floatformat:2|intcomma }} {{ action.unit_humanize }}</td>
<td class="align-middle">{{ action.comment|default_if_none:"" }}</td>
<td class="align-middle">
<td class="align-middle float-right">
{% if is_default_member and has_access %}
<button data-form-url="{% url 'compensation:action-edit' obj.id action.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit action' %}">
{% fa5_icon 'edit' %}
</button>
<button data-form-url="{% url 'compensation:action-remove' obj.id action.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove action' %}">
{% fa5_icon 'trash' %}
</button>

View File

@@ -20,7 +20,7 @@
</div>
</div>
</div>
<div class="card-body scroll-300">
<div class="card-body scroll-300 p-2">
<table class="table table-hover">
<thead>
<tr>
@@ -34,8 +34,10 @@
{% trans 'Comment' %}
</th>
{% if is_default_member and has_access %}
<th scope="col">
{% trans 'Action' %}
<th class="w-10" scope="col">
<span class="float-right">
{% trans 'Action' %}
</span>
</th>
{% endif %}
</tr>
@@ -47,10 +49,17 @@
{% trans deadline.type_humanized %}
</td>
<td class="align-middle">{{ deadline.date|default_if_none:"---" }}</td>
<td class="align-middle">{{ deadline.comment }}</td>
<td>
<td class="align-middle">
<div class="scroll-150">
{{ deadline.comment }}
</div>
</td>
<td class="align-middle float-right">
{% if is_default_member and has_access %}
<button data-form-url="{% url 'deadline-remove' deadline.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove deadline' %}">
<button data-form-url="{% url 'compensation:deadline-edit' obj.id deadline.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit deadline' %}">
{% fa5_icon 'edit' %}
</button>
<button data-form-url="{% url 'compensation:deadline-remove' obj.id deadline.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove deadline' %}">
{% fa5_icon 'trash' %}
</button>
{% endif %}

View File

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

View File

@@ -20,27 +20,26 @@
</div>
</div>
</div>
<div class="card-body scroll-300">
{% if sum_before_states > sum_after_states %}
<div class="row alert alert-danger">
{% trans 'Missing surfaces according to states before: ' %}{{ diff_states|floatformat:2 }} m²
</div>
{% endif %}
{% if sum_before_states > sum_after_states %}
<div class="alert alert-danger mb-0">
{% trans 'Missing surfaces according to states before: ' %}{{ diff_states|floatformat:2 }} m²
</div>
{% endif %}
<div class="card-body scroll-300 p-2">
<table class="table table-hover">
<thead>
<tr>
<th class="w-25" scope="col">
<th class="w-50" scope="col">
{% trans 'Biotope type' %}
</th>
<th class="w-25" scope="col">
{% trans 'Biotope additional type' %}
</th>
<th scope="col">
{% trans 'Surface' %}
</th>
{% if is_default_member and has_access %}
<th scope="col">
{% trans 'Action' %}
<th class="w-10" scope="col">
<span class="float-right">
{% trans 'Action' %}
</span>
</th>
{% endif %}
</tr>
@@ -48,19 +47,21 @@
<tbody>
{% for state in after_states %}
<tr>
<td class="align-middle">
{{ state.biotope_type }}
<td>
<span>{{ state.biotope_type }}</span>
{% if state.biotope_type_details.count > 0 %}
<br>
{% for detail in state.biotope_type_details.all %}
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
{% endfor %}
{% endif %}
</td>
<td class="align-middle">
{% for biotope_extra in state.biotope_type_details.all %}
<div class="mb-2" title="{{ biotope_extra }}">
{{ biotope_extra.long_name }}
</div>
{% endfor %}
</td>
<td class="align-middle">{{ state.surface|floatformat:2 }} m²</td>
<td class="align-middle">
<td>{{ state.surface|floatformat:2 }} m²</td>
<td class="align-middle float-right">
{% if is_default_member and has_access %}
<button data-form-url="{% url 'compensation:state-edit' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit state' %}">
{% fa5_icon 'edit' %}
</button>
<button data-form-url="{% url 'compensation:state-remove' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove state' %}">
{% fa5_icon 'trash' %}
</button>

View File

@@ -20,27 +20,26 @@
</div>
</div>
</div>
<div class="card-body scroll-300">
{% if sum_before_states < sum_after_states %}
<div class="row alert alert-danger">
{% trans 'Missing surfaces according to states after: ' %}{{ diff_states|floatformat:2 }} m²
</div>
{% endif %}
{% if sum_before_states < sum_after_states %}
<div class="alert alert-danger mb-0">
{% trans 'Missing surfaces according to states after: ' %}{{ diff_states|floatformat:2 }} m²
</div>
{% endif %}
<div class="card-body scroll-300 p-2">
<table class="table table-hover">
<thead>
<tr>
<th class="w-25" scope="col">
<th class="w-50" scope="col">
{% trans 'Biotope type' %}
</th>
<th class="w-25" scope="col">
{% trans 'Biotope additional type' %}
</th>
<th scope="col">
{% trans 'Surface' %}
</th>
{% if is_default_member and has_access %}
<th scope="col">
{% trans 'Action' %}
<th class="w-10" scope="col">
<span class="float-right">
{% trans 'Action' %}
</span>
</th>
{% endif %}
</tr>
@@ -48,19 +47,21 @@
<tbody>
{% for state in before_states %}
<tr>
<td class="align-middle">
{{ state.biotope_type }}
<td>
<span>{{ state.biotope_type }}</span>
{% if state.biotope_type_details.count > 0 %}
<br>
{% for detail in state.biotope_type_details.all %}
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
{% endfor %}
{% endif %}
</td>
<td class="align-middle">
{% for biotope_extra in state.biotope_type_details.all %}
<div class="mb-2" title="{{ biotope_extra }}">
{{ biotope_extra.long_name }}
</div>
{% endfor %}
</td>
<td class="align-middle">{{ state.surface|floatformat:2 }} m²</td>
<td class="align-middle">
<td>{{ state.surface|floatformat:2 }} m²</td>
<td class="align-middle float-right">
{% if is_default_member and has_access %}
<button data-form-url="{% url 'compensation:state-edit' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit state' %}">
{% fa5_icon 'edit' %}
</button>
<button data-form-url="{% url 'compensation:state-remove' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove state' %}">
{% fa5_icon 'trash' %}
</button>

View File

@@ -14,16 +14,16 @@
{% block body %}
<div id="detail-header" class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
<h3>{% trans 'Compensation' %}<br> {{obj.identifier}}</h3>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
{% include 'compensation/detail/compensation/includes/controls.html' %}
</div>
</div>
<hr>
<div id="data" class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
<div class="table-container">
<table class="table table-hover">
<tr>
@@ -105,39 +105,42 @@
</table>
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="row">
{% include 'map/geom_form.html' %}
</div>
<div class="row">
{% include 'konova/includes/parcels.html' %}
</div>
<div class="row">
{% include 'konova/includes/comment_card.html' %}
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
<div class="col">
<div class="row">
{% include 'map/geom_form.html' %}
</div>
<div class="row">
{% include 'konova/includes/parcels.html' %}
</div>
<div class="row">
{% include 'konova/includes/comment_card.html' %}
</div>
</div>
</div>
</div>
<hr>
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
{% include 'compensation/detail/compensation/includes/states-before.html' %}
<div id="related_data">
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
{% include 'compensation/detail/compensation/includes/states-before.html' %}
</div>
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
{% include 'compensation/detail/compensation/includes/states-after.html' %}
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
{% include 'compensation/detail/compensation/includes/states-after.html' %}
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
{% include 'compensation/detail/compensation/includes/actions.html' %}
</div>
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
{% include 'compensation/detail/compensation/includes/deadlines.html' %}
</div>
</div>
</div>
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
{% include 'compensation/detail/compensation/includes/actions.html' %}
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
{% include 'compensation/detail/compensation/includes/deadlines.html' %}
</div>
</div>
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
{% include 'compensation/detail/compensation/includes/documents.html' %}
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
{% include 'compensation/detail/compensation/includes/documents.html' %}
</div>
</div>
</div>

View File

@@ -1,4 +1,4 @@
{% load i18n l10n fontawesome_5 humanize %}
{% load i18n l10n fontawesome_5 humanize ksp_filters %}
<div id="actions" class="card">
<div class="card-header rlp-r">
<div class="row">
@@ -11,7 +11,7 @@
<div class="col-sm-6">
<div class="d-flex justify-content-end">
{% if is_default_member and has_access %}
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:acc-new-action' obj.id %}" title="{% trans 'Add new action' %}">
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:acc:new-action' obj.id %}" title="{% trans 'Add new action' %}">
{% fa5_icon 'plus' %}
{% fa5_icon 'seedling' %}
</button>
@@ -20,25 +20,24 @@
</div>
</div>
</div>
<div class="card-body scroll-300">
<div class="card-body scroll-300 p-2">
<table class="table table-hover">
<thead>
<tr>
<th class="w-25" scope="col">
<th class="" scope="col">
{% trans 'Action type' %}
</th>
<th class="w-25" scope="col">
{% trans 'Action type details' %}
</th>
<th scope="col">
{% trans 'Amount' context 'Compensation' %}
</th>
<th scope="col">
{% trans 'Comment' %}
</th>
{% if default_member and has_access %}
<th scope="col">
{% trans 'Action' %}
{% if is_default_member and has_access %}
<th class="w-10" scope="col">
<span class="float-right">
{% trans 'Action' %}
</span>
</th>
{% endif %}
</tr>
@@ -46,19 +45,27 @@
<tbody>
{% for action in actions %}
<tr>
<td class="align-middle">
{{ action.action_type }}
<td class="">
<span>{{ action.action_type }}</span>
{% if action.action_type_details.count > 0 %}
<br>
{% for detail in action.action_type_details.all %}
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
{% endfor %}
{% endif %}
</td>
<td class="align-middle">
{% for detail in action.action_type_details.all %}
<div class="mb-2" title="{{detail}}">{{detail.long_name}}</div>
{% endfor %}
<td class="">{{ action.amount|floatformat:2|intcomma }} {{ action.unit_humanize }}</td>
<td class="">
<div class="scroll-150">
{{ action.comment }}
</div>
</td>
<td class="align-middle">{{ action.amount|floatformat:2|intcomma }} {{ action.unit_humanize }}</td>
<td class="align-middle">{{ action.comment|default_if_none:"" }}</td>
<td class="align-middle">
<td class="align-middle float-right">
{% if is_default_member and has_access %}
<button data-form-url="{% url 'compensation:acc-action-remove' obj.id action.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove action' %}">
<button data-form-url="{% url 'compensation:acc:action-edit' obj.id action.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit action' %}">
{% fa5_icon 'edit' %}
</button>
<button data-form-url="{% url 'compensation:acc:action-remove' obj.id action.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove action' %}">
{% fa5_icon 'trash' %}
</button>
{% endif %}

View File

@@ -6,36 +6,36 @@
LANIS
</button>
</a>
<a href="{% url 'compensation:acc-report' obj.id %}" target="_blank" class="mr-2">
<a href="{% url 'compensation:acc:report' obj.id %}" target="_blank" class="mr-2">
<button class="btn btn-default" title="{% trans 'Public report' %}">
{% fa5_icon 'file-alt' %}
</button>
</a>
{% if has_access %}
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Share' %}" data-form-url="{% url 'compensation:share-create' obj.id %}">
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Share' %}" data-form-url="{% url 'compensation:acc:share-create' obj.id %}">
{% fa5_icon 'share-alt' %}
</button>
{% if is_ets_member %}
{% if obj.recorded %}
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Unrecord' %}" data-form-url="{% url 'compensation:acc-record' obj.id %}">
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Unrecord' %}" data-form-url="{% url 'compensation:acc:record' obj.id %}">
{% fa5_icon 'bookmark' 'far' %}
</button>
{% else %}
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Record' %}" data-form-url="{% url 'compensation:acc-record' obj.id %}">
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Record' %}" data-form-url="{% url 'compensation:acc:record' obj.id %}">
{% fa5_icon 'bookmark' %}
</button>
{% endif %}
{% endif %}
{% if is_default_member %}
<a href="{% url 'compensation:acc-edit' obj.id %}" class="mr-2">
<a href="{% url 'compensation:acc:edit' obj.id %}" class="mr-2">
<button class="btn btn-default" title="{% trans 'Edit' %}">
{% fa5_icon 'edit' %}
</button>
</a>
<button class="btn btn-default btn-modal mr-2" data-form-url="{% url 'compensation:acc-log' obj.id %}" title="{% trans 'Show log' %}">
<button class="btn btn-default btn-modal mr-2" data-form-url="{% url 'compensation:acc:log' obj.id %}" title="{% trans 'Show log' %}">
{% fa5_icon 'history' %}
</button>
<button class="btn btn-default btn-modal" data-form-url="{% url 'compensation:acc-remove' obj.id %}" title="{% trans 'Delete' %}">
<button class="btn btn-default btn-modal" data-form-url="{% url 'compensation:acc:remove' obj.id %}" title="{% trans 'Delete' %}">
{% fa5_icon 'trash' %}
</button>
{% endif %}

View File

@@ -11,7 +11,7 @@
<div class="col-sm-6">
<div class="d-flex justify-content-end">
{% if is_default_member and has_access %}
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:acc-new-deadline' obj.id %}" title="{% trans 'Add new deadline' %}">
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:acc:new-deadline' obj.id %}" title="{% trans 'Add new deadline' %}">
{% fa5_icon 'plus' %}
{% fa5_icon 'calendar-check' %}
</button>
@@ -20,7 +20,7 @@
</div>
</div>
</div>
<div class="card-body scroll-300">
<div class="card-body scroll-300 p-2">
<table class="table table-hover">
<thead>
<tr>
@@ -33,8 +33,10 @@
<th scope="col">
{% trans 'Comment' %}
</th>
<th scope="col">
{% trans 'Action' %}
<th class="w-10" scope="col">
<span class="float-right">
{% trans 'Action' %}
</span>
</th>
</tr>
</thead>
@@ -45,10 +47,17 @@
{% trans deadline.type_humanized %}
</td>
<td class="align-middle">{{ deadline.date|default_if_none:"---" }}</td>
<td class="align-middle">{{ deadline.comment }}</td>
<td>
<td class="align-middle">
<div class="scroll-150">
{{ deadline.comment }}
</div>
</td>
<td class="align-middle float-right">
{% if is_default_member and has_access %}
<button data-form-url="{% url 'deadline-remove' deadline.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove deadline' %}">
<button data-form-url="{% url 'compensation:acc:deadline-edit' obj.id deadline.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit deadline' %}">
{% fa5_icon 'edit' %}
</button>
<button data-form-url="{% url 'compensation:acc:deadline-remove' obj.id deadline.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove deadline' %}">
{% fa5_icon 'trash' %}
</button>
{% endif %}

View File

@@ -11,7 +11,7 @@
<div class="col-sm-6">
<div class="d-flex justify-content-end">
{% if is_default_member and has_access %}
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:acc-new-deduction' obj.id %}" title="{% trans 'Add new deduction' %}">
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:acc:new-deduction' obj.id %}" title="{% trans 'Add new deduction' %}">
{% fa5_icon 'plus' %}
{% fa5_icon 'tree' %}
</button>
@@ -20,7 +20,7 @@
</div>
</div>
</div>
<div class="card-body scroll-300">
<div class="card-body scroll-300 p-2">
<table class="table table-hover">
<thead>
<tr>
@@ -36,8 +36,10 @@
<th scope="col">
{% trans 'Created' %}
</th>
<th scope="col">
{% trans 'Action' %}
<th class="w-10" scope="col">
<span class="float-right">
{% trans 'Action' %}
</span>
</th>
</tr>
</thead>
@@ -58,9 +60,12 @@
</td>
<td class="align-middle">{{ deduction.surface|floatformat:2|intcomma }} m²</td>
<td class="align-middle">{{ deduction.created.timestamp|default_if_none:""|naturalday}}</td>
<td>
<td class="align-middle float-right">
{% if is_default_member and has_access %}
<button data-form-url="{% url 'compensation:acc-remove-deduction' deduction.account.id deduction.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove Deduction' %}">
<button data-form-url="{% url 'compensation:acc:edit-deduction' deduction.account.id deduction.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit Deduction' %}">
{% fa5_icon 'edit' %}
</button>
<button data-form-url="{% url 'compensation:acc:remove-deduction' deduction.account.id deduction.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove Deduction' %}">
{% fa5_icon 'trash' %}
</button>
{% endif %}

View File

@@ -11,7 +11,7 @@
<div class="col-sm-6">
<div class="d-flex justify-content-end">
{% if is_default_member and has_access %}
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:acc-new-doc' obj.id %}" title="{% trans 'Add new document' %}">
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:acc:new-doc' obj.id %}" title="{% trans 'Add new document' %}">
{% fa5_icon 'plus' %}
{% fa5_icon 'file' %}
</button>
@@ -20,7 +20,7 @@
</div>
</div>
</div>
<div class="card-body scroll-300">
<div class="card-body scroll-300 p-2">
<table class="table table-hover">
<thead>
<tr>
@@ -28,10 +28,15 @@
{% trans 'Title' %}
</th>
<th scope="col">
{% trans 'Comment' %}
{% trans 'Created on' %}
</th>
<th scope="col">
{% trans 'Action' %}
{% trans 'Comment' %}
</th>
<th class="w-10" scope="col">
<span class="float-right">
{% trans 'Action' %}
</span>
</th>
</tr>
</thead>
@@ -39,14 +44,24 @@
{% for doc in obj.documents.all %}
<tr>
<td class="align-middle">
<a href="{% url 'compensation:acc-get-doc' doc.id %}">
<a href="{% url 'compensation:acc:get-doc' obj.id doc.id %}">
{{ doc.title }}
</a>
</td>
<td class="align-middle">{{ doc.comment }}</td>
<td>
<td class="align-middle">
{{ doc.date_of_creation }}
</td>
<td class="align-middle">
<div class="scroll-150">
{{ doc.comment }}
</div>
</td>
<td class="align-middle float-right">
{% if is_default_member and has_access %}
<button data-form-url="{% url 'compensation:acc-remove-doc' doc.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove document' %}">
<button data-form-url="{% url 'compensation:acc:edit-doc' obj.id doc.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit document' %}">
{% fa5_icon 'edit' %}
</button>
<button data-form-url="{% url 'compensation:acc:remove-doc' obj.id doc.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove document' %}">
{% fa5_icon 'trash' %}
</button>
{% endif %}

View File

@@ -11,7 +11,7 @@
<div class="col-sm-6">
<div class="d-flex justify-content-end">
{% if is_default_member and has_access %}
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:acc-new-state' obj.id %}" title="{% trans 'Add new state after' %}">
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:acc:new-state' obj.id %}" title="{% trans 'Add new state after' %}">
{% fa5_icon 'plus' %}
{% fa5_icon 'layer-group' %}
</button>
@@ -20,27 +20,26 @@
</div>
</div>
</div>
<div class="card-body scroll-300">
{% if sum_before_states > sum_after_states %}
<div class="row alert alert-danger">
{% trans 'Missing surfaces according to states before: ' %}{{ diff_states|floatformat:2 }} m²
</div>
{% endif %}
{% if sum_before_states > sum_after_states %}
<div class="alert alert-danger mb-0">
{% trans 'Missing surfaces according to states before: ' %}{{ diff_states|floatformat:2 }} m²
</div>
{% endif %}
<div class="card-body scroll-300 p-2">
<table class="table table-hover">
<thead>
<tr>
<th class="w-25" scope="col">
<th class="w-50" scope="col">
{% trans 'Biotope type' %}
</th>
<th class="w-25" scope="col">
{% trans 'Biotope additional type' %}
</th>
<th scope="col">
{% trans 'Surface' %}
</th>
{% if is_default_member and has_access %}
<th scope="col">
{% trans 'Action' %}
<th class="w-10" scope="col">
<span class="float-right">
{% trans 'Action' %}
</span>
</th>
{% endif %}
</tr>
@@ -48,20 +47,22 @@
<tbody>
{% for state in after_states %}
<tr>
<td class="align-middle">
{{ state.biotope_type }}
<td>
<span>{{ state.biotope_type }}</span>
{% if state.biotope_type_details.count > 0 %}
<br>
{% for detail in state.biotope_type_details.all %}
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
{% endfor %}
{% endif %}
</td>
<td class="align-middle">
{% for biotope_extra in state.biotope_type_details.all %}
<div class="mb-2" title="{{ biotope_extra }}">
{{ biotope_extra.long_name }}
</div>
{% endfor %}
</td>
<td class="align-middle">{{ state.surface|floatformat:2 }} m²</td>
<td class="align-middle">
<td>{{ state.surface|floatformat:2 }} m²</td>
<td class="align-middle float-right">
{% if is_default_member and has_access %}
<button data-form-url="{% url 'compensation:acc-state-remove' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove state' %}">
<button data-form-url="{% url 'compensation:acc:state-edit' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit state' %}">
{% fa5_icon 'edit' %}
</button>
<button data-form-url="{% url 'compensation:acc:state-remove' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove state' %}">
{% fa5_icon 'trash' %}
</button>
{% endif %}

View File

@@ -11,7 +11,7 @@
<div class="col-sm-6">
<div class="d-flex justify-content-end">
{% if is_default_member and has_access %}
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:acc-new-state' obj.id %}?before=true" title="{% trans 'Add new state before' %}">
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:acc:new-state' obj.id %}?before=true" title="{% trans 'Add new state before' %}">
{% fa5_icon 'plus' %}
{% fa5_icon 'layer-group' %}
</button>
@@ -20,27 +20,26 @@
</div>
</div>
</div>
<div class="card-body scroll-300">
{% if sum_before_states < sum_after_states %}
<div class="row alert alert-danger">
{% trans 'Missing surfaces according to states after: ' %}{{ diff_states|floatformat:2 }} m²
</div>
{% endif %}
{% if sum_before_states < sum_after_states %}
<div class="alert alert-danger mb-0">
{% trans 'Missing surfaces according to states after: ' %}{{ diff_states|floatformat:2 }} m²
</div>
{% endif %}
<div class="card-body scroll-300 p-2">
<table class="table table-hover">
<thead>
<tr>
<th class="w-25" scope="col">
<th class="w-50" scope="col">
{% trans 'Biotope type' %}
</th>
<th class="w-25" scope="col">
{% trans 'Biotope additional type' %}
</th>
<th scope="col">
{% trans 'Surface' %}
</th>
{% if is_default_member and has_access %}
<th scope="col">
{% trans 'Action' %}
<th class="w-10" scope="col">
<span class="float-right">
{% trans 'Action' %}
</span>
</th>
{% endif %}
</tr>
@@ -48,20 +47,22 @@
<tbody>
{% for state in before_states %}
<tr>
<td class="align-middle">
{{ state.biotope_type }}
<td>
<span>{{ state.biotope_type }}</span>
{% if state.biotope_type_details.count > 0 %}
<br>
{% for detail in state.biotope_type_details.all %}
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
{% endfor %}
{% endif %}
</td>
<td class="align-middle">
{% for biotope_extra in state.biotope_type_details.all %}
<div class="mb-2" title="{{ biotope_extra }}">
{{ biotope_extra.long_name }}
</div>
{% endfor %}
</td>
<td class="align-middle">{{ state.surface|floatformat:2 }} m²</td>
<td class="align-middle">
<td>{{ state.surface|floatformat:2 }} m²</td>
<td class="align-middle float-right">
{% if is_default_member and has_access %}
<button data-form-url="{% url 'compensation:acc-state-remove' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove state' %}">
<button data-form-url="{% url 'compensation:acc:state-edit' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit state' %}">
{% fa5_icon 'edit' %}
</button>
<button data-form-url="{% url 'compensation:acc:state-remove' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove state' %}">
{% fa5_icon 'trash' %}
</button>
{% endif %}

View File

@@ -14,16 +14,16 @@
{% block body %}
<div id="detail-header" class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
<h3>{% trans 'Eco-account' %}<br> {{obj.identifier}}</h3>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
{% include 'compensation/detail/eco_account/includes/controls.html' %}
</div>
</div>
<hr>
<div id="data" class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
<div class="table-container">
<table class="table table-hover">
<tr>
@@ -88,7 +88,7 @@
</table>
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
<div class="row">
{% include 'map/geom_form.html' %}
</div>
@@ -102,28 +102,30 @@
</div>
<hr>
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
{% include 'compensation/detail/eco_account/includes/states-before.html' %}
<div id="related_data">
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
{% include 'compensation/detail/eco_account/includes/states-before.html' %}
</div>
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
{% include 'compensation/detail/eco_account/includes/states-after.html' %}
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
{% include 'compensation/detail/eco_account/includes/states-after.html' %}
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
{% include 'compensation/detail/eco_account/includes/actions.html' %}
</div>
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
{% include 'compensation/detail/eco_account/includes/deadlines.html' %}
</div>
</div>
</div>
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
{% include 'compensation/detail/eco_account/includes/actions.html' %}
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
{% include 'compensation/detail/eco_account/includes/deadlines.html' %}
</div>
</div>
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
{% include 'compensation/detail/eco_account/includes/documents.html' %}
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
{% include 'compensation/detail/eco_account/includes/deductions.html' %}
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
{% include 'compensation/detail/eco_account/includes/documents.html' %}
</div>
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
{% include 'compensation/detail/eco_account/includes/deductions.html' %}
</div>
</div>
</div>

View File

@@ -3,7 +3,7 @@
{% block body %}
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
<h3>{% trans 'Report' %}</h3>
<h4>{{obj.identifier}}</h4>
<div class="table-container">
@@ -33,7 +33,7 @@
{% include 'compensation/detail/compensation/includes/states-after.html' %}
{% include 'compensation/detail/compensation/includes/actions.html' %}
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
<div class="row">
{% include 'map/geom_form.html' %}
</div>

View File

@@ -3,7 +3,7 @@
{% block body %}
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
<h3>{% trans 'Report' %}</h3>
<h4>{{obj.identifier}}</h4>
<div class="table-container">
@@ -50,7 +50,7 @@
{% include 'compensation/detail/compensation/includes/states-after.html' %}
{% include 'compensation/detail/compensation/includes/actions.html' %}
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
<div class="row">
{% include 'map/geom_form.html' %}
</div>

View File

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

View File

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

View File

@@ -2,7 +2,7 @@
Author: Michel Peltriaux
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())

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,31 +8,42 @@ Created on: 24.08.21
from django.urls import path
from compensation.views.eco_account import *
app_name = "acc"
urlpatterns = [
path("", index_view, name="acc-index"),
path('new/', new_view, name='acc-new'),
path('new/id', new_id_view, name='acc-new-id'),
path('<id>', detail_view, name='acc-detail'),
path('<id>/log', log_view, name='acc-log'),
path('<id>/record', record_view, name='acc-record'),
path('<id>/report', report_view, name='acc-report'),
path('<id>/edit', edit_view, name='acc-edit'),
path('<id>/remove', remove_view, name='acc-remove'),
path('<id>/state/new', state_new_view, name='acc-new-state'),
path('<id>/action/new', action_new_view, name='acc-new-action'),
path('<id>/state/<state_id>/remove', state_remove_view, name='acc-state-remove'),
path('<id>/action/<action_id>/remove', action_remove_view, name='acc-action-remove'),
path('<id>/deadline/new', deadline_new_view, name="acc-new-deadline"),
path("", index_view, name="index"),
path('new/', new_view, name='new'),
path('new/id', new_id_view, name='new-id'),
path('<id>', detail_view, name='detail'),
path('<id>/log', log_view, name='log'),
path('<id>/record', record_view, name='record'),
path('<id>/report', report_view, name='report'),
path('<id>/edit', edit_view, name='edit'),
path('<id>/remove', remove_view, name='remove'),
path('<id>/state/new', state_new_view, name='new-state'),
path('<id>/state/<state_id>/edit', state_edit_view, name='state-edit'),
path('<id>/state/<state_id>/remove', state_remove_view, name='state-remove'),
path('<id>/action/new', action_new_view, name='new-action'),
path('<id>/action/<action_id>/edit', action_edit_view, name='action-edit'),
path('<id>/action/<action_id>/remove', action_remove_view, name='action-remove'),
path('<id>/deadline/new', deadline_new_view, name="new-deadline"),
path('<id>/deadline/<deadline_id>/edit', deadline_edit_view, name='deadline-edit'),
path('<id>/deadline/<deadline_id>/remove', deadline_remove_view, name='deadline-remove'),
path('<id>/share/<token>', share_view, name='share'),
path('<id>/share', create_share_view, name='share-create'),
# Documents
path('<id>/document/new/', new_document_view, name='acc-new-doc'),
path('document/<doc_id>', get_document_view, name='acc-get-doc'),
path('document/<doc_id>/remove/', remove_document_view, name='acc-remove-doc'),
path('<id>/document/new/', new_document_view, name='new-doc'),
path('<id>/document/<doc_id>', get_document_view, name='get-doc'),
path('<id>/document/<doc_id>/edit', edit_document_view, name='edit-doc'),
path('<id>/document/<doc_id>/remove/', remove_document_view, name='remove-doc'),
# Eco-account deductions
path('<id>/remove/<deduction_id>', deduction_remove_view, name='acc-remove-deduction'),
path('<id>/deduct/new', new_deduction_view, name='acc-new-deduction'),
path('<id>/deduction/<deduction_id>/remove', deduction_remove_view, name='remove-deduction'),
path('<id>/deduction/<deduction_id>/edit', deduction_edit_view, name='edit-deduction'),
path('<id>/deduct/new', new_deduction_view, name='new-deduction'),
]

View File

@@ -8,7 +8,9 @@ Created on: 24.08.21
from django.urls import path
from compensation.views.payment import *
app_name = "pay"
urlpatterns = [
path('<intervention_id>/new', new_payment_view, name='pay-new'),
path('<id>/remove', payment_remove_view, name='pay-remove'),
path('<id>/new', new_payment_view, name='new'),
path('<id>/remove/<payment_id>', payment_remove_view, name='remove'),
path('<id>/edit/<payment_id>', payment_edit_view, name='edit'),
]

View File

@@ -10,6 +10,6 @@ from django.urls import path, include
app_name = "compensation"
urlpatterns = [
path("", include("compensation.urls.compensation")),
path("acc/", include("compensation.urls.eco_account")),
path("pay/", include("compensation.urls.payment")),
path("acc/", include("compensation.urls.eco_account", namespace="acc")),
path("pay/", include("compensation.urls.payment", namespace="pay")),
]

View File

@@ -6,18 +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
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
@@ -78,7 +83,7 @@ def new_view(request: HttpRequest, intervention_id: str = None):
comp.identifier
)
)
messages.success(request, _("Compensation {} added").format(comp.identifier))
messages.success(request, COMPENSATION_ADDED_TEMPLATE.format(comp.identifier))
return redirect("compensation:detail", id=comp.id)
else:
messages.error(request, FORM_INVALID, extra_tags="danger",)
@@ -255,7 +260,7 @@ def remove_view(request: HttpRequest, id: str):
form = RemoveModalForm(request.POST or None, instance=comp, request=request)
return form.process_request(
request=request,
msg_success=_("Compensation removed"),
msg_success=COMPENSATION_REMOVED_TEMPLATE.format(comp.identifier),
redirect_url=reverse("compensation:index"),
)
@@ -273,54 +278,52 @@ 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")
msg_success=DOCUMENT_ADDED,
redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data"
)
@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,
@@ -328,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")
@@ -345,7 +374,8 @@ 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"
)
@@ -366,7 +396,32 @@ 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"
)
@@ -387,7 +442,56 @@ 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"
)
@login_required
@default_group_required
@shared_access_required(Compensation, "id")
def deadline_remove_view(request: HttpRequest, id: str, deadline_id: str):
""" Renders a form for removing 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 = RemoveDeadlineModalForm(request.POST or None, instance=comp, deadline=deadline, request=request)
return form.process_request(
request,
msg_success=DEADLINE_REMOVED,
redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data"
)
@@ -405,11 +509,37 @@ 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"
)
@@ -427,11 +557,13 @@ 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"
)

View File

@@ -16,20 +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
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
@@ -89,7 +95,7 @@ def new_view(request: HttpRequest):
)
)
messages.success(request, _("Eco-Account {} added").format(acc.identifier))
return redirect("compensation:acc-detail", id=acc.id)
return redirect("compensation:acc:detail", id=acc.id)
else:
messages.error(request, FORM_INVALID, extra_tags="danger",)
else:
@@ -147,7 +153,7 @@ def edit_view(request: HttpRequest, id: str):
# The data form takes the geom form for processing, as well as the performing user
acc = data_form.save(request.user, geom_form)
messages.success(request, _("Eco-Account {} edited").format(acc.identifier))
return redirect("compensation:acc-detail", id=acc.id)
return redirect("compensation:acc:detail", id=acc.id)
else:
messages.error(request, FORM_INVALID, extra_tags="danger",)
else:
@@ -254,13 +260,13 @@ def remove_view(request: HttpRequest, id: str):
user = request.user
if not in_group(user, ETS_GROUP):
messages.info(request, CANCEL_ACC_RECORDED_OR_DEDUCTED)
return redirect("compensation:acc-detail", id=id)
return redirect("compensation:acc:detail", id=id)
form = RemoveModalForm(request.POST or None, instance=acc, request=request)
return form.process_request(
request=request,
msg_success=_("Eco-account removed"),
redirect_url=reverse("compensation:acc-index"),
redirect_url=reverse("compensation:acc:index"),
)
@@ -284,10 +290,39 @@ 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")
msg_success=DEDUCTION_REMOVED,
redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data"
)
@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"
)
@@ -357,7 +392,8 @@ 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"
)
@@ -378,7 +414,8 @@ 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"
)
@@ -396,11 +433,37 @@ 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"
)
@@ -418,11 +481,85 @@ 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"
)
@login_required
@default_group_required
@shared_access_required(EcoAccount, "id")
def deadline_remove_view(request: HttpRequest, id: str, deadline_id: str):
""" Renders a form for removing 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 = RemoveDeadlineModalForm(request.POST or None, instance=comp, deadline=deadline, request=request)
return form.process_request(
request,
msg_success=DEADLINE_REMOVED,
redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data"
)
@@ -443,7 +580,8 @@ 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"
)
@@ -460,55 +598,78 @@ 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")
msg_success=DOCUMENT_ADDED,
redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data",
)
@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,
@@ -533,7 +694,8 @@ def new_deduction_view(request: HttpRequest, id: str):
form = NewDeductionModalForm(request.POST or None, instance=acc, request=request)
return form.process_request(
request,
msg_success=_("Deduction added")
msg_success=DEDUCTION_ADDED,
redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data"
)
@@ -632,7 +794,7 @@ def share_view(request: HttpRequest, id: str, token: str):
_("{} has been shared with you").format(obj.identifier)
)
obj.share_with(user)
return redirect("compensation:acc-detail", id=id)
return redirect("compensation:acc:detail", id=id)
else:
messages.error(
request,

View File

@@ -5,54 +5,86 @@ Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 09.08.21
"""
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
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, 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")
msg_success=PAYMENT_ADDED,
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"),
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"
)

View File

@@ -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

View File

@@ -11,13 +11,14 @@ from django.contrib import messages
from django.db import models
from django.db.models import QuerySet
from django.http import HttpRequest
from django.urls import reverse
from compensation.models import AbstractCompensation
from ema.managers import EmaManager
from ema.utils.quality import EmaQualityChecker
from konova.models import AbstractDocument, generate_document_file_upload_path, RecordableObjectMixin, ShareableObjectMixin
from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE
from konova.utils.message_templates import DATA_UNSHARED_EXPLANATION
from konova.utils.message_templates import DATA_UNSHARED_EXPLANATION, DOCUMENT_REMOVED_TEMPLATE
class Ema(AbstractCompensation, ShareableObjectMixin, RecordableObjectMixin):
@@ -50,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
@@ -119,6 +98,14 @@ class Ema(AbstractCompensation, ShareableObjectMixin, RecordableObjectMixin):
is_ready = is_recorded
return is_ready
def get_share_link(self):
""" Returns the share url for the object
Returns:
"""
return reverse("ema:share", args=(self.id, self.access_token))
class EmaDocument(AbstractDocument):
"""
@@ -134,7 +121,7 @@ class EmaDocument(AbstractDocument):
max_length=1000,
)
def delete(self, *args, **kwargs):
def delete(self, user=None, *args, **kwargs):
"""
Custom delete functionality for EcoAccountDocuments.
Removes the folder from the file system if there are no further documents for this entry.
@@ -156,6 +143,9 @@ class EmaDocument(AbstractDocument):
folder_path = self.file.path.split("/")[:-1]
folder_path = "/".join(folder_path)
if user:
self.instance.mark_as_edited(user, edit_comment=DOCUMENT_REMOVED_TEMPLATE.format(self.title))
# Remove the file itself
super().delete(*args, **kwargs)

View File

@@ -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

View File

@@ -1,4 +1,4 @@
{% load i18n l10n fontawesome_5 humanize %}
{% load i18n l10n fontawesome_5 humanize ksp_filters %}
<div id="actions" class="card">
<div class="card-header rlp-r">
<div class="row">
@@ -20,42 +20,49 @@
</div>
</div>
</div>
<div class="card-body scroll-300">
<div class="card-body scroll-300 p-2">
<table class="table table-hover">
<thead>
<tr>
<th class="w-25" scope="col">
{% trans 'Action type' %}
</th>
<th class="w-25" scope="col">
{% trans 'Action type details' %}
</th>
<th scope="col">
{% trans 'Amount' context 'Compensation' %}
</th>
<th scope="col">
{% trans 'Comment' %}
</th>
<th scope="col">
{% trans 'Action' %}
<th class="w-10" scope="col">
<span class="float-right">
{% trans 'Action' %}
</span>
</th>
</tr>
</thead>
<tbody>
{% for action in obj.actions.all %}
<tr>
<td class="align-middle">
{{ action.action_type }}
<td class="">
<span>{{ action.action_type }}</span>
{% if action.action_type_details.count > 0 %}
<br>
{% for detail in action.action_type_details.all %}
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
{% endfor %}
{% endif %}
</td>
<td class="align-middle">
{% for detail in action.action_type_details.all %}
<div class="mb-2" title="{{detail}}">{{detail.long_name}}</div>
{% endfor %}
<td class="">{{ action.amount|floatformat:2|intcomma }} {{ action.unit_humanize }}</td>
<td class="">
<div class="scroll-150">
{{ action.comment }}
</div>
</td>
<td class="align-middle">{{ action.amount|floatformat:2|intcomma }} {{ action.unit_humanize }}</td>
<td class="align-middle">{{ action.comment|default_if_none:"" }}</td>
<td class="align-middle">
<td class="align-middle float-right">
{% if is_default_member and has_access %}
<button data-form-url="{% url 'ema:action-edit' obj.id action.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit action' %}">
{% fa5_icon 'edit' %}
</button>
<button data-form-url="{% url 'ema:action-remove' obj.id action.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove action' %}">
{% fa5_icon 'trash' %}
</button>

View File

@@ -20,7 +20,7 @@
</div>
</div>
</div>
<div class="card-body scroll-300">
<div class="card-body scroll-300 p-2">
<table class="table table-hover">
<thead>
<tr>
@@ -33,8 +33,10 @@
<th scope="col">
{% trans 'Comment' %}
</th>
<th scope="col">
{% trans 'Action' %}
<th class="w-10" scope="col">
<span class="float-right">
{% trans 'Action' %}
</span>
</th>
</tr>
</thead>
@@ -45,10 +47,17 @@
{% trans deadline.type_humanized %}
</td>
<td class="align-middle">{{ deadline.date|default_if_none:"---" }}</td>
<td class="align-middle">{{ deadline.comment }}</td>
<td>
<td class="align-middle">
<div class="scroll-150">
{{ deadline.comment }}
</div>
</td>
<td class="align-middle float-right">
{% if is_default_member and has_access %}
<button data-form-url="{% url 'deadline-remove' deadline.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove deadline' %}">
<button data-form-url="{% url 'ema:deadline-edit' obj.id deadline.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit deadline' %}">
{% fa5_icon 'edit' %}
</button>
<button data-form-url="{% url 'ema:deadline-remove' obj.id deadline.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove deadline' %}">
{% fa5_icon 'trash' %}
</button>
{% endif %}

View File

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

View File

@@ -20,45 +20,46 @@
</div>
</div>
</div>
<div class="card-body scroll-300">
{% if sum_before_states > sum_after_states %}
<div class="row alert alert-danger">
{% trans 'Missing surfaces according to states before: ' %}{{ diff_states|floatformat:2 }} m²
</div>
{% endif %}
{% if sum_before_states > sum_after_states %}
<div class="alert alert-danger mb-0">
{% trans 'Missing surfaces according to states before: ' %}{{ diff_states|floatformat:2 }} m²
</div>
{% endif %}
<div class="card-body scroll-300 p-2">
<table class="table table-hover">
<thead>
<tr>
<th class="w-25" scope="col">
<th class="w-50" scope="col">
{% trans 'Biotope type' %}
</th>
<th class="w-25" scope="col">
{% trans 'Biotope additional type' %}
</th>
<th scope="col">
{% trans 'Surface' %}
</th>
<th scope="col">
{% trans 'Action' %}
<th class="w-10" scope="col">
<span class="float-right">
{% trans 'Action' %}
</span>
</th>
</tr>
</thead>
<tbody>
{% for state in after_states %}
<tr>
<td class="align-middle">
{{ state.biotope_type }}
<td>
<span>{{ state.biotope_type }}</span>
{% if state.biotope_type_details.count > 0 %}
<br>
{% for detail in state.biotope_type_details.all %}
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
{% endfor %}
{% endif %}
</td>
<td class="align-middle">
{% for biotope_extra in state.biotope_type_details.all %}
<div class="mb-2" title="{{ biotope_extra }}">
{{ biotope_extra.long_name }}
</div>
{% endfor %}
</td>
<td class="align-middle">{{ state.surface|floatformat:2 }} m²</td>
<td class="align-middle">
<td>{{ state.surface|floatformat:2 }} m²</td>
<td class="align-middle float-right">
{% if is_default_member and has_access %}
<button data-form-url="{% url 'ema:state-edit' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit state' %}">
{% fa5_icon 'edit' %}
</button>
<button data-form-url="{% url 'ema:state-remove' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove state' %}">
{% fa5_icon 'trash' %}
</button>

View File

@@ -20,45 +20,46 @@
</div>
</div>
</div>
<div class="card-body scroll-300">
{% if sum_before_states < sum_after_states %}
<div class="row alert alert-danger">
{% trans 'Missing surfaces according to states after: ' %}{{ diff_states|floatformat:2 }} m²
</div>
{% endif %}
{% if sum_before_states < sum_after_states %}
<div class="alert alert-danger mb-0">
{% trans 'Missing surfaces according to states after: ' %}{{ diff_states|floatformat:2 }} m²
</div>
{% endif %}
<div class="card-body scroll-300 p-2">
<table class="table table-hover">
<thead>
<tr>
<th class="w-25" scope="col">
<th class="w-50" scope="col">
{% trans 'Biotope type' %}
</th>
<th class="w-25" scope="col">
{% trans 'Biotope additional type' %}
</th>
<th scope="col">
{% trans 'Surface' %}
</th>
<th scope="col">
{% trans 'Action' %}
<th class="w-10" scope="col">
<span class="float-right">
{% trans 'Action' %}
</span>
</th>
</tr>
</thead>
<tbody>
{% for state in before_states %}
<tr>
<td class="align-middle">
{{ state.biotope_type }}
<td>
<span>{{ state.biotope_type }}</span>
{% if state.biotope_type_details.count > 0 %}
<br>
{% for detail in state.biotope_type_details.all %}
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
{% endfor %}
{% endif %}
</td>
<td class="align-middle">
{% for biotope_extra in state.biotope_type_details.all %}
<div class="mb-2" title="{{ biotope_extra }}">
{{ biotope_extra.long_name }}
</div>
{% endfor %}
</td>
<td class="align-middle">{{ state.surface|floatformat:2 }} m²</td>
<td class="align-middle">
<td>{{ state.surface|floatformat:2 }} m²</td>
<td class="align-middle float-right">
{% if is_default_member and has_access %}
<button data-form-url="{% url 'ema:state-edit' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit state' %}">
{% fa5_icon 'edit' %}
</button>
<button data-form-url="{% url 'ema:state-remove' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove state' %}">
{% fa5_icon 'trash' %}
</button>

View File

@@ -14,16 +14,16 @@
{% block body %}
<div id="detail-header" class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
<h3>{% trans 'Payment funded compensation' %} <br> {{obj.identifier}}</h3>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
{% include 'ema/detail/includes/controls.html' %}
</div>
</div>
<hr>
<div id="data" class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
<div class="table-container">
<table class="table table-hover">
<tr>
@@ -82,7 +82,7 @@
</table>
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
<div class="row">
{% include 'map/geom_form.html' %}
</div>
@@ -95,26 +95,27 @@
</div>
</div>
<hr>
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
{% include 'ema/detail/includes/states-before.html' %}
<div id="related_data">
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
{% include 'ema/detail/includes/states-before.html' %}
</div>
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
{% include 'ema/detail/includes/states-after.html' %}
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
{% include 'ema/detail/includes/states-after.html' %}
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
{% include 'ema/detail/includes/actions.html' %}
</div>
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
{% include 'ema/detail/includes/deadlines.html' %}
</div>
</div>
</div>
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
{% include 'ema/detail/includes/actions.html' %}
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
{% include 'ema/detail/includes/deadlines.html' %}
</div>
</div>
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
{% include 'ema/detail/includes/documents.html' %}
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
{% include 'ema/detail/includes/documents.html' %}
</div>
</div>
</div>

View File

@@ -3,7 +3,7 @@
{% block body %}
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
<h3>{% trans 'Report' %}</h3>
<h4>{{obj.identifier}}</h4>
<div class="table-container">
@@ -37,7 +37,7 @@
{% include 'compensation/detail/compensation/includes/states-after.html' %}
{% include 'compensation/detail/compensation/includes/actions.html' %}
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
<div class="row">
{% include 'map/geom_form.html' %}
</div>

View File

@@ -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,

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

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

View File

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

View File

@@ -6,20 +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
from konova.utils.message_templates import IDENTIFIER_REPLACED, FORM_INVALID, DATA_UNSHARED, DATA_UNSHARED_EXPLANATION, \
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
@@ -290,7 +297,8 @@ 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"
)
@@ -311,7 +319,32 @@ 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"
)
@@ -332,7 +365,8 @@ 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"
)
@@ -349,54 +383,78 @@ 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")
msg_success=DOCUMENT_ADDED,
redirect_url=reverse("ema:detail", args=(id,)) + "#related_data"
)
@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,
@@ -418,11 +476,37 @@ 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"
)
@@ -440,11 +524,13 @@ 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"
)
@@ -564,4 +650,52 @@ def create_share_view(request: HttpRequest, id: str):
return form.process_request(
request,
msg_success=_("Share settings updated")
)
@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")
def deadline_remove_view(request: HttpRequest, id: str, deadline_id: str):
""" Renders a form for removing 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 = RemoveDeadlineModalForm(request.POST or None, instance=ema, deadline=deadline, request=request)
return form.process_request(
request,
msg_success=DEADLINE_REMOVED,
redirect_url=reverse("ema:detail", args=(id,)) + "#related_data"
)

View File

@@ -7,6 +7,8 @@ Created on: 02.12.20
"""
from dal import autocomplete
from django import forms
from konova.utils.message_templates import EDITED_GENERAL_DATA
from user.models import User
from django.db import transaction
from django.urls import reverse, reverse_lazy
@@ -333,7 +335,7 @@ class EditInterventionForm(NewInterventionForm):
self.instance.responsible.conservation_file_number = conservation_file_number
self.instance.responsible.save()
user_action = UserActionLogEntry.get_edited_action(user)
user_action = self.instance.mark_as_edited(user, edit_comment=EDITED_GENERAL_DATA)
geometry = geom_form.save(user_action)
self.instance.geometry = geometry
@@ -347,8 +349,5 @@ class EditInterventionForm(NewInterventionForm):
self.instance.modified = user_action
self.instance.save()
# Uncheck and unrecord intervention due to changed data
self.instance.mark_as_edited(user)
return self.instance

View File

@@ -6,16 +6,20 @@ Created on: 27.09.21
"""
from dal import autocomplete
from user.models import User
from django.core.exceptions import ObjectDoesNotExist
from django.db.models.fields.files import FieldFile
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
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from compensation.models import EcoAccount
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
@@ -80,10 +84,8 @@ class ShareModalForm(BaseModalForm):
"""
# Initialize share_link field
url_name = f"{self.instance._meta.app_label}:share"
self.share_link = self.request.build_absolute_uri(
reverse(url_name, args=(self.instance.id, self.instance.access_token,))
)
share_link = self.instance.get_share_link()
self.share_link = self.request.build_absolute_uri(share_link)
self.initialize_form_field(
"url",
self.share_link
@@ -158,6 +160,7 @@ class NewRevocationModalForm(BaseModalForm):
}
)
)
document_model = RevocationDocument
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -169,10 +172,50 @@ class NewRevocationModalForm(BaseModalForm):
def save(self):
revocation = self.instance.add_revocation(self)
self.instance.mark_as_edited(self.user, self.request)
self.instance.mark_as_edited(self.user, self.request, edit_comment=REVOCATION_ADDED)
return revocation
class EditRevocationModalForm(NewRevocationModalForm):
revocation = None
def __init__(self, *args, **kwargs):
self.revocation = kwargs.pop("revocation", None)
super().__init__(*args, **kwargs)
try:
doc = self.revocation.document.file
except ObjectDoesNotExist:
doc = None
form_data = {
"date": str(self.revocation.date),
"file": doc,
"comment": self.revocation.comment,
}
self.load_initial_data(form_data)
def save(self):
revocation = self.instance.edit_revocation(self)
self.instance.mark_as_edited(self.user, self.request, edit_comment=REVOCATION_EDITED)
return revocation
class RemoveRevocationModalForm(RemoveModalForm):
""" Removing modal form for Revocation
Can be used for anything, where removing shall be confirmed by the user a second time.
"""
revocation = None
def __init__(self, *args, **kwargs):
revocation = kwargs.pop("revocation", None)
self.revocation = revocation
super().__init__(*args, **kwargs)
def save(self):
self.instance.remove_revocation(self)
class CheckModalForm(BaseModalForm):
""" The modal form for running a check on interventions and their compensations
@@ -333,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
@@ -351,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:
@@ -368,11 +423,99 @@ class NewDeductionModalForm(BaseModalForm):
)
return is_valid_surface and super_result
def __create_deduction(self):
""" Creates the deduction
Returns:
"""
with transaction.atomic():
user_action_create = UserActionLogEntry.get_created_action(self.user)
deduction = EcoAccountDeduction.objects.create(
intervention=self.cleaned_data["intervention"],
account=self.cleaned_data["account"],
surface=self.cleaned_data["surface"],
created=user_action_create,
)
return deduction
def save(self):
deduction = self.instance.add_deduction(self)
self.instance.mark_as_edited(self.user, self.request, reset_recorded=False)
deduction = self.__create_deduction()
self.cleaned_data["intervention"].mark_as_edited(self.user, edit_comment=DEDUCTION_ADDED)
self.cleaned_data["account"].mark_as_edited(self.user, edit_comment=DEDUCTION_ADDED)
return deduction
class 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

View File

@@ -8,6 +8,9 @@ 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
from user.models import User
@@ -15,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
@@ -25,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
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
@@ -99,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
@@ -195,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):
@@ -228,27 +204,55 @@ class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, Chec
)
return revocation
def add_deduction(self, form):
""" Adds a new deduction to the intervention
def edit_revocation(self, form):
""" Updates a revocation of the intervention
Args:
form (NewDeductionModalForm): The form holding the data
form (EditRevocationModalForm): The form holding the data
Returns:
"""
form_data = form.cleaned_data
user = form.user
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():
user_action_create = UserActionLogEntry.get_created_action(user)
deduction = EcoAccountDeduction.objects.create(
intervention=self,
account=form_data["account"],
surface=form_data["surface"],
created=user_action_create,
)
return deduction
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
@@ -263,9 +267,12 @@ class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, Chec
Returns:
"""
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
def set_status_messages(self, request: HttpRequest):
""" Setter for different information that need to be rendered
@@ -278,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)
@@ -299,6 +313,29 @@ class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, Chec
and is_free_of_revocations
return is_ready
def get_share_link(self):
""" Returns the share url for the object
Returns:
"""
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):
"""
@@ -314,7 +351,7 @@ class InterventionDocument(AbstractDocument):
max_length=1000,
)
def delete(self, *args, **kwargs):
def delete(self, user=None, *args, **kwargs):
"""
Custom delete functionality for InterventionDocuments.
Removes the folder from the file system if there are no further documents for this entry.
@@ -336,6 +373,9 @@ class InterventionDocument(AbstractDocument):
folder_path = self.file.path.split("/")[:-1]
folder_path = "/".join(folder_path)
if user:
self.instance.mark_as_edited(user, edit_comment=DOCUMENT_REMOVED_TEMPLATE.format(self.title))
# Remove the file itself
super().delete(*args, **kwargs)

View File

@@ -9,7 +9,10 @@ Created on: 15.11.21
import shutil
from django.contrib.gis.db import models
from django.core.exceptions import ObjectDoesNotExist
from konova.models import BaseResource, AbstractDocument, generate_document_file_upload_path
from konova.utils.message_templates import REVOCATION_REMOVED
class Revocation(BaseResource):
@@ -22,10 +25,18 @@ class Revocation(BaseResource):
def delete(self, *args, **kwargs):
# Make sure related objects are being removed as well
if self.document:
try:
self.document.delete(*args, **kwargs)
except ObjectDoesNotExist:
# No file to delete
pass
super().delete()
@property
def intervention(self):
return self.legal.intervention
class RevocationDocument(AbstractDocument):
"""

View File

@@ -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)

View File

@@ -22,19 +22,21 @@
</div>
</div>
</div>
<div class="card-body scroll-300">
<div class="card-body scroll-300 p-2">
<table class="table table-hover">
<thead>
<tr>
<th scope="col">
<th class="w-25" scope="col">
{% trans 'Identifier' %}
</th>
<th scope="col">
{% trans 'Title' %}
</th>
{% if is_default_member and has_access %}
<th scope="col">
{% trans 'Action' %}
<th class="w-10" scope="col">
<span class="float-right">
{% trans 'Action' %}
</span>
</th>
{% endif %}
</tr>
@@ -50,7 +52,7 @@
<td class="align-middle">{{ comp.title }}</td>
<td>
{% if is_default_member and has_access %}
<button data-form-url="{% url 'compensation:remove' comp.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove compensation' %}">
<button data-form-url="{% url 'intervention:remove-compensation' obj.id comp.id %}" class="btn btn-default btn-modal float-right" title="{% trans 'Remove compensation' %}">
{% fa5_icon 'trash' %}
</button>
{% endif %}

View File

@@ -11,7 +11,7 @@
<div class="col-sm-6">
<div class="d-flex justify-content-end">
{% if is_default_member and has_access %}
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'intervention:acc-new-deduction' obj.id %}" title="{% trans 'Add new deduction' %}">
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'intervention:new-deduction' obj.id %}" title="{% trans 'Add new deduction' %}">
{% fa5_icon 'plus' %}
{% fa5_icon 'tree' %}
</button>
@@ -20,11 +20,11 @@
</div>
</div>
</div>
<div class="card-body scroll-300">
<div class="card-body scroll-300 p-2">
<table class="table table-hover">
<thead>
<tr>
<th scope="col">
<th class="w-25" scope="col">
{% trans 'Account Identifier' %}
</th>
<th scope="col">
@@ -34,8 +34,10 @@
{% trans 'Created' %}
</th>
{% if is_default_member and has_access %}
<th scope="col">
{% trans 'Action' %}
<th class="w-10" scope="col">
<span class="float-right">
{% trans 'Action' %}
</span>
</th>
{% endif %}
</tr>
@@ -44,7 +46,7 @@
{% for deduction in obj.deductions.all %}
<tr {% if deduction.account.deleted %}class="align-middle alert-danger" title="{% trans 'Eco-account deleted! Deduction invalid!' %}" {% elif not deduction.account.recorded %}class="align-middle alert-danger" title="{% trans 'Eco-account not recorded! Deduction invalid!' %}" {% endif %}>
<td class="align-middle">
<a href="{% url 'compensation:acc-detail' deduction.account.id %}">
<a href="{% url 'compensation:acc:detail' deduction.account.id %}">
{% if deduction.account.deleted or not deduction.account.recorded %}
{% fa5_icon 'exclamation-triangle' %}
{% endif %}
@@ -53,9 +55,12 @@
</td>
<td class="align-middle">{{ deduction.surface|floatformat:2|intcomma }} m²</td>
<td class="align-middle">{{ deduction.created.timestamp|default_if_none:""|naturalday}}</td>
<td>
<td class="align-middle float-right">
{% if is_default_member and has_access %}
<button data-form-url="{% url 'compensation:acc-remove-deduction' deduction.account.id deduction.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove Deduction' %}">
<button data-form-url="{% url 'intervention:edit-deduction' obj.id deduction.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit Deduction' %}">
{% fa5_icon 'edit' %}
</button>
<button data-form-url="{% url 'intervention:remove-deduction' obj.id deduction.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove Deduction' %}">
{% fa5_icon 'trash' %}
</button>
{% endif %}

View File

@@ -20,19 +20,24 @@
</div>
</div>
</div>
<div class="card-body scroll-300">
<div class="card-body scroll-300 p-2">
<table class="table table-hover">
<thead>
<tr>
<th scope="col">
{% trans 'Title' %}
</th>
<th scope="col">
{% trans 'Created on' %}
</th>
<th scope="col">
{% trans 'Comment' %}
</th>
{% if is_default_member and has_access %}
<th scope="col">
{% trans 'Action' %}
<th class="w-10" scope="col">
<span class="float-right">
{% trans 'Action' %}
</span>
</th>
{% endif %}
</tr>
@@ -40,15 +45,27 @@
<tbody>
{% for doc in obj.documents.all %}
<tr>
<td class="align-middle">
<a href="{% url 'intervention:get-doc' doc.id %}">
<td>
<a href="{% url 'intervention:get-doc' obj.id doc.id %}">
{{ doc.title }}
</a>
</td>
<td class="align-middle">{{ doc.comment }}</td>
<td>
<div class="scroll-150">
{{ doc.date_of_creation }}
</div>
</td>
<td>
<div class="scroll-150">
{{ doc.comment }}
</div>
</td>
<td class="align-middle float-right">
{% if is_default_member and has_access %}
<button data-form-url="{% url 'intervention:remove-doc' doc.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove document' %}">
<button data-form-url="{% url 'intervention:edit-doc' obj.id doc.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit document' %}">
{% fa5_icon 'edit' %}
</button>
<button data-form-url="{% url 'intervention:remove-doc' obj.id doc.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove document' %}">
{% fa5_icon 'trash' %}
</button>
{% endif %}

View File

@@ -11,7 +11,7 @@
<div class="col-sm-6">
<div class="d-flex justify-content-end">
{% if is_default_member and has_access %}
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:pay-new' obj.id %}" title="{% trans 'Add new payment' %}">
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:pay:new' obj.id %}" title="{% trans 'Add new payment' %}">
{% fa5_icon 'plus' %}
{% fa5_icon 'money-bill-wave' %}
</button>
@@ -20,7 +20,7 @@
</div>
</div>
</div>
<div class="card-body scroll-300">
<div class="card-body scroll-300 p-2">
<table class="table table-hover">
<thead>
<tr>
@@ -30,12 +30,14 @@
<th scope="col">
{% trans 'Due on' %}
</th>
<th scope="col">
<th class="w-50" scope="col">
{% trans 'Comment' %}
</th>
{% if is_default_member and has_access %}
<th scope="col">
{% trans 'Action' %}
<th class="w-10" scope="col">
<span class="float-right">
{% trans 'Action' %}
</span>
</th>
{% endif %}
</tr>
@@ -47,10 +49,17 @@
{{ pay.amount|floatformat:2 }} €
</td>
<td class="align-middle">{{ pay.due_on|default_if_none:"---" }}</td>
<td class="align-middle">{{ pay.comment }}</td>
<td>
<td class="align-middle">
<div class="scroll-150">
{{ pay.comment }}
</div>
</td>
<td class="align-middle float-right">
{% if is_default_member and has_access %}
<button data-form-url="{% url 'compensation:pay-remove' pay.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove payment' %}">
<button data-form-url="{% url 'compensation:pay:edit' obj.id pay.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit payment' %}">
{% fa5_icon 'edit' %}
</button>
<button data-form-url="{% url 'compensation:pay:remove' obj.id pay.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove payment' %}">
{% fa5_icon 'trash' %}
</button>
{% endif %}

View File

@@ -23,7 +23,7 @@
</div>
</div>
</div>
<div class="card-body scroll-300">
<div class="card-body scroll-300 p-2">
<table class="table table-hover">
<thead>
<tr>
@@ -37,8 +37,10 @@
{% trans 'Comment' %}
</th>
{% if is_default_member and has_access %}
<th scope="col">
{% trans 'Action' %}
<th class="w-10" scope="col">
<span class="float-right">
{% trans 'Action' %}
</span>
</th>
{% endif %}
</tr>
@@ -56,10 +58,17 @@
</a>
{% endif %}
</td>
<td class="align-middle">{{ rev.comment }}</td>
<td>
<td class="align-middle">
<div class="scroll-150">
{{ rev.comment }}
</div>
</td>
<td class="align-middle float-right">
{% if is_default_member and has_access %}
<button data-form-url="{% url 'intervention:remove-revocation' rev.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove revocation' %}">
<button data-form-url="{% url 'intervention:edit-revocation' obj.id rev.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit revocation' %}">
{% fa5_icon 'edit' %}
</button>
<button data-form-url="{% url 'intervention:remove-revocation' obj.id rev.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove revocation' %}">
{% fa5_icon 'trash' %}
</button>
{% endif %}

View File

@@ -14,16 +14,16 @@
{% block body %}
<div id="detail-header" class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
<h3>{% trans 'Intervention' %}<br> {{obj.identifier}}</h3>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
{% include 'intervention/detail/includes/controls.html' %}
</div>
</div>
<hr>
<div id="data" class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
<div class="table-container">
<table class="table table-hover">
<tr {% if not obj.title %}class="alert alert-danger" title="{% trans 'Missing' %}" {% endif %}>
@@ -122,7 +122,7 @@
</table>
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
<div class="row">
{% include 'map/geom_form.html' %}
</div>
@@ -136,27 +136,29 @@
</div>
<hr>
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
{% include 'intervention/detail/includes/compensations.html' %}
<div id="related_data">
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
{% include 'intervention/detail/includes/compensations.html' %}
</div>
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
{% include 'intervention/detail/includes/payments.html' %}
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
{% include 'intervention/detail/includes/payments.html' %}
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
{% include 'intervention/detail/includes/deductions.html' %}
</div>
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
{% include 'intervention/detail/includes/revocation.html' %}
</div>
</div>
</div>
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
{% include 'intervention/detail/includes/deductions.html' %}
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
{% include 'intervention/detail/includes/revocation.html' %}
</div>
</div>
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
{% include 'intervention/detail/includes/documents.html' %}
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
{% include 'intervention/detail/includes/documents.html' %}
</div>
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
</div>
</div>
</div>

View File

@@ -3,7 +3,7 @@
{% block body %}
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
<h3>{% trans 'Report' %}</h3>
<h4>{{obj.identifier}}</h4>
<div class="table-container">
@@ -62,7 +62,7 @@
<th scope="row">{% trans 'Deductions of eco-accounts' %}</th>
<td class="align-middle">
{% for deduction in deductions %}
<a href="{% url 'compensation:acc-report' deduction.account.id %}">
<a href="{% url 'compensation:acc:report' deduction.account.id %}">
{{deduction.account.identifier}} - {{deduction.account.title}}
</a>
<br>
@@ -96,7 +96,7 @@
</table>
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
<div class="row">
{% include 'map/geom_form.html' %}
</div>

View File

@@ -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 = {

View File

@@ -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()
@@ -210,11 +213,13 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase):
# Attention: Despite the fact, this url refers to a compensation app route, we test it here for the interventions.
# Reason: A payment is some kind of compensation for an intervention. Therefore it lives inside the compensation app.
# BUT: Payments are added on the intervention detail page. Therefore it's part of a regular intervention workflow.
new_payment_url = reverse("compensation:pay-new", args=(self.intervention.id,))
new_payment_url = reverse("compensation:pay:new", args=(self.intervention.id,))
# 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
@@ -390,7 +413,7 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase):
"""
# Create the url for creating a new deduction
new_url = reverse("compensation:acc-new-deduction", args=(self.eco_account.id,))
new_url = reverse("compensation:acc:new-deduction", args=(self.eco_account.id,))
# Prepare the form data
test_surface = 100.00
@@ -414,8 +437,10 @@ 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,))
delete_url = reverse("compensation:acc:remove-deduction", args=(self.eco_account.id, deduction.id,))
post_data = {
"confirm": True
}
@@ -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.

View File

@@ -9,7 +9,8 @@ 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
record_view, remove_document_view, get_document_view, get_revocation_view, new_id_view, report_view, \
remove_deduction_view, remove_compensation_view, edit_deduction_view, edit_revocation_view, edit_document_view
app_name = "intervention"
urlpatterns = [
@@ -26,16 +27,23 @@ urlpatterns = [
path('<id>/record', record_view, name='record'),
path('<id>/report', report_view, name='report'),
# Compensations
path('<id>/compensation/<comp_id>/remove', remove_compensation_view, name='remove-compensation'),
# Documents
path('<id>/document/new/', new_document_view, name='new-doc'),
path('document/<doc_id>', get_document_view, name='get-doc'),
path('document/<doc_id>/remove/', remove_document_view, name='remove-doc'),
path('<id>/document/<doc_id>', get_document_view, name='get-doc'),
path('<id>/document/<doc_id>/remove/', remove_document_view, name='remove-doc'),
path('<id>/document/<doc_id>/edit/', edit_document_view, name='edit-doc'),
# Deductions
path('<id>/deduction/new', new_deduction_view, name='acc-new-deduction'),
path('<id>/deduction/new', new_deduction_view, name='new-deduction'),
path('<id>/deduction/<deduction_id>/edit', edit_deduction_view, name='edit-deduction'),
path('<id>/deduction/<deduction_id>/remove', remove_deduction_view, name='remove-deduction'),
# Revocation routes
path('<id>/revocation/new', new_revocation_view, name='new-revocation'),
path('revocation/<id>/remove', remove_revocation_view, name='remove-revocation'),
path('<id>/revocation/<revocation_id>/edit', edit_revocation_view, name='edit-revocation'),
path('<id>/revocation/<revocation_id>/remove', remove_revocation_view, name='remove-revocation'),
path('revocation/<doc_id>', get_revocation_view, name='get-doc-revocation'),
]

View File

@@ -89,7 +89,10 @@ class InterventionQualityChecker(AbstractQualityChecker):
Returns:
"""
c_comps = self.obj.compensations.count()
comps = self.obj.compensations.filter(
deleted=None
)
c_comps = comps.count()
c_pays = self.obj.payments.count()
c_deducs = self.obj.deductions.count()
c_all = c_comps + c_pays + c_deducs

View File

@@ -1,21 +1,24 @@
from django.contrib.auth.decorators import login_required
from django.core.exceptions import ObjectDoesNotExist
from django.utils.translation import gettext_lazy as _
from django.http import HttpRequest, JsonResponse
from django.http import HttpRequest, JsonResponse, Http404
from django.shortcuts import render
from intervention.forms.forms import NewInterventionForm, EditInterventionForm
from intervention.forms.modalForms import ShareModalForm, NewRevocationModalForm, \
CheckModalForm, NewDeductionModalForm, 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
CHECKED_RECORDED_RESET, DEDUCTION_REMOVED, DEDUCTION_ADDED, REVOCATION_ADDED, REVOCATION_REMOVED, \
COMPENSATION_REMOVED_TEMPLATE, DOCUMENT_ADDED, DEDUCTION_EDITED, REVOCATION_EDITED, DOCUMENT_EDITED
from konova.utils.user_checks import in_group
@@ -126,10 +129,11 @@ 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")
msg_success=DOCUMENT_ADDED,
redirect_url=reverse("intervention:detail", args=(id,)) + "#related_data"
)
@@ -157,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,
@@ -206,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):
@@ -241,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,
@@ -337,21 +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)
form = RemoveModalForm(request.POST or None, instance=obj, request=request)
intervention = get_object_or_404(Intervention, id=id)
revocation = get_object_or_404(Revocation, id=revocation_id)
form = EditRevocationModalForm(request.POST or None, request.FILES or None, instance=intervention, revocation=revocation, request=request)
return form.process_request(
request,
_("Revocation removed"),
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=(intervention.id,)) + "#related_data"
)
@@ -455,7 +503,8 @@ def new_revocation_view(request: HttpRequest, id: str):
form = NewRevocationModalForm(request.POST or None, request.FILES or None, instance=intervention, request=request)
return form.process_request(
request,
msg_success=_("Revocation added")
msg_success=REVOCATION_ADDED,
redirect_url=reverse("intervention:detail", args=(id,)) + "#related_data"
)
@@ -502,7 +551,64 @@ def new_deduction_view(request: HttpRequest, id: str):
form = NewDeductionModalForm(request.POST or None, instance=intervention, request=request)
return form.process_request(
request,
msg_success=_("Deduction added")
msg_success=DEDUCTION_ADDED,
redirect_url=reverse("intervention:detail", args=(id,)) + "#related_data",
)
@login_required
@default_group_required
@shared_access_required(Intervention, "id")
def remove_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 = RemoveEcoAccountDeductionModalForm(request.POST or None, instance=intervention, deduction=eco_deduction, request=request)
return form.process_request(
request=request,
msg_success=DEDUCTION_REMOVED,
redirect_url=reverse("intervention:detail", args=(id,)) + "#related_data"
)
@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"
)
@@ -530,6 +636,31 @@ def record_view(request: HttpRequest, id: str):
)
def remove_compensation_view(request:HttpRequest, id: str, comp_id: str):
""" Renders a modal view for removing the compensation
Args:
request (HttpRequest): The incoming request
id (str): The compensation's id
Returns:
"""
intervention = get_object_or_404(Intervention, id=id)
try:
comp = intervention.compensations.get(
id=comp_id
)
except ObjectDoesNotExist:
raise Http404("Unknown compensation")
form = RemoveModalForm(request.POST or None, instance=comp, request=request)
return form.process_request(
request=request,
msg_success=COMPENSATION_REMOVED_TEMPLATE.format(comp.identifier),
redirect_url=reverse("intervention:detail", args=(id,)) + "#related_data",
)
def report_view(request:HttpRequest, id: str):
""" Renders the public report view

View File

@@ -8,6 +8,8 @@ Created on: 22.07.21
from django.contrib import admin
from konova.models import Geometry, Deadline, GeometryConflict, Parcel, District
from konova.utils.message_templates import COMPENSATION_REMOVED_TEMPLATE
from user.models import UserAction
class GeometryAdmin(admin.ModelAdmin):
@@ -78,6 +80,9 @@ class BaseObjectAdmin(BaseResourceAdmin):
"identifier",
"title",
]
actions = [
"restore_deleted_data"
]
def get_fields(self, request, obj=None):
return super().get_fields(request, obj) + ["deleted"]
@@ -87,6 +92,14 @@ class BaseObjectAdmin(BaseResourceAdmin):
"deleted",
]
def restore_deleted_data(self, request, queryset):
queryset = queryset.filter(
deleted__isnull=False
)
for entry in queryset:
entry.deleted.delete()
# Outcommented for a cleaner admin backend on production
#admin.site.register(Geometry, GeometryAdmin)

View File

@@ -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

View File

@@ -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
@@ -330,7 +333,24 @@ class RemoveModalForm(BaseModalForm):
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
@@ -515,7 +572,9 @@ class RecordModalForm(BaseModalForm):
Returns:
"""
comps = self.instance.compensations.all()
comps = self.instance.compensations.filter(
deleted=None,
)
comps_valid = True
for comp in comps:
checker = comp.quality_check()

View File

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

View File

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

View File

@@ -101,3 +101,19 @@ class AbstractDocument(BaseResource):
def is_file_size_valid(cls, _file):
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()

View File

@@ -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",

View File

@@ -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:
"""
action = UserActionLogEntry.get_edited_action(performing_user, edit_comment)
self.modified = action
self.log.add(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,6 +303,7 @@ class RecordableObjectMixin(models.Model):
request,
CHECKED_RECORDED_RESET
)
return action
@abstractmethod
def is_ready_for_publish(self) -> bool:
@@ -349,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
@@ -463,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)
@@ -479,6 +495,15 @@ class ShareableObjectMixin(models.Model):
"""
return self.users.all()
@abstractmethod
def get_share_url(self):
""" Returns the share url for the object
Returns:
"""
raise NotImplementedError("Must be implemented in subclasses!")
class GeoReferencedMixin(models.Model):
geometry = models.ForeignKey("konova.Geometry", null=True, blank=True, on_delete=models.SET_NULL)
@@ -520,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,
)

View File

@@ -22,7 +22,7 @@ class Parcel(UuidModel):
To avoid conflicts due to german Umlaute, the field names are shortened and vocals are dropped.
"""
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)

View File

@@ -219,16 +219,38 @@ 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
*/
.select2-container{
.select2{
width: 100% !important;
}
/*
Similar to bootstraps 'shadow-lg'
*/
.select2-results{
box-shadow: 0 1rem 3rem rgba(0,0,0,.2) !important;
}
.select2-results__option--highlighted{
background-color: var(--rlp-red) !important;
}
.select2-container--default .select2-results__group{
background-color: var(--rlp-gray-light);
}
.select2-container--default .select2-results__option .select2-results__option{
padding-left: 2em !important;
}
.select2-container--default .select2-results > .select2-results__options{
max-height: 500px !important;
}
.select2-container--default .select2-results__option .select2-results__option{
padding-left: 2em;
}

View File

@@ -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)

View File

@@ -4,7 +4,7 @@
{% trans 'Eco-account' %}
</h4>
<div class="row">
<a class="text-decoration-none" href="{% url 'compensation:acc-index' %}">
<a class="text-decoration-none" href="{% url 'compensation:acc:index' %}">
<div class="col-sm-5">
<div class="qs-box d-flex justify-content-center align-items-center">
{% fa5_icon 'tree' %}
@@ -26,12 +26,12 @@
<div class="col-sm-12 col-lg">
<div class="col-sm">
<div class="row my-1">
<a href="{% url 'compensation:acc-new' %}">
<a href="{% url 'compensation:acc:new' %}">
<button class="btn btn-default">{% fa5_icon 'plus' %} {% trans 'Create' %}</button>
</a>
</div>
<div class="row my-1">
<a href="{% url 'compensation:acc-index' %}">
<a href="{% url 'compensation:acc:index' %}">
<button class="btn btn-default">{% fa5_icon 'eye' %} {% trans 'Show' %}</button>
</a>
</div>

View File

@@ -52,3 +52,19 @@ def default_if_zero(val1, val2):
"""
return val1 if val1 > 0 else val2
@register.filter("shorten")
def shorten(val, length):
""" Returns val shortened to the first number of length character
Args:
val (str): The value
length (int): The number of characters left
Returns:
"""
if val is not None and len(val) > length:
val = f"{val[:length]}..."
return val

View File

@@ -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

View File

@@ -23,7 +23,7 @@ from konova.autocompletes import EcoAccountAutocomplete, \
ShareUserAutocomplete, BiotopeExtraCodeAutocomplete, CompensationActionDetailCodeAutocomplete
from konova.settings import SSO_SERVER, SSO_PUBLIC_KEY, SSO_PRIVATE_KEY, DEBUG
from konova.sso.sso import KonovaSSOClient
from konova.views import logout_view, home_view, remove_deadline_view
from konova.views import logout_view, home_view
sso_client = KonovaSSOClient(SSO_SERVER, SSO_PUBLIC_KEY, SSO_PRIVATE_KEY)
urlpatterns = [
@@ -40,9 +40,6 @@ urlpatterns = [
path('analysis/', include("analysis.urls")),
path('api/', include("api.urls")),
# Generic deadline routes
path('deadline/<id>/remove', remove_deadline_view, name="deadline-remove"),
# Autocomplete paths for all apps
path("atcmplt/eco-accounts", EcoAccountAutocomplete.as_view(), name="accounts-autocomplete"),
path("atcmplt/interventions", InterventionAutocomplete.as_view(), name="interventions-autocomplete"),

View File

@@ -10,6 +10,7 @@ from django.utils.translation import gettext_lazy as _
from konova.forms import RemoveModalForm
from konova.models import AbstractDocument
from konova.utils.message_templates import DOCUMENT_REMOVED_TEMPLATE
def get_document(doc: AbstractDocument):
@@ -49,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 '{}' deleted").format(title)
msg_success=DOCUMENT_REMOVED_TEMPLATE.format(title),
)

View File

@@ -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)

View File

@@ -18,14 +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")

View File

@@ -99,25 +99,6 @@ def home_view(request: HttpRequest):
return render(request, template, context)
@login_required
def remove_deadline_view(request: HttpRequest, id:str):
""" Renders a modal form for removing a deadline object
Args:
request (HttpRequest): The incoming request
id (str): The deadline id
Returns:
"""
deadline = get_object_or_404(Deadline, id=id)
form = RemoveModalForm(request.POST or None, instance=deadline, request=request)
return form.process_request(
request,
msg_success=_("Deadline removed")
)
def get_404_view(request: HttpRequest, exception=None):
""" Returns a 404 handling view

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -23,20 +23,19 @@
{% include 'navbars/navbar.html' %}
{% endblock %}
</header>
<div class="container-fluid mt-3 px-5">
<div class="">
<div class="col">
{% for message in messages %}
<div class="row alert alert-{{ message.tags }}">
{{ message }}
</div>
{% endfor %}
<div class="row alert alert-{{ message.tags }}">
{{ message }}
</div>
{% endfor %}
</div>
<div class="container-fluid mt-3 px-4">
{% comment %}
The modal wrapper, which can be used on every view can stay on the base.html template
{% endcomment %}
<div class="modal fade" tabindex="-1" role="dialog" id="modal">
<div class="modal-dialog modal-md modal-lg" role="document">
<div class="modal-dialog modal-md modal-xl" role="document">
<div class="modal-content"></div>
</div>
</div>

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More