Compare commits

...

24 Commits

Author SHA1 Message Date
d5accb2143 # Deadline tests refactored
* refactors tests for deadline views to check whether they work properly
2025-10-21 09:14:46 +02:00
6056a8882d # Deadline views refactored
* refactors AbstractDeadlineViews (new, edit, remove) to inherit from BaseModalFormView
* refactors KOM, OEK, EMA views for deadline views
2025-10-21 09:04:57 +02:00
c7a4c309bf # CompensationAction views refactored
* refactors AbstractCompensationActionViews (new, edit, remove) to inherit from BaseModalFormView
* refactors KOM, OEK, EMA views for compensation actions
* moves message template strings into message_templates.py
2025-10-21 08:48:30 +02:00
a7b23935a1 # RecordModalForm refactored
* refactors AbstractRecordModalForm
* refactors recording view for ema, intervention and eco account
2025-10-20 16:29:50 +02:00
97fbe02742 # BaseModalFormView refactoring
* extends BaseModalFormView to hold general logic for processing GET and POST requests for BaseModalForm endpoints
* refactors uuid check to use a specific parameter instead of kwargs
* fixes css bug where modal form input elements would not be visible
* refactors check view for intervention from function to class
* refactors DeductionViews to inherit from extended BaseModalFormView
2025-10-20 16:13:58 +02:00
ed5d571704 # Bugfix NewCompensationForm
* fixes bug where a form error would trigger a wrong error warning
2025-10-20 13:52:15 +02:00
a86d86b731 # NewEcoAccount EditEcoAccount view
* refactors new and edit eco account views from function to class based
* removes info message if checked intervention is altered and loses the current checked state
* updates comments/documentation
* removes code duplicates
* fixes display error where modal form was hidden behind menu bar of map client
* fixes bug where compensation could not be created directly from intervention
2025-10-20 09:49:18 +02:00
73178b3fd2 # NewCompensation EditCompensation view
* refactors new and edit compensation views from function to class based
* adds checked property to compensation to return parent-intervention's checked info
* fixes bug where compensation could be added to recorded intervention
* updates translations
2025-10-19 14:06:14 +02:00
278a951e92 # NewEma EditEma views
* refactors views for new ema and edit ema from function to class based
* moves shared access check to base edit form view to be checked for every inheriting class
* fixes bug where private variables changed on singleton objects
* updates translations
2025-10-19 13:10:22 +02:00
9e4a78ec60 # EditIntervention view
* refactors edit intervention view from function to class
2025-10-19 12:50:35 +02:00
d03b714fb5 # NewIntervention view
* introduces BaseFormView and BaseNewSpatialLocatedObjectFormView
* refactors new intervention view from function to class
2025-10-19 12:37:13 +02:00
a9b402862b # Detail View
* introduces BaseDetailView
* refactors detail views for EIV, KOM, OEK, EMA from function based to class based
* refactors already class based HomeView to inherit from new BaseView
2025-10-17 15:40:45 +02:00
61ec9c8c9b # ClientProxyView
* refactors login required from method decorator to mixin inheritance
2025-10-17 14:42:44 +02:00
f2baa054bf # Report view refactoring
* refactors function based report views into class based for EIV, OEK, EMA, KOM
* introduces BaseReportView for proper inheritance of shared logic
* refactors generating of qr codes into proper class
2025-10-17 11:07:16 +02:00
242730435e # Deduction views
* refactors deduction views on interventions and eco accounts from function to class based
* introduces basic checks on shared access and permission on BaseView on dispatching --> checks shall be overwritten on inheriting classes
2025-10-16 15:36:57 +02:00
afbdf221c3 # User view refactoring
* refactors majority of user views into class based views
* introduces BaseModalFormView and BaseView for even more generic usage
* renames url identifier user:index into user:detail for more clarity
2025-10-15 17:09:40 +02:00
be9f6f1b7e # Identifier Generator View EcoAccount refactoring
* refactors identifier generator view for ecoaccount
* simplifies base identifier generator view even further
2025-10-15 16:46:07 +02:00
80e8925a63 # Identifier Generator View Compensation refactoring
* refactors identifier generator view for compensation
2025-10-15 16:42:42 +02:00
c597e1934b # Identifier Generator View refactoring
* refactors identifier generator view for interventions
* simplifies same view for ema
2025-10-15 16:40:35 +02:00
a44d8658d4 # NewId Generator Ema refactoring
* introduces BaseNewIdentifierGeneratorView class
* refactors new identifier generator view for ema
2025-10-15 16:29:05 +02:00
bb71c0fcc8 # Index Ema refactoring
* refactors index view for ema
2025-10-15 16:14:03 +02:00
67acddf701 # Index EcoAccount refactoring
* refactors index view for eco account
2025-10-15 16:12:21 +02:00
21bb988d86 # Index Compensation refactoring
* refactors index view for compensations
2025-10-15 16:03:53 +02:00
1ceffccd40 # Index Intervention refactoring
* introduces BaseIndexView class
* refactors index view for interventions
2025-10-15 16:00:51 +02:00
57 changed files with 1709 additions and 1905 deletions

View File

View File

@@ -168,6 +168,17 @@ class NewCompensationForm(AbstractCompensationForm,
comp.log.add(action)
return comp, action
def is_valid(self):
valid = super().is_valid()
intervention = self.cleaned_data.get("intervention", None)
if intervention.is_recorded:
valid &= False
self.add_error(
"intervention",
_("This intervention is currently recorded. You cannot add further compensations as long as it is recorded.")
)
return valid
def save(self, user: User, geom_form: SimpleGeomForm):
with transaction.atomic():
comp, action = self.__create_comp(user)

View File

@@ -7,10 +7,12 @@ Created on: 18.08.22
"""
from dal import autocomplete
from django import forms
from django.shortcuts import get_object_or_404
from django.utils.translation import gettext_lazy as _
from codelist.models import KonovaCode
from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_COMPENSATION_ACTION_DETAIL_ID
from compensation.models import CompensationAction
from intervention.inputs import CompensationActionTreeCheckboxSelectMultiple
from konova.forms.modals import BaseModalForm, RemoveModalForm
from konova.utils.message_templates import COMPENSATION_ACTION_EDITED, ADDED_COMPENSATION_ACTION
@@ -114,7 +116,8 @@ class EditCompensationActionModalForm(NewCompensationActionModalForm):
action = None
def __init__(self, *args, **kwargs):
self.action = kwargs.pop("action", None)
action_id = kwargs.pop("action_id", None)
self.action = get_object_or_404(CompensationAction, id=action_id)
super().__init__(*args, **kwargs)
self.form_title = _("Edit action")
form_data = {
@@ -147,8 +150,8 @@ class RemoveCompensationActionModalForm(RemoveModalForm):
action = None
def __init__(self, *args, **kwargs):
action = kwargs.pop("action", None)
self.action = action
action_id = kwargs.pop("action_id", None)
self.action = get_object_or_404(CompensationAction, id=action_id)
super().__init__(*args, **kwargs)
def save(self):

View File

@@ -6,10 +6,11 @@ Created on: 18.08.22
"""
from django import forms
from django.shortcuts import get_object_or_404
from django.utils.translation import gettext_lazy as _
from konova.forms.modals import BaseModalForm
from konova.models import DeadlineType
from konova.models import DeadlineType, Deadline
from konova.utils import validators
from konova.utils.message_templates import DEADLINE_EDITED
@@ -90,7 +91,8 @@ class EditDeadlineModalForm(NewDeadlineModalForm):
deadline = None
def __init__(self, *args, **kwargs):
self.deadline = kwargs.pop("deadline", None)
deadline_id = kwargs.pop("deadline_id", None)
self.deadline = get_object_or_404(Deadline, id=deadline_id)
super().__init__(*args, **kwargs)
self.form_title = _("Edit deadline")
form_data = {

View File

@@ -80,7 +80,11 @@ class EditCompensationActionModalFormTestCase(NewCompensationActionModalFormTest
self.compensation.actions.add(self.comp_action)
def test_init(self):
form = EditCompensationActionModalForm(request=self.request, instance=self.compensation, action=self.comp_action)
form = EditCompensationActionModalForm(
request=self.request,
instance=self.compensation,
action_id=self.comp_action.id
)
self.assertEqual(form.form_title, str(_("Edit action")))
self.assertEqual(len(form.fields["action_type"].initial), self.comp_action.action_type.count())
self.assertEqual(len(form.fields["action_type_details"].initial), self.comp_action.action_type_details.count())
@@ -101,7 +105,7 @@ class EditCompensationActionModalFormTestCase(NewCompensationActionModalFormTest
"comment": comment,
}
form = EditCompensationActionModalForm(data, request=self.request, instance=self.compensation, action=self.comp_action)
form = EditCompensationActionModalForm(data, request=self.request, instance=self.compensation, action_id=self.comp_action.id)
self.assertTrue(form.is_valid())
action = form.save()
@@ -126,7 +130,7 @@ class RemoveCompensationActionModalFormTestCase(EditCompensationActionModalFormT
def test_init(self):
self.assertIn(self.comp_action, self.compensation.actions.all())
form = RemoveCompensationActionModalForm(request=self.request, instance=self.compensation, action=self.comp_action)
form = RemoveCompensationActionModalForm(request=self.request, instance=self.compensation, action_id=self.comp_action.id)
self.assertEqual(form.action, self.comp_action)
def test_save(self):
@@ -137,7 +141,7 @@ class RemoveCompensationActionModalFormTestCase(EditCompensationActionModalFormT
data,
request=self.request,
instance=self.compensation,
action=self.comp_action
action_id=self.comp_action.id
)
self.assertTrue(form.is_valid())
self.assertIn(self.comp_action, self.compensation.actions.all())

View File

@@ -36,7 +36,7 @@ class AbstractCompensationModelTestCase(BaseTestCase):
data,
request=self.request,
instance=self.compensation,
deadline=self.finished_deadline,
deadline_id=self.finished_deadline.id,
)
self.assertTrue(form.is_valid(), msg=form.errors)
self.assertIn(self.finished_deadline, self.compensation.deadlines.all())

View File

@@ -10,26 +10,27 @@ from django.urls import path
from compensation.views.compensation.document import EditCompensationDocumentView, NewCompensationDocumentView, \
GetCompensationDocumentView, RemoveCompensationDocumentView
from compensation.views.compensation.resubmission import CompensationResubmissionView
from compensation.views.compensation.report import report_view
from compensation.views.compensation.report import CompensationReportView
from compensation.views.compensation.deadline import NewCompensationDeadlineView, EditCompensationDeadlineView, \
RemoveCompensationDeadlineView
from compensation.views.compensation.action import NewCompensationActionView, EditCompensationActionView, \
RemoveCompensationActionView
from compensation.views.compensation.state import NewCompensationStateView, EditCompensationStateView, \
RemoveCompensationStateView
from compensation.views.compensation.compensation import index_view, new_view, new_id_view, detail_view, edit_view, \
remove_view
from compensation.views.compensation.compensation import \
remove_view, CompensationIndexView, CompensationIdentifierGeneratorView, CompensationDetailView, \
NewCompensationFormView, EditCompensationFormView
from compensation.views.compensation.log import CompensationLogView
urlpatterns = [
# Main compensation
path("", index_view, name="index"),
path('new/id', new_id_view, name='new-id'),
path('new/<intervention_id>', new_view, name='new'),
path('new', new_view, name='new'),
path('<id>', detail_view, name='detail'),
path("", CompensationIndexView.as_view(), name="index"),
path('new/id', CompensationIdentifierGeneratorView.as_view(), name='new-id'),
path('new/<intervention_id>', NewCompensationFormView.as_view(), name='new'),
path('new', NewCompensationFormView.as_view(), name='new'),
path('<id>', CompensationDetailView.as_view(), name='detail'),
path('<id>/log', CompensationLogView.as_view(), name='log'),
path('<id>/edit', edit_view, name='edit'),
path('<id>/edit', EditCompensationFormView.as_view(), name='edit'),
path('<id>/remove', remove_view, name='remove'),
path('<id>/state/new', NewCompensationStateView.as_view(), name='new-state'),
@@ -43,7 +44,7 @@ urlpatterns = [
path('<id>/deadline/new', NewCompensationDeadlineView.as_view(), name="new-deadline"),
path('<id>/deadline/<deadline_id>/edit', EditCompensationDeadlineView.as_view(), name='deadline-edit'),
path('<id>/deadline/<deadline_id>/remove', RemoveCompensationDeadlineView.as_view(), name='deadline-remove'),
path('<id>/report', report_view, name='report'),
path('<id>/report', CompensationReportView.as_view(), name='report'),
path('<id>/resub', CompensationResubmissionView.as_view(), name='resubmission-create'),
# Documents

View File

@@ -8,11 +8,12 @@ Created on: 24.08.21
from django.urls import path
from compensation.autocomplete.eco_account import EcoAccountAutocomplete
from compensation.views.eco_account.eco_account import index_view, new_view, new_id_view, edit_view, remove_view, \
detail_view
from compensation.views.eco_account.eco_account import remove_view, \
EcoAccountIndexView, EcoAccountIdentifierGeneratorView, EcoAccountDetailView, NewEcoAccountFormView, \
EditEcoAccountFormView
from compensation.views.eco_account.log import EcoAccountLogView
from compensation.views.eco_account.record import EcoAccountRecordView
from compensation.views.eco_account.report import report_view
from compensation.views.eco_account.report import EcoAccountReportView
from compensation.views.eco_account.resubmission import EcoAccountResubmissionView
from compensation.views.eco_account.state import NewEcoAccountStateView, EditEcoAccountStateView, \
RemoveEcoAccountStateView
@@ -28,14 +29,14 @@ from compensation.views.eco_account.deduction import NewEcoAccountDeductionView,
app_name = "acc"
urlpatterns = [
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("", EcoAccountIndexView.as_view(), name="index"),
path('new/', NewEcoAccountFormView.as_view(), name='new'),
path('new/id', EcoAccountIdentifierGeneratorView.as_view(), name='new-id'),
path('<id>', EcoAccountDetailView.as_view(), name='detail'),
path('<id>/log', EcoAccountLogView.as_view(), name='log'),
path('<id>/record', EcoAccountRecordView.as_view(), name='record'),
path('<id>/report', report_view, name='report'),
path('<id>/edit', edit_view, name='edit'),
path('<id>/report', EcoAccountReportView.as_view(), name='report'),
path('<id>/edit', EditEcoAccountFormView.as_view(), name='edit'),
path('<id>/remove', remove_view, name='remove'),
path('<id>/resub', EcoAccountResubmissionView.as_view(), name='resubmission-create'),

View File

@@ -5,53 +5,23 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22
"""
from django.contrib.auth.decorators import login_required
from django.http import HttpRequest
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.utils.decorators import method_decorator
from compensation.forms.modals.compensation_action import RemoveCompensationActionModalForm, \
EditCompensationActionModalForm, NewCompensationActionModalForm
from compensation.models import Compensation, CompensationAction
from konova.decorators import shared_access_required, default_group_required, login_required_modal
from konova.utils.message_templates import COMPENSATION_ACTION_REMOVED, COMPENSATION_ACTION_EDITED, \
COMPENSATION_ACTION_ADDED
from compensation.models import Compensation
from konova.views.action import AbstractNewCompensationActionView, AbstractEditCompensationActionView, \
AbstractRemoveCompensationActionView
_COMPENSATION_DETAIL_URL_NAME = "compensation:detail"
class NewCompensationActionView(AbstractNewCompensationActionView):
model = Compensation
redirect_url = "compensation:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Compensation, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
_MODEL_CLS = Compensation
_REDIRECT_URL = _COMPENSATION_DETAIL_URL_NAME
class EditCompensationActionView(AbstractEditCompensationActionView):
model = Compensation
redirect_url = "compensation:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Compensation, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
_MODEL_CLS = Compensation
_REDIRECT_URL = _COMPENSATION_DETAIL_URL_NAME
class RemoveCompensationActionView(AbstractRemoveCompensationActionView):
model = Compensation
redirect_url = "compensation:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Compensation, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
_MODEL_CLS = Compensation
_REDIRECT_URL = _COMPENSATION_DETAIL_URL_NAME

View File

@@ -7,8 +7,8 @@ Created on: 19.08.22
"""
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Sum
from django.http import HttpRequest, JsonResponse
from django.shortcuts import get_object_or_404, render, redirect
from django.urls import reverse
@@ -18,279 +18,150 @@ from compensation.forms.compensation import EditCompensationForm, NewCompensatio
from compensation.models import Compensation
from compensation.tables.compensation import CompensationTable
from intervention.models import Intervention
from konova.contexts import BaseContext
from konova.decorators import shared_access_required, default_group_required, any_group_check, login_required_modal, \
uuid_required
from konova.forms import SimpleGeomForm
from konova.decorators import shared_access_required, default_group_required, login_required_modal
from konova.forms.modals import RemoveModalForm
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.message_templates import COMPENSATION_REMOVED_TEMPLATE, DATA_CHECKED_PREVIOUSLY_TEMPLATE, \
RECORDED_BLOCKS_EDIT, CHECK_STATE_RESET, FORM_INVALID, PARAMS_INVALID, IDENTIFIER_REPLACED, \
COMPENSATION_ADDED_TEMPLATE, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE
RECORDED_BLOCKS_EDIT, PARAMS_INVALID
from konova.views.base import BaseIndexView, BaseIdentifierGeneratorView, BaseNewSpatialLocatedObjectFormView, \
BaseEditSpatialLocatedObjectFormView
from konova.views.detail import BaseDetailView
@login_required
@any_group_check
def index_view(request: HttpRequest):
"""
Renders the index view for compensation
class CompensationIndexView(LoginRequiredMixin, BaseIndexView):
_TAB_TITLE = _("Compensations - Overview")
_INDEX_TABLE_CLS = CompensationTable
Args:
request (HttpRequest): The incoming request
Returns:
A rendered view
"""
template = "generic_index.html"
compensations = Compensation.objects.filter(
deleted=None, # only show those which are not deleted individually
intervention__deleted=None, # and don't show the ones whose intervention has been deleted
).order_by(
"-modified__timestamp"
)
table = CompensationTable(
request=request,
queryset=compensations
)
context = {
"table": table,
TAB_TITLE_IDENTIFIER: _("Compensations - Overview"),
}
context = BaseContext(request, context).context
return render(request, template, context)
def _get_queryset(self):
qs = Compensation.objects.filter(
deleted=None, # only show those which are not deleted individually
intervention__deleted=None, # and don't show the ones whose intervention has been deleted
).order_by(
"-modified__timestamp"
)
return qs
@login_required
@default_group_required
@shared_access_required(Intervention, "intervention_id")
def new_view(request: HttpRequest, intervention_id: str = None):
"""
Renders a view for a new compensation creation
class NewCompensationFormView(BaseNewSpatialLocatedObjectFormView):
_FORM_CLS = NewCompensationForm
_MODEL_CLS = Compensation
_TEMPLATE = "compensation/form/view.html"
_TAB_TITLE = _("New Compensation")
_REDIRECT_URL = "compensation:detail"
Args:
request (HttpRequest): The incoming request
def _user_has_shared_access(self, user, **kwargs):
# On a new compensation make sure the intervention (if call came directly through an intervention's detail
# view) is shared with the user
intervention_id = kwargs.get("intervention_id", None)
if not intervention_id:
return True
else:
intervention = get_object_or_404(Intervention, id=intervention_id)
return intervention.is_shared_with(user)
Returns:
def _user_has_permission(self, user):
# User has to be an ets user
return user.is_default_user()
"""
template = "compensation/form/view.html"
if intervention_id is not None:
try:
intervention = Intervention.objects.get(id=intervention_id)
except ObjectDoesNotExist:
messages.error(request, PARAMS_INVALID)
return redirect("home")
if intervention.is_recorded:
messages.info(
request,
RECORDED_BLOCKS_EDIT
)
return redirect("intervention:detail", id=intervention_id)
data_form = NewCompensationForm(request.POST or None, intervention_id=intervention_id)
geom_form = SimpleGeomForm(request.POST or None, read_only=False)
if request.method == "POST":
if data_form.is_valid() and geom_form.is_valid():
generated_identifier = data_form.cleaned_data.get("identifier", None)
comp = data_form.save(request.user, geom_form)
if generated_identifier != comp.identifier:
messages.info(
request,
IDENTIFIER_REPLACED.format(
generated_identifier,
comp.identifier
def dispatch(self, request, *args, **kwargs):
# Make sure there is an existing intervention based on the given id
# Compensations can not exist without an intervention
intervention_id = kwargs.get("intervention_id", None)
if intervention_id:
try:
intervention = Intervention.objects.get(id=intervention_id)
if intervention.is_recorded:
messages.info(
request,
RECORDED_BLOCKS_EDIT
)
)
messages.success(request, COMPENSATION_ADDED_TEMPLATE.format(comp.identifier))
if geom_form.has_geometry_simplified():
messages.info(
request,
GEOMETRY_SIMPLIFIED
)
num_ignored_geometries = geom_form.get_num_geometries_ignored()
if num_ignored_geometries > 0:
messages.info(
request,
GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries)
)
return redirect("compensation:detail", id=comp.id)
else:
messages.error(request, FORM_INVALID, extra_tags="danger",)
else:
# For clarification: nothing in this case
pass
context = {
"form": data_form,
"geom_form": geom_form,
TAB_TITLE_IDENTIFIER: _("New compensation"),
}
context = BaseContext(request, context).context
return render(request, template, context)
return redirect("intervention:detail", id=intervention_id)
except ObjectDoesNotExist:
messages.error(request, PARAMS_INVALID, extra_tags="danger")
return redirect("home")
return super().dispatch(request, *args, **kwargs)
@login_required
@default_group_required
def new_id_view(request: HttpRequest):
""" JSON endpoint
class EditCompensationFormView(BaseEditSpatialLocatedObjectFormView):
_MODEL_CLS = Compensation
_FORM_CLS = EditCompensationForm
_TEMPLATE = "compensation/form/view.html"
_REDIRECT_URL = "compensation:detail"
Provides fetching of free identifiers for e.g. AJAX calls
def _user_has_permission(self, user):
# User has to be a default user
return user.is_default_user()
"""
tmp = Compensation()
identifier = tmp.generate_new_identifier()
while Compensation.objects.filter(identifier=identifier).exists():
identifier = tmp.generate_new_identifier()
return JsonResponse(
data={
"gen_data": identifier
class CompensationIdentifierGeneratorView(LoginRequiredMixin, BaseIdentifierGeneratorView):
_MODEL_CLS = Compensation
_REDIRECT_URL = "compensation:index"
class CompensationDetailView(BaseDetailView):
_MODEL_CLS = Compensation
_TEMPLATE = "compensation/detail/compensation/view.html"
def _get_object(self, id: str):
""" Returns the compensation
Args:
id (str): The compensation's id
Returns:
obj (Compensation): The compensation
"""
comp = get_object_or_404(
Compensation.objects.select_related(
"modified",
"created",
"geometry"
),
id=id,
deleted=None,
intervention__deleted=None,
)
return comp
def _get_detail_context(self, obj: Compensation):
""" Generate object specific detail context for view
Args:
obj (): The record
Returns:
"""
# Order states according to surface
before_states = obj.before_states.all().prefetch_related("biotope_type").order_by("-surface")
after_states = obj.after_states.all().prefetch_related("biotope_type").order_by("-surface")
actions = obj.actions.all().prefetch_related("action_type")
# Precalculate logical errors between before- and after-states
# Sum() returns None in case of no states, so we catch that and replace it with 0 for easier handling
sum_before_states = obj.get_surface_before_states()
sum_after_states = obj.get_surface_after_states()
diff_states = abs(sum_before_states - sum_after_states)
last_checked = obj.intervention.get_last_checked_action()
last_checked_tooltip = ""
if last_checked:
last_checked_tooltip = DATA_CHECKED_PREVIOUSLY_TEMPLATE.format(
last_checked.get_timestamp_str_formatted(),
last_checked.user
)
context = {
"last_checked": last_checked,
"last_checked_tooltip": last_checked_tooltip,
"actions": actions,
"before_states": before_states,
"after_states": after_states,
"sum_before_states": sum_before_states,
"sum_after_states": sum_after_states,
"diff_states": diff_states,
"has_finished_deadlines": obj.get_finished_deadlines().exists(),
}
)
@login_required
@default_group_required
@shared_access_required(Compensation, "id")
def edit_view(request: HttpRequest, id: str):
"""
Renders a view for editing compensations
Args:
request (HttpRequest): The incoming request
Returns:
"""
template = "compensation/form/view.html"
# Get object from db
comp = get_object_or_404(Compensation, id=id)
if comp.is_recorded:
messages.info(
request,
RECORDED_BLOCKS_EDIT
)
return redirect("compensation:detail", id=id)
# Create forms, initialize with values from db/from POST request
data_form = EditCompensationForm(request.POST or None, instance=comp)
geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=comp)
if request.method == "POST":
if data_form.is_valid() and geom_form.is_valid():
# Preserve state of intervention checked to determine whether the user must be informed or not
# about a change of the check state
intervention_is_checked = comp.intervention.checked is not None
# The data form takes the geom form for processing, as well as the performing user
comp = data_form.save(request.user, geom_form)
if intervention_is_checked:
messages.info(request, CHECK_STATE_RESET)
messages.success(request, _("Compensation {} edited").format(comp.identifier))
if geom_form.has_geometry_simplified():
messages.info(
request,
GEOMETRY_SIMPLIFIED
)
num_ignored_geometries = geom_form.get_num_geometries_ignored()
if num_ignored_geometries > 0:
messages.info(
request,
GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries)
)
return redirect("compensation:detail", id=comp.id)
else:
messages.error(request, FORM_INVALID, extra_tags="danger",)
else:
# For clarification: nothing in this case
pass
context = {
"form": data_form,
"geom_form": geom_form,
TAB_TITLE_IDENTIFIER: _("Edit {}").format(comp.identifier),
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required
@any_group_check
@uuid_required
def detail_view(request: HttpRequest, id: str):
""" Renders a detail view for a compensation
Args:
request (HttpRequest): The incoming request
id (str): The compensation's id
Returns:
"""
template = "compensation/detail/compensation/view.html"
comp = get_object_or_404(
Compensation.objects.select_related(
"modified",
"created",
"geometry"
),
id=id,
deleted=None,
intervention__deleted=None,
)
geom_form = SimpleGeomForm(instance=comp)
parcels = comp.get_underlying_parcels()
_user = request.user
is_data_shared = comp.intervention.is_shared_with(_user)
# Order states according to surface
before_states = comp.before_states.all().prefetch_related("biotope_type").order_by("-surface")
after_states = comp.after_states.all().prefetch_related("biotope_type").order_by("-surface")
actions = comp.actions.all().prefetch_related("action_type")
# Precalculate logical errors between before- and after-states
# Sum() returns None in case of no states, so we catch that and replace it with 0 for easier handling
sum_before_states = comp.get_surface_before_states()
sum_after_states = comp.get_surface_after_states()
diff_states = abs(sum_before_states - sum_after_states)
request = comp.set_status_messages(request)
last_checked = comp.intervention.get_last_checked_action()
last_checked_tooltip = ""
if last_checked:
last_checked_tooltip = DATA_CHECKED_PREVIOUSLY_TEMPLATE.format(last_checked.get_timestamp_str_formatted(), last_checked.user)
requesting_user_is_only_shared_user = comp.is_only_shared_with(_user)
if requesting_user_is_only_shared_user:
messages.info(
request,
DO_NOT_FORGET_TO_SHARE
)
context = {
"obj": comp,
"last_checked": last_checked,
"last_checked_tooltip": last_checked_tooltip,
"geom_form": geom_form,
"parcels": parcels,
"is_entry_shared": is_data_shared,
"actions": actions,
"before_states": before_states,
"after_states": after_states,
"sum_before_states": sum_before_states,
"sum_after_states": sum_after_states,
"diff_states": diff_states,
"is_default_member": _user.in_group(DEFAULT_GROUP),
"is_zb_member": _user.in_group(ZB_GROUP),
"is_ets_member": _user.in_group(ETS_GROUP),
"LANIS_LINK": comp.get_LANIS_link(),
TAB_TITLE_IDENTIFIER: f"{comp.identifier} - {comp.title}",
"has_finished_deadlines": comp.get_finished_deadlines().exists(),
}
context = BaseContext(request, context).context
return render(request, template, context)
return context
@login_required_modal

View File

@@ -5,45 +5,21 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22
"""
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from compensation.models import Compensation
from konova.decorators import shared_access_required, default_group_required, login_required_modal
from konova.views.deadline import AbstractRemoveDeadlineView, AbstractEditDeadlineView, AbstractNewDeadlineView
_COMPENSATION_DETAIL_URL_NAME = "compensation:detail"
class NewCompensationDeadlineView(AbstractNewDeadlineView):
model = Compensation
redirect_url = "compensation:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Compensation, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
_MODEL_CLS = Compensation
_REDIRECT_URL = _COMPENSATION_DETAIL_URL_NAME
class EditCompensationDeadlineView(AbstractEditDeadlineView):
model = Compensation
redirect_url = "compensation:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Compensation, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
_MODEL_CLS = Compensation
_REDIRECT_URL = _COMPENSATION_DETAIL_URL_NAME
class RemoveCompensationDeadlineView(AbstractRemoveDeadlineView):
model = Compensation
redirect_url = "compensation:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Compensation, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
_MODEL_CLS = Compensation
_REDIRECT_URL = _COMPENSATION_DETAIL_URL_NAME

View File

@@ -5,77 +5,48 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22
"""
from django.http import HttpRequest
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from compensation.models import Compensation
from konova.contexts import BaseContext
from konova.decorators import uuid_required
from konova.forms import SimpleGeomForm
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.generators import generate_qr_code
from konova.sub_settings.django_settings import BASE_URL
from konova.utils.qrcode import QrCode
from konova.views.report import BaseReportView
@uuid_required
def report_view(request: HttpRequest, id: str):
""" Renders the public report view
Args:
request (HttpRequest): The incoming request
id (str): The id of the intervention
class BaseCompensationReportView(BaseReportView):
def _get_compensation_report_context(self, obj):
# Order states by surface
before_states = obj.before_states.all().order_by("-surface").prefetch_related("biotope_type")
after_states = obj.after_states.all().order_by("-surface").prefetch_related("biotope_type")
actions = obj.actions.all().prefetch_related("action_type")
Returns:
"""
# Reuse the compensation report template since compensations are structurally identical
template = "compensation/report/compensation/report.html"
comp = get_object_or_404(Compensation, id=id)
tab_title = _("Report {}").format(comp.identifier)
# If intervention is not recorded (yet or currently) we need to render another template without any data
if not comp.is_ready_for_publish():
template = "report/unavailable.html"
context = {
TAB_TITLE_IDENTIFIER: tab_title,
return {
"before_states": before_states,
"after_states": after_states,
"actions": actions,
}
context = BaseContext(request, context).context
return render(request, template, context)
# Prepare data for map viewer
geom_form = SimpleGeomForm(
instance=comp
)
parcels = comp.get_underlying_parcels()
qrcode_url = request.build_absolute_uri(reverse("compensation:report", args=(id,)))
qrcode_img = generate_qr_code(qrcode_url, 10)
qrcode_lanis_url = comp.get_LANIS_link()
qrcode_img_lanis = generate_qr_code(qrcode_lanis_url, 7)
class CompensationReportView(BaseCompensationReportView):
_MODEL = Compensation
_TEMPLATE = "compensation/report/compensation/report.html"
# Order states by surface
before_states = comp.before_states.all().order_by("-surface").prefetch_related("biotope_type")
after_states = comp.after_states.all().order_by("-surface").prefetch_related("biotope_type")
actions = comp.actions.all().prefetch_related("action_type")
def _get_report_context(self, obj):
report_url = BASE_URL + reverse("compensation:report", args=(obj.id,))
qrcode_report = QrCode(report_url, 10)
qrcode_lanis = QrCode(obj.get_LANIS_link(), 7)
context = {
"obj": comp,
"qrcode": {
"img": qrcode_img,
"url": qrcode_url,
},
"qrcode_lanis": {
"img": qrcode_img_lanis,
"url": qrcode_lanis_url,
},
"is_entry_shared": False, # disables action buttons during rendering
"before_states": before_states,
"after_states": after_states,
"geom_form": geom_form,
"parcels": parcels,
"actions": actions,
"tables_scrollable": False,
TAB_TITLE_IDENTIFIER: tab_title,
}
context = BaseContext(request, context).context
return render(request, template, context)
report_context = {
"qrcode": {
"img": qrcode_report.get_img(),
"url": qrcode_report.get_content(),
},
"qrcode_lanis": {
"img": qrcode_lanis.get_img(),
"url": qrcode_lanis.get_content(),
},
"is_entry_shared": False, # disables action buttons during rendering
"tables_scrollable": False,
}
report_context.update(self._get_compensation_report_context(obj))
return report_context

View File

@@ -5,46 +5,22 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22
"""
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from compensation.models import EcoAccount
from konova.decorators import shared_access_required, default_group_required, login_required_modal
from konova.views.action import AbstractNewCompensationActionView, AbstractEditCompensationActionView, \
AbstractRemoveCompensationActionView
_ECO_ACCOUNT_DETAIL_URL_NAME = "compensation:acc:detail"
class NewEcoAccountActionView(AbstractNewCompensationActionView):
model = EcoAccount
redirect_url = "compensation:acc:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(EcoAccount, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
_MODEL_CLS = EcoAccount
_REDIRECT_URL = _ECO_ACCOUNT_DETAIL_URL_NAME
class EditEcoAccountActionView(AbstractEditCompensationActionView):
model = EcoAccount
redirect_url = "compensation:acc:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(EcoAccount, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
_MODEL_CLS = EcoAccount
_REDIRECT_URL = _ECO_ACCOUNT_DETAIL_URL_NAME
class RemoveEcoAccountActionView(AbstractRemoveCompensationActionView):
model = EcoAccount
redirect_url = "compensation:acc:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(EcoAccount, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
_MODEL_CLS = EcoAccount
_REDIRECT_URL = _ECO_ACCOUNT_DETAIL_URL_NAME

View File

@@ -5,45 +5,22 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22
"""
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from compensation.models import EcoAccount
from konova.decorators import shared_access_required, default_group_required, login_required_modal
from konova.views.deadline import AbstractNewDeadlineView, AbstractEditDeadlineView, AbstractRemoveDeadlineView
_ECO_ACCOUNT_DETAIL_URL_NAME = "compensation:acc:detail"
class NewEcoAccountDeadlineView(AbstractNewDeadlineView):
model = EcoAccount
redirect_url = "compensation:acc:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(EcoAccount, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
_MODEL_CLS = EcoAccount
_REDIRECT_URL = _ECO_ACCOUNT_DETAIL_URL_NAME
class EditEcoAccountDeadlineView(AbstractEditDeadlineView):
model = EcoAccount
redirect_url = "compensation:acc:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(EcoAccount, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
_MODEL_CLS = EcoAccount
_REDIRECT_URL = _ECO_ACCOUNT_DETAIL_URL_NAME
class RemoveEcoAccountDeadlineView(AbstractRemoveDeadlineView):
model = EcoAccount
redirect_url = "compensation:acc:detail"
_MODEL_CLS = EcoAccount
_REDIRECT_URL = _ECO_ACCOUNT_DETAIL_URL_NAME
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(EcoAccount, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)

View File

@@ -5,54 +5,29 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22
"""
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import Http404
from django.utils.decorators import method_decorator
from compensation.models import EcoAccount
from konova.decorators import default_group_required, login_required_modal
from konova.views.deduction import AbstractNewDeductionView, AbstractEditDeductionView, AbstractRemoveDeductionView
_ECO_ACCOUNT_DETAIl_URL_NAME = "compensation:acc:detail"
class NewEcoAccountDeductionView(AbstractNewDeductionView):
model = EcoAccount
redirect_url = "compensation:acc:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
class NewEcoAccountDeductionView(LoginRequiredMixin, AbstractNewDeductionView):
_MODEL_CLS = EcoAccount
_REDIRECT_URL = _ECO_ACCOUNT_DETAIl_URL_NAME
def _custom_check(self, obj):
# New deductions can only be created if the eco account has been recorded
if not obj.recorded:
raise Http404()
class EditEcoAccountDeductionView(AbstractEditDeductionView):
def _custom_check(self, obj):
pass
model = EcoAccount
redirect_url = "compensation:acc:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
class EditEcoAccountDeductionView(LoginRequiredMixin, AbstractEditDeductionView):
_MODEL_CLS = EcoAccount
_REDIRECT_URL = _ECO_ACCOUNT_DETAIl_URL_NAME
class RemoveEcoAccountDeductionView(AbstractRemoveDeductionView):
def _custom_check(self, obj):
pass
model = EcoAccount
redirect_url = "compensation:acc:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
class RemoveEcoAccountDeductionView(LoginRequiredMixin, AbstractRemoveDeductionView):
_MODEL_CLS = EcoAccount
_REDIRECT_URL = _ECO_ACCOUNT_DETAIl_URL_NAME

View File

@@ -7,8 +7,8 @@ Created on: 19.08.22
"""
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.db.models import Sum
from django.http import HttpRequest, JsonResponse
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpRequest
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
@@ -17,43 +17,51 @@ from compensation.forms.eco_account import EditEcoAccountForm, NewEcoAccountForm
from compensation.models import EcoAccount
from compensation.tables.eco_account import EcoAccountTable
from konova.contexts import BaseContext
from konova.decorators import shared_access_required, default_group_required, any_group_check, login_required_modal, \
uuid_required
from konova.decorators import shared_access_required, default_group_required, login_required_modal
from konova.forms import SimpleGeomForm
from konova.settings import ETS_GROUP, DEFAULT_GROUP, ZB_GROUP
from konova.settings import ETS_GROUP
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.message_templates import CANCEL_ACC_RECORDED_OR_DEDUCTED, RECORDED_BLOCKS_EDIT, FORM_INVALID, \
IDENTIFIER_REPLACED, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE
IDENTIFIER_REPLACED, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE
from konova.views.base import BaseIndexView, BaseIdentifierGeneratorView, BaseNewSpatialLocatedObjectFormView, \
BaseEditSpatialLocatedObjectFormView
from konova.views.detail import BaseDetailView
@login_required
@any_group_check
def index_view(request: HttpRequest):
"""
Renders the index view for eco accounts
class EcoAccountIndexView(LoginRequiredMixin, BaseIndexView):
_INDEX_TABLE_CLS = EcoAccountTable
_TAB_TITLE = _("Eco-account - Overview")
Args:
request (HttpRequest): The incoming request
def _get_queryset(self):
qs = EcoAccount.objects.filter(
deleted=None,
).order_by(
"-modified__timestamp"
)
return qs
Returns:
A rendered view
"""
template = "generic_index.html"
eco_accounts = EcoAccount.objects.filter(
deleted=None,
).order_by(
"-modified__timestamp"
)
table = EcoAccountTable(
request=request,
queryset=eco_accounts
)
context = {
"table": table,
TAB_TITLE_IDENTIFIER: _("Eco-account - Overview"),
}
context = BaseContext(request, context).context
return render(request, template, context)
class NewEcoAccountFormView(BaseNewSpatialLocatedObjectFormView):
_FORM_CLS = NewEcoAccountForm
_MODEL_CLS = EcoAccount
_TEMPLATE = "compensation/form/view.html"
_TAB_TITLE = _("New Eco-Account")
_REDIRECT_URL = "compensation:acc:detail"
def _user_has_permission(self, user):
# User has to be a default user
return user.is_default_user()
class EditEcoAccountFormView(BaseEditSpatialLocatedObjectFormView):
_FORM_CLS = EditEcoAccountForm
_MODEL_CLS = EcoAccount
_TEMPLATE = "compensation/form/view.html"
_REDIRECT_URL = "compensation:acc:detail"
def _user_has_permission(self, user):
# User has to be a default user
return user.is_default_user()
@login_required
@@ -112,23 +120,9 @@ def new_view(request: HttpRequest):
return render(request, template, context)
@login_required
@default_group_required
def new_id_view(request: HttpRequest):
""" JSON endpoint
Provides fetching of free identifiers for e.g. AJAX calls
"""
tmp = EcoAccount()
identifier = tmp.generate_new_identifier()
while EcoAccount.objects.filter(identifier=identifier).exists():
identifier = tmp.generate_new_identifier()
return JsonResponse(
data={
"gen_data": identifier
}
)
class EcoAccountIdentifierGeneratorView(LoginRequiredMixin, BaseIdentifierGeneratorView):
_MODEL_CLS = EcoAccount
_REDIRECT_URL = "compensation:acc:index"
@login_required
@@ -192,86 +186,72 @@ def edit_view(request: HttpRequest, id: str):
return render(request, template, context)
@login_required
@any_group_check
@uuid_required
def detail_view(request: HttpRequest, id: str):
""" Renders a detail view for a compensation
class EcoAccountDetailView(BaseDetailView):
_MODEL_CLS = EcoAccount
_TEMPLATE = "compensation/detail/eco_account/view.html"
Args:
request (HttpRequest): The incoming request
id (str): The compensation's id
def _get_object(self, id: str):
""" Fetch object for detail view
Returns:
Args:
id (str): The record's id'
"""
template = "compensation/detail/eco_account/view.html"
acc = get_object_or_404(
EcoAccount.objects.prefetch_related(
"deadlines",
).select_related(
'geometry',
'responsible',
),
id=id,
deleted=None,
)
geom_form = SimpleGeomForm(instance=acc)
parcels = acc.get_underlying_parcels()
_user = request.user
is_data_shared = acc.is_shared_with(_user)
Returns:
# Order states according to surface
before_states = acc.before_states.order_by("-surface")
after_states = acc.after_states.order_by("-surface")
# Precalculate logical errors between before- and after-states
# Sum() returns None in case of no states, so we catch that and replace it with 0 for easier handling
sum_before_states = acc.get_surface_before_states()
sum_after_states = acc.get_surface_after_states()
diff_states = abs(sum_before_states - sum_after_states)
# Calculate rest of available surface for deductions
available_total = acc.deductable_rest
available_relative = acc.get_deductable_rest_relative()
# Prefetch related data to decrease the amount of db connections
deductions = acc.deductions.filter(
intervention__deleted=None,
)
actions = acc.actions.all()
request = acc.set_status_messages(request)
requesting_user_is_only_shared_user = acc.is_only_shared_with(_user)
if requesting_user_is_only_shared_user:
messages.info(
request,
DO_NOT_FORGET_TO_SHARE
"""
acc = get_object_or_404(
EcoAccount.objects.prefetch_related(
"deadlines",
).select_related(
'geometry',
'responsible',
),
id=id,
deleted=None,
)
return acc
context = {
"obj": acc,
"geom_form": geom_form,
"parcels": parcels,
"is_entry_shared": is_data_shared,
"before_states": before_states,
"after_states": after_states,
"sum_before_states": sum_before_states,
"sum_after_states": sum_after_states,
"diff_states": diff_states,
"available": available_relative,
"available_total": available_total,
"is_default_member": _user.in_group(DEFAULT_GROUP),
"is_zb_member": _user.in_group(ZB_GROUP),
"is_ets_member": _user.in_group(ETS_GROUP),
"LANIS_LINK": acc.get_LANIS_link(),
"deductions": deductions,
"actions": actions,
TAB_TITLE_IDENTIFIER: f"{acc.identifier} - {acc.title}",
"has_finished_deadlines": acc.get_finished_deadlines().exists(),
}
context = BaseContext(request, context).context
return render(request, template, context)
def _get_detail_context(self, obj: EcoAccount):
""" Generate object specific detail context for view
Args:
obj (): The record
Returns:
"""
# Order states according to surface
before_states = obj.before_states.order_by("-surface")
after_states = obj.after_states.order_by("-surface")
# Precalculate logical errors between before- and after-states
# Sum() returns None in case of no states, so we catch that and replace it with 0 for easier handling
sum_before_states = obj.get_surface_before_states()
sum_after_states = obj.get_surface_after_states()
diff_states = abs(sum_before_states - sum_after_states)
# Calculate rest of available surface for deductions
available_total = obj.deductable_rest
available_relative = obj.get_deductable_rest_relative()
# Prefetch related data to decrease the amount of db connections
deductions = obj.deductions.filter(
intervention__deleted=None,
)
actions = obj.actions.all()
context = {
"before_states": before_states,
"after_states": after_states,
"sum_before_states": sum_before_states,
"sum_after_states": sum_after_states,
"diff_states": diff_states,
"available": available_relative,
"available_total": available_total,
"deductions": deductions,
"actions": actions,
"has_finished_deadlines": obj.get_finished_deadlines().exists(),
}
return context
@login_required_modal

View File

@@ -5,20 +5,12 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22
"""
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from django.contrib.auth.mixins import LoginRequiredMixin
from compensation.models import EcoAccount
from konova.decorators import shared_access_required, conservation_office_group_required, login_required_modal
from konova.views.record import AbstractRecordView
class EcoAccountRecordView(AbstractRecordView):
model = EcoAccount
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(conservation_office_group_required)
@method_decorator(shared_access_required(EcoAccount, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
class EcoAccountRecordView(LoginRequiredMixin, AbstractRecordView):
_MODEL_CLS = EcoAccount
_REDIRECT_URL = "compensation:acc:detail"

View File

@@ -5,85 +5,41 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22
"""
from django.http import HttpRequest
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from compensation.models import EcoAccount
from konova.contexts import BaseContext
from konova.decorators import uuid_required
from konova.forms import SimpleGeomForm
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.generators import generate_qr_code
from compensation.views.compensation.report import BaseCompensationReportView
from konova.sub_settings.django_settings import BASE_URL
from konova.utils.qrcode import QrCode
@uuid_required
def report_view(request: HttpRequest, id: str):
""" Renders the public report view
class EcoAccountReportView(BaseCompensationReportView):
_MODEL = EcoAccount
_TEMPLATE = "compensation/report/eco_account/report.html"
Args:
request (HttpRequest): The incoming request
id (str): The id of the intervention
def _get_report_context(self, obj):
report_url = BASE_URL + reverse("compensation:acc:report", args=(obj.id,))
qrcode_report = QrCode(report_url, 10)
qrcode_lanis = QrCode(obj.get_LANIS_link(), 7)
Returns:
# Reduce amount of db fetched data to the bare minimum we need in the template (deduction's intervention id and identifier)
deductions = obj.deductions.all() \
.distinct("intervention") \
.select_related("intervention") \
.values_list("intervention__id", "intervention__identifier", "intervention__title", named=True)
"""
# Reuse the compensation report template since EcoAccounts are structurally identical
template = "compensation/report/eco_account/report.html"
acc = get_object_or_404(EcoAccount, id=id)
tab_title = _("Report {}").format(acc.identifier)
# If intervention is not recorded (yet or currently) we need to render another template without any data
if not acc.is_ready_for_publish():
template = "report/unavailable.html"
context = {
TAB_TITLE_IDENTIFIER: tab_title,
report_context = {
"qrcode": {
"img": qrcode_report.get_img(),
"url": qrcode_report.get_content(),
},
"qrcode_lanis": {
"img": qrcode_lanis.get_img(),
"url": qrcode_lanis.get_content(),
},
"is_entry_shared": False, # disables action buttons during rendering
"deductions": deductions,
"tables_scrollable": False,
}
context = BaseContext(request, context).context
return render(request, template, context)
# Prepare data for map viewer
geom_form = SimpleGeomForm(
instance=acc
)
parcels = acc.get_underlying_parcels()
qrcode_url = request.build_absolute_uri(reverse("compensation:acc:report", args=(id,)))
qrcode_img = generate_qr_code(qrcode_url, 10)
qrcode_lanis_url = acc.get_LANIS_link()
qrcode_img_lanis = generate_qr_code(qrcode_lanis_url, 7)
# Order states by surface
before_states = acc.before_states.all().order_by("-surface").select_related("biotope_type__parent")
after_states = acc.after_states.all().order_by("-surface").select_related("biotope_type__parent")
actions = acc.actions.all().prefetch_related("action_type__parent")
# Reduce amount of db fetched data to the bare minimum we need in the template (deduction's intervention id and identifier)
deductions = acc.deductions.all()\
.distinct("intervention")\
.select_related("intervention")\
.values_list("intervention__id", "intervention__identifier", "intervention__title", named=True)
context = {
"obj": acc,
"qrcode": {
"img": qrcode_img,
"url": qrcode_url,
},
"qrcode_lanis": {
"img": qrcode_img_lanis,
"url": qrcode_lanis_url,
},
"is_entry_shared": False, # disables action buttons during rendering
"before_states": before_states,
"after_states": after_states,
"geom_form": geom_form,
"parcels": parcels,
"actions": actions,
"deductions": deductions,
"tables_scrollable": False,
TAB_TITLE_IDENTIFIER: tab_title,
}
context = BaseContext(request, context).context
return render(request, template, context)
report_context.update(self._get_compensation_report_context(obj))
return report_context

View File

@@ -10,25 +10,26 @@ from django.urls import path
from ema.views.action import NewEmaActionView, EditEmaActionView, RemoveEmaActionView
from ema.views.deadline import NewEmaDeadlineView, EditEmaDeadlineView, RemoveEmaDeadlineView
from ema.views.document import NewEmaDocumentView, EditEmaDocumentView, RemoveEmaDocumentView, GetEmaDocumentView
from ema.views.ema import index_view, new_view, new_id_view, detail_view, edit_view, remove_view
from ema.views.ema import remove_view, EmaIndexView, \
EmaIdentifierGeneratorView, EmaDetailView, EditEmaFormView, NewEmaFormView
from ema.views.log import EmaLogView
from ema.views.record import EmaRecordView
from ema.views.report import report_view
from ema.views.report import EmaReportView
from ema.views.resubmission import EmaResubmissionView
from ema.views.share import EmaShareFormView, EmaShareByTokenView
from ema.views.state import NewEmaStateView, EditEmaStateView, RemoveEmaStateView
app_name = "ema"
urlpatterns = [
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("", EmaIndexView.as_view(), name="index"),
path("new/", NewEmaFormView.as_view(), name="new"),
path("new/id", EmaIdentifierGeneratorView.as_view(), name="new-id"),
path("<id>", EmaDetailView.as_view(), name="detail"),
path('<id>/log', EmaLogView.as_view(), name='log'),
path('<id>/edit', edit_view, name='edit'),
path('<id>/edit', EditEmaFormView.as_view(), name='edit'),
path('<id>/remove', remove_view, name='remove'),
path('<id>/record', EmaRecordView.as_view(), name='record'),
path('<id>/report', report_view, name='report'),
path('<id>/report', EmaReportView.as_view(), name='report'),
path('<id>/resub', EmaResubmissionView.as_view(), name='resubmission-create'),
path('<id>/state/new', NewEmaStateView.as_view(), name='new-state'),

View File

@@ -5,46 +5,31 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22
"""
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from ema.models import Ema
from konova.decorators import shared_access_required, conservation_office_group_required, login_required_modal
from konova.views.action import AbstractNewCompensationActionView, AbstractEditCompensationActionView, \
AbstractRemoveCompensationActionView
_EMA_ACCOUNT_DETAIL_URL_NAME = "ema:detail"
class NewEmaActionView(AbstractNewCompensationActionView):
model = Ema
redirect_url = "ema:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(conservation_office_group_required)
@method_decorator(shared_access_required(Ema, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
_MODEL_CLS = Ema
_REDIRECT_URL = _EMA_ACCOUNT_DETAIL_URL_NAME
def _user_has_permission(self, user):
return user.is_ets_user()
class EditEmaActionView(AbstractEditCompensationActionView):
model = Ema
redirect_url = "ema:detail"
_MODEL_CLS = Ema
_REDIRECT_URL = _EMA_ACCOUNT_DETAIL_URL_NAME
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(conservation_office_group_required)
@method_decorator(shared_access_required(Ema, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
def _user_has_permission(self, user):
return user.is_ets_user()
class RemoveEmaActionView(AbstractRemoveCompensationActionView):
model = Ema
redirect_url = "ema:detail"
_MODEL_CLS = Ema
_REDIRECT_URL = _EMA_ACCOUNT_DETAIL_URL_NAME
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(conservation_office_group_required)
@method_decorator(shared_access_required(Ema, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
def _user_has_permission(self, user):
return user.is_ets_user()

View File

@@ -5,46 +5,30 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22
"""
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from ema.models import Ema
from konova.decorators import shared_access_required, conservation_office_group_required, login_required_modal
from konova.views.deadline import AbstractNewDeadlineView, AbstractRemoveDeadlineView, AbstractEditDeadlineView
_EMA_DETAIL_URL_NAME = "ema:detail"
class NewEmaDeadlineView(AbstractNewDeadlineView):
model = Ema
redirect_url = "ema:detail"
_MODEL_CLS = Ema
_REDIRECT_URL = _EMA_DETAIL_URL_NAME
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(conservation_office_group_required)
@method_decorator(shared_access_required(Ema, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
def _user_has_permission(self, user):
return user.is_ets_user()
class EditEmaDeadlineView(AbstractEditDeadlineView):
model = Ema
redirect_url = "ema:detail"
_MODEL_CLS = Ema
_REDIRECT_URL = _EMA_DETAIL_URL_NAME
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(conservation_office_group_required)
@method_decorator(shared_access_required(Ema, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
def _user_has_permission(self, user):
return user.is_ets_user()
class RemoveEmaDeadlineView(AbstractRemoveDeadlineView):
model = Ema
redirect_url = "ema:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(conservation_office_group_required)
@method_decorator(shared_access_required(Ema, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
_MODEL_CLS = Ema
_REDIRECT_URL = _EMA_DETAIL_URL_NAME
def _user_has_permission(self, user):
return user.is_ets_user()

View File

@@ -5,248 +5,112 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22
"""
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.db.models import Sum
from django.http import HttpRequest, JsonResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpRequest
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from ema.forms import NewEmaForm, EditEmaForm
from ema.models import Ema
from ema.tables import EmaTable
from konova.contexts import BaseContext
from konova.decorators import shared_access_required, conservation_office_group_required, login_required_modal, \
uuid_required
from konova.forms import SimpleGeomForm
from konova.decorators import shared_access_required, conservation_office_group_required, login_required_modal
from konova.forms.modals import RemoveModalForm
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.message_templates import RECORDED_BLOCKS_EDIT, IDENTIFIER_REPLACED, FORM_INVALID, \
DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE
from konova.views.base import BaseIndexView, BaseIdentifierGeneratorView, BaseNewSpatialLocatedObjectFormView, \
BaseEditSpatialLocatedObjectFormView
from konova.views.detail import BaseDetailView
@login_required
def index_view(request: HttpRequest):
""" Renders the index view for EMAs
class EmaIndexView(LoginRequiredMixin, BaseIndexView):
_TAB_TITLE = _("EMAs - Overview")
_INDEX_TABLE_CLS = EmaTable
Args:
request (HttpRequest): The incoming request
Returns:
"""
template = "generic_index.html"
emas = Ema.objects.filter(
deleted=None,
).order_by(
"-modified__timestamp"
)
table = EmaTable(
request,
queryset=emas
)
context = {
"table": table,
TAB_TITLE_IDENTIFIER: _("EMAs - Overview"),
}
context = BaseContext(request, context).context
return render(request, template, context)
def _get_queryset(self):
qs = Ema.objects.filter(
deleted=None,
).order_by(
"-modified__timestamp"
)
return qs
@login_required
@conservation_office_group_required
def new_view(request: HttpRequest):
"""
Renders a view for a new eco account creation
class NewEmaFormView(BaseNewSpatialLocatedObjectFormView):
_FORM_CLS = NewEmaForm
_MODEL_CLS = Ema
_TEMPLATE = "ema/form/view.html"
_TAB_TITLE = _("New EMA")
_REDIRECT_URL = "ema:detail"
Args:
request (HttpRequest): The incoming request
Returns:
"""
template = "ema/form/view.html"
data_form = NewEmaForm(request.POST or None)
geom_form = SimpleGeomForm(request.POST or None, read_only=False)
if request.method == "POST":
if data_form.is_valid() and geom_form.is_valid():
generated_identifier = data_form.cleaned_data.get("identifier", None)
ema = data_form.save(request.user, geom_form)
if generated_identifier != ema.identifier:
messages.info(
request,
IDENTIFIER_REPLACED.format(
generated_identifier,
ema.identifier
)
)
messages.success(request, _("EMA {} added").format(ema.identifier))
if geom_form.has_geometry_simplified():
messages.info(
request,
GEOMETRY_SIMPLIFIED
)
num_ignored_geometries = geom_form.get_num_geometries_ignored()
if num_ignored_geometries > 0:
messages.info(
request,
GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries)
)
return redirect("ema:detail", id=ema.id)
else:
messages.error(request, FORM_INVALID, extra_tags="danger",)
else:
# For clarification: nothing in this case
pass
context = {
"form": data_form,
"geom_form": geom_form,
TAB_TITLE_IDENTIFIER: _("New EMA"),
}
context = BaseContext(request, context).context
return render(request, template, context)
def _user_has_permission(self, user):
# User has to be an ets user
return user.is_ets_user()
@login_required
@conservation_office_group_required
def new_id_view(request: HttpRequest):
""" JSON endpoint
class EditEmaFormView(BaseEditSpatialLocatedObjectFormView):
_MODEL_CLS = Ema
_FORM_CLS = EditEmaForm
_TEMPLATE = "ema/form/view.html"
_REDIRECT_URL = "ema:detail"
_TAB_TITLE = _("Edit {}")
Provides fetching of free identifiers for e.g. AJAX calls
def _user_has_permission(self, user):
# User has to be an ets user
return user.is_ets_user()
"""
tmp = Ema()
identifier = tmp.generate_new_identifier()
while Ema.objects.filter(identifier=identifier).exists():
identifier = tmp.generate_new_identifier()
return JsonResponse(
data={
"gen_data": identifier
class EmaIdentifierGeneratorView(LoginRequiredMixin, BaseIdentifierGeneratorView):
_MODEL_CLS = Ema
_REDIRECT_URL = "ema:index"
def _user_has_permission(self, user):
return user.is_ets_user()
class EmaDetailView(BaseDetailView):
_MODEL_CLS = Ema
_TEMPLATE = "ema/detail/view.html"
def _get_object(self, id: str):
""" Fetch object for detail view
Args:
id (str): The record's id'
Returns:
"""
ema = get_object_or_404(Ema, id=id, deleted=None)
return ema
def _get_detail_context(self, obj: Ema):
""" Generate object specific detail context for view
Args:
obj (): The record
Returns:
"""
# Order states according to surface
before_states = obj.before_states.all().order_by("-surface")
after_states = obj.after_states.all().order_by("-surface")
# Precalculate logical errors between before- and after-states
# Sum() returns None in case of no states, so we catch that and replace it with 0 for easier handling
sum_before_states = obj.get_surface_before_states()
sum_after_states = obj.get_surface_after_states()
diff_states = abs(sum_before_states - sum_after_states)
context = {
"before_states": before_states,
"after_states": after_states,
"sum_before_states": sum_before_states,
"sum_after_states": sum_after_states,
"diff_states": diff_states,
"has_finished_deadlines": obj.get_finished_deadlines().exists(),
}
)
@login_required
@uuid_required
def detail_view(request: HttpRequest, id: str):
""" Renders the detail view of an EMA
Args:
request (HttpRequest): The incoming request
id (str): The EMA id
Returns:
"""
template = "ema/detail/view.html"
ema = get_object_or_404(Ema, id=id, deleted=None)
geom_form = SimpleGeomForm(instance=ema)
parcels = ema.get_underlying_parcels()
_user = request.user
is_entry_shared = ema.is_shared_with(_user)
# Order states according to surface
before_states = ema.before_states.all().order_by("-surface")
after_states = ema.after_states.all().order_by("-surface")
# Precalculate logical errors between before- and after-states
# Sum() returns None in case of no states, so we catch that and replace it with 0 for easier handling
sum_before_states = ema.get_surface_before_states()
sum_after_states = ema.get_surface_after_states()
diff_states = abs(sum_before_states - sum_after_states)
ema.set_status_messages(request)
requesting_user_is_only_shared_user = ema.is_only_shared_with(_user)
if requesting_user_is_only_shared_user:
messages.info(
request,
DO_NOT_FORGET_TO_SHARE
)
context = {
"obj": ema,
"geom_form": geom_form,
"parcels": parcels,
"is_entry_shared": is_entry_shared,
"before_states": before_states,
"after_states": after_states,
"sum_before_states": sum_before_states,
"sum_after_states": sum_after_states,
"diff_states": diff_states,
"is_default_member": _user.in_group(DEFAULT_GROUP),
"is_zb_member": _user.in_group(ZB_GROUP),
"is_ets_member": _user.in_group(ETS_GROUP),
"LANIS_LINK": ema.get_LANIS_link(),
TAB_TITLE_IDENTIFIER: f"{ema.identifier} - {ema.title}",
"has_finished_deadlines": ema.get_finished_deadlines().exists(),
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required
@conservation_office_group_required
@shared_access_required(Ema, "id")
def edit_view(request: HttpRequest, id: str):
"""
Renders a view for editing compensations
Args:
request (HttpRequest): The incoming request
Returns:
"""
template = "compensation/form/view.html"
# Get object from db
ema = get_object_or_404(Ema, id=id)
if ema.is_recorded:
messages.info(
request,
RECORDED_BLOCKS_EDIT
)
return redirect("ema:detail", id=id)
# Create forms, initialize with values from db/from POST request
data_form = EditEmaForm(request.POST or None, instance=ema)
geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=ema)
if request.method == "POST":
if data_form.is_valid() and geom_form.is_valid():
# The data form takes the geom form for processing, as well as the performing user
ema = data_form.save(request.user, geom_form)
messages.success(request, _("EMA {} edited").format(ema.identifier))
if geom_form.has_geometry_simplified():
messages.info(
request,
GEOMETRY_SIMPLIFIED
)
num_ignored_geometries = geom_form.get_num_geometries_ignored()
if num_ignored_geometries > 0:
messages.info(
request,
GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries)
)
return redirect("ema:detail", id=ema.id)
else:
messages.error(request, FORM_INVALID, extra_tags="danger",)
else:
# For clarification: nothing in this case
pass
context = {
"form": data_form,
"geom_form": geom_form,
TAB_TITLE_IDENTIFIER: _("Edit {}").format(ema.identifier),
}
context = BaseContext(request, context).context
return render(request, template, context)
return context
@login_required_modal

View File

@@ -5,20 +5,12 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22
"""
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from django.contrib.auth.mixins import LoginRequiredMixin
from ema.models import Ema
from konova.decorators import shared_access_required, conservation_office_group_required, login_required_modal
from konova.views.record import AbstractRecordView
class EmaRecordView(AbstractRecordView):
model = Ema
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(conservation_office_group_required)
@method_decorator(shared_access_required(Ema, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
class EmaRecordView(LoginRequiredMixin, AbstractRecordView):
_MODEL_CLS = Ema
_REDIRECT_URL = "ema:detail"

View File

@@ -5,77 +5,36 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22
"""
from django.http import HttpRequest
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from compensation.views.compensation.report import BaseCompensationReportView
from ema.models import Ema
from konova.contexts import BaseContext
from konova.decorators import uuid_required
from konova.forms import SimpleGeomForm
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.generators import generate_qr_code
from konova.sub_settings.django_settings import BASE_URL
from konova.utils.qrcode import QrCode
@uuid_required
def report_view(request:HttpRequest, id: str):
""" Renders the public report view
Args:
request (HttpRequest): The incoming request
id (str): The id of the intervention
class EmaReportView(BaseCompensationReportView):
_TEMPLATE = "ema/report/report.html"
_MODEL = Ema
Returns:
def _get_report_context(self, obj):
report_url = BASE_URL + reverse("ema:report", args=(obj.id,))
qrcode_report = QrCode(report_url, 10)
qrcode_lanis = QrCode(obj.get_LANIS_link(), 7)
"""
# Reuse the compensation report template since EMAs are structurally identical
template = "ema/report/report.html"
ema = get_object_or_404(Ema, id=id)
generic_compensation_report_context = self._get_compensation_report_context(obj)
tab_title = _("Report {}").format(ema.identifier)
# If intervention is not recorded (yet or currently) we need to render another template without any data
if not ema.is_ready_for_publish():
template = "report/unavailable.html"
context = {
TAB_TITLE_IDENTIFIER: tab_title,
report_context = {
"qrcode": {
"img": qrcode_report.get_img(),
"url": qrcode_report.get_content(),
},
"qrcode_lanis": {
"img": qrcode_lanis.get_img(),
"url": qrcode_lanis.get_content(),
},
"is_entry_shared": False, # disables action buttons during rendering
"tables_scrollable": False,
}
context = BaseContext(request, context).context
return render(request, template, context)
# Prepare data for map viewer
geom_form = SimpleGeomForm(
instance=ema,
)
parcels = ema.get_underlying_parcels()
qrcode_url = request.build_absolute_uri(reverse("ema:report", args=(id,)))
qrcode_img = generate_qr_code(qrcode_url, 10)
qrcode_lanis_url = ema.get_LANIS_link()
qrcode_img_lanis = generate_qr_code(qrcode_lanis_url, 7)
# Order states by surface
before_states = ema.before_states.all().order_by("-surface").prefetch_related("biotope_type")
after_states = ema.after_states.all().order_by("-surface").prefetch_related("biotope_type")
actions = ema.actions.all().prefetch_related("action_type")
context = {
"obj": ema,
"qrcode": {
"img": qrcode_img,
"url": qrcode_url
},
"qrcode_lanis": {
"img": qrcode_img_lanis,
"url": qrcode_lanis_url
},
"is_entry_shared": False, # disables action buttons during rendering
"before_states": before_states,
"after_states": after_states,
"geom_form": geom_form,
"parcels": parcels,
"actions": actions,
"tables_scrollable": False,
TAB_TITLE_IDENTIFIER: tab_title,
}
context = BaseContext(request, context).context
return render(request, template, context)
report_context.update(generic_compensation_report_context)
return report_context

View File

@@ -172,7 +172,8 @@ class EditEcoAccountDeductionModalForm(NewEcoAccountDeductionModalForm):
deduction = None
def __init__(self, *args, **kwargs):
self.deduction = kwargs.pop("deduction", None)
deduction_id = kwargs.pop("deduction_id", None)
self.deduction = EcoAccountDeduction.objects.get(id=deduction_id)
super().__init__(*args, **kwargs)
self.form_title = _("Edit Deduction")
form_data = {
@@ -252,19 +253,20 @@ class RemoveEcoAccountDeductionModalForm(RemoveModalForm):
Can be used for anything, where removing shall be confirmed by the user a second time.
"""
deduction = None
_DEDUCTION_OBJ = None
def __init__(self, *args, **kwargs):
deduction = kwargs.pop("deduction", None)
self.deduction = deduction
deduction_id = kwargs.pop("deduction_id", None)
deduction = EcoAccountDeduction.objects.get(id=deduction_id)
self._DEDUCTION_OBJ = 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()
self._DEDUCTION_OBJ.intervention.mark_as_edited(self.user, edit_comment=DEDUCTION_REMOVED)
self._DEDUCTION_OBJ.account.mark_as_edited(self.user, edit_comment=DEDUCTION_REMOVED)
self._DEDUCTION_OBJ.delete()
def check_for_recorded_instance(self):
if self.deduction.intervention.is_recorded:
if self._DEDUCTION_OBJ.intervention.is_recorded:
self.block_form()

View File

@@ -8,16 +8,18 @@ Created on: 30.11.20
from django.urls import path
from intervention.autocomplete.intervention import InterventionAutocomplete
from intervention.views.check import check_view
from intervention.views.check import InterventionCheckView
from intervention.views.compensation import remove_compensation_view
from intervention.views.deduction import NewInterventionDeductionView, EditInterventionDeductionView, \
RemoveInterventionDeductionView
from intervention.views.document import NewInterventionDocumentView, GetInterventionDocumentView, \
RemoveInterventionDocumentView, EditInterventionDocumentView
from intervention.views.intervention import index_view, new_view, new_id_view, detail_view, edit_view, remove_view
from intervention.views.intervention import remove_view, \
InterventionIndexView, InterventionIdentifierGeneratorView, InterventionDetailView, NewInterventionFormView, \
EditInterventionFormView
from intervention.views.log import InterventionLogView
from intervention.views.record import InterventionRecordView
from intervention.views.report import report_view
from intervention.views.report import InterventionReportView
from intervention.views.resubmission import InterventionResubmissionView
from intervention.views.revocation import new_revocation_view, edit_revocation_view, remove_revocation_view, \
get_revocation_view
@@ -25,18 +27,18 @@ from intervention.views.share import InterventionShareFormView, InterventionShar
app_name = "intervention"
urlpatterns = [
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("", InterventionIndexView.as_view(), name="index"),
path('new/', NewInterventionFormView.as_view(), name='new'),
path('new/id', InterventionIdentifierGeneratorView.as_view(), name='new-id'),
path('<id>', InterventionDetailView.as_view(), name='detail'),
path('<id>/log', InterventionLogView.as_view(), name='log'),
path('<id>/edit', edit_view, name='edit'),
path('<id>/edit', EditInterventionFormView.as_view(), name='edit'),
path('<id>/remove', remove_view, name='remove'),
path('<id>/share/<token>', InterventionShareByTokenView.as_view(), name='share-token'),
path('<id>/share', InterventionShareFormView.as_view(), name='share-form'),
path('<id>/check', check_view, name='check'),
path('<id>/check', InterventionCheckView.as_view(), name='check'),
path('<id>/record', InterventionRecordView.as_view(), name='record'),
path('<id>/report', report_view, name='report'),
path('<id>/report', InterventionReportView.as_view(), name='report'),
path('<id>/resub', InterventionResubmissionView.as_view(), name='resubmission-create'),
# Compensations

View File

@@ -5,35 +5,26 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22
"""
from django.contrib.auth.decorators import login_required
from django.http import HttpRequest
from django.shortcuts import get_object_or_404
from django.contrib.auth.mixins import LoginRequiredMixin
from django.utils.translation import gettext_lazy as _
from intervention.forms.modals.check import CheckModalForm
from intervention.models import Intervention
from konova.decorators import registration_office_group_required, shared_access_required
from konova.utils.message_templates import INTERVENTION_INVALID
from konova.views.base import BaseModalFormView
@login_required
@registration_office_group_required
@shared_access_required(Intervention, "id")
def check_view(request: HttpRequest, id: str):
""" Renders check form for an intervention
class InterventionCheckView(LoginRequiredMixin, BaseModalFormView):
_MODEL_CLS = Intervention
_FORM_CLS = CheckModalForm
_MSG_SUCCESS = _("Check performed")
_MSG_ERROR = INTERVENTION_INVALID
_REDIRECT_URL = "intervention:detail"
Args:
request (HttpRequest): The incoming request
id (str): Intervention's id
Returns:
"""
intervention = get_object_or_404(Intervention, id=id)
form = CheckModalForm(request.POST or None, instance=intervention, request=request)
return form.process_request(
request,
msg_success=_("Check performed"),
msg_error=INTERVENTION_INVALID
)
def _user_has_permission(self, user):
return user.is_zb_user()
def _get_redirect_url(self, *args, **kwargs):
redirect_url = super()._get_redirect_url(*args, **kwargs)
redirect_url += "#related_data"
return redirect_url

View File

@@ -5,51 +5,27 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22
"""
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from django.contrib.auth.mixins import LoginRequiredMixin
from intervention.models import Intervention
from konova.decorators import default_group_required, shared_access_required
from konova.utils.message_templates import DEDUCTION_ADDED, DEDUCTION_EDITED, DEDUCTION_REMOVED
from konova.views.deduction import AbstractNewDeductionView, AbstractEditDeductionView, AbstractRemoveDeductionView
_INTERVENTION_DETAIL_URL_NAME = "intervention:detail"
class NewInterventionDeductionView(AbstractNewDeductionView):
def _custom_check(self, obj):
pass
model = Intervention
redirect_url = "intervention:detail"
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Intervention, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
class NewInterventionDeductionView(LoginRequiredMixin, AbstractNewDeductionView):
_MODEL_CLS = Intervention
_MSG_SUCCESS = DEDUCTION_ADDED
_REDIRECT_URL = _INTERVENTION_DETAIL_URL_NAME
class EditInterventionDeductionView(AbstractEditDeductionView):
def _custom_check(self, obj):
pass
model = Intervention
redirect_url = "intervention:detail"
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Intervention, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
class EditInterventionDeductionView(LoginRequiredMixin, AbstractEditDeductionView):
_MODEL_CLS = Intervention
_MSG_SUCCESS = DEDUCTION_EDITED
_REDIRECT_URL = _INTERVENTION_DETAIL_URL_NAME
class RemoveInterventionDeductionView(AbstractRemoveDeductionView):
def _custom_check(self, obj):
pass
model = Intervention
redirect_url = "intervention:detail"
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Intervention, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
class RemoveInterventionDeductionView(LoginRequiredMixin, AbstractRemoveDeductionView):
_MODEL_CLS = Intervention
_MSG_SUCCESS = DEDUCTION_REMOVED
_REDIRECT_URL = _INTERVENTION_DETAIL_URL_NAME

View File

@@ -7,7 +7,8 @@ Created on: 19.08.22
"""
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.http import JsonResponse, HttpRequest
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpRequest
from django.shortcuts import get_object_or_404, render, redirect
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
@@ -16,198 +17,106 @@ from intervention.forms.intervention import EditInterventionForm, NewInterventio
from intervention.models import Intervention
from intervention.tables import InterventionTable
from konova.contexts import BaseContext
from konova.decorators import default_group_required, shared_access_required, any_group_check, login_required_modal, \
uuid_required
from konova.decorators import default_group_required, shared_access_required, login_required_modal
from konova.forms import SimpleGeomForm
from konova.forms.modals import RemoveModalForm
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.message_templates import DATA_CHECKED_PREVIOUSLY_TEMPLATE, RECORDED_BLOCKS_EDIT, \
CHECK_STATE_RESET, FORM_INVALID, IDENTIFIER_REPLACED, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED, \
GEOMETRIES_IGNORED_TEMPLATE
CHECK_STATE_RESET, FORM_INVALID, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE
from konova.views.base import BaseIndexView, BaseIdentifierGeneratorView, BaseNewSpatialLocatedObjectFormView, \
BaseEditSpatialLocatedObjectFormView
from konova.views.detail import BaseDetailView
@login_required
@any_group_check
def index_view(request: HttpRequest):
"""
Renders the index view for Interventions
class InterventionIndexView(LoginRequiredMixin, BaseIndexView):
_INDEX_TABLE_CLS = InterventionTable
_TAB_TITLE = _("Interventions - Overview")
Args:
request (HttpRequest): The incoming request
Returns:
A rendered view
"""
template = "generic_index.html"
# Filtering by user access is performed in table filter inside InterventionTableFilter class
interventions = Intervention.objects.filter(
deleted=None, # not deleted
).select_related(
"legal"
).order_by(
"-modified__timestamp"
)
table = InterventionTable(
request=request,
queryset=interventions
)
context = {
"table": table,
TAB_TITLE_IDENTIFIER: _("Interventions - Overview"),
}
context = BaseContext(request, context).context
return render(request, template, context)
def _get_queryset(self):
qs = Intervention.objects.filter(
deleted=None,
).select_related(
"legal"
).order_by(
"-modified__timestamp"
)
return qs
@login_required
@default_group_required
def new_view(request: HttpRequest):
"""
Renders a view for a new intervention creation
Args:
request (HttpRequest): The incoming request
Returns:
"""
template = "intervention/form/view.html"
data_form = NewInterventionForm(request.POST or None)
geom_form = SimpleGeomForm(request.POST or None, read_only=False)
if request.method == "POST":
if data_form.is_valid() and geom_form.is_valid():
generated_identifier = data_form.cleaned_data.get("identifier", None)
intervention = data_form.save(request.user, geom_form)
if generated_identifier != intervention.identifier:
messages.info(
request,
IDENTIFIER_REPLACED.format(
generated_identifier,
intervention.identifier
)
)
messages.success(request, _("Intervention {} added").format(intervention.identifier))
if geom_form.has_geometry_simplified():
messages.info(
request,
GEOMETRY_SIMPLIFIED
)
num_ignored_geometries = geom_form.get_num_geometries_ignored()
if num_ignored_geometries > 0:
messages.info(
request,
GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries)
)
return redirect("intervention:detail", id=intervention.id)
else:
messages.error(request, FORM_INVALID, extra_tags="danger",)
else:
# For clarification: nothing in this case
pass
context = {
"form": data_form,
"geom_form": geom_form,
TAB_TITLE_IDENTIFIER: _("New intervention"),
}
context = BaseContext(request, context).context
return render(request, template, context)
class NewInterventionFormView(BaseNewSpatialLocatedObjectFormView):
_MODEL_CLS = Intervention
_FORM_CLS = NewInterventionForm
_TEMPLATE = "intervention/form/view.html"
_REDIRECT_URL = "intervention:detail"
_TAB_TITLE = _("New intervention")
@login_required
@default_group_required
def new_id_view(request: HttpRequest):
""" JSON endpoint
class EditInterventionFormView(BaseEditSpatialLocatedObjectFormView):
_MODEL_CLS = Intervention
_FORM_CLS = EditInterventionForm
_TEMPLATE = "intervention/form/view.html"
_REDIRECT_URL = "intervention:detail"
_TAB_TITLE = _("Edit {}")
Provides fetching of free identifiers for e.g. AJAX calls
"""
tmp_intervention = Intervention()
identifier = tmp_intervention.generate_new_identifier()
while Intervention.objects.filter(identifier=identifier).exists():
identifier = tmp_intervention.generate_new_identifier()
return JsonResponse(
data={
"gen_data": identifier
class InterventionIdentifierGeneratorView(LoginRequiredMixin, BaseIdentifierGeneratorView):
_MODEL_CLS = Intervention
_REDIRECT_URL = "intervention:index"
class InterventionDetailView(BaseDetailView):
_MODEL_CLS = Intervention
_TEMPLATE = "intervention/detail/view.html"
def _get_object(self, id: str):
""" Returns the intervention
Args:
id (str): The intervention's id
Returns:
obj (Intervention): The intervention
"""
# Fetch data, filter out deleted related data
obj = get_object_or_404(
self._MODEL_CLS.objects.select_related(
"geometry",
"legal",
"responsible",
).prefetch_related(
"legal__revocations",
),
id=id,
deleted=None
)
return obj
def _get_detail_context(self, obj: Intervention):
""" Generate object specific detail context for view
Args:
obj (): The record
Returns:
"""
compensations = obj.compensations.filter(deleted=None)
last_checked = obj.get_last_checked_action()
last_checked_tooltip = ""
if last_checked:
last_checked_tooltip = DATA_CHECKED_PREVIOUSLY_TEMPLATE.format(
last_checked.get_timestamp_str_formatted(),
last_checked.user
)
has_payment_without_document = obj.payments.exists() and not obj.get_documents()[1].exists()
context = {
"last_checked": last_checked,
"last_checked_tooltip": last_checked_tooltip,
"compensations": compensations,
"has_payment_without_document": has_payment_without_document,
}
)
@login_required
@any_group_check
@uuid_required
def detail_view(request: HttpRequest, id: str):
""" Renders a detail view for viewing an intervention's data
Args:
request (HttpRequest): The incoming request
id (str): The intervention's id
Returns:
"""
template = "intervention/detail/view.html"
# Fetch data, filter out deleted related data
intervention = get_object_or_404(
Intervention.objects.select_related(
"geometry",
"legal",
"responsible",
).prefetch_related(
"legal__revocations",
),
id=id,
deleted=None
)
compensations = intervention.compensations.filter(
deleted=None,
)
_user = request.user
is_data_shared = intervention.is_shared_with(user=_user)
geom_form = SimpleGeomForm(
instance=intervention,
)
last_checked = intervention.get_last_checked_action()
last_checked_tooltip = ""
if last_checked:
last_checked_tooltip = DATA_CHECKED_PREVIOUSLY_TEMPLATE.format(
last_checked.get_timestamp_str_formatted(),
last_checked.user
)
has_payment_without_document = intervention.payments.exists() and not intervention.get_documents()[1].exists()
requesting_user_is_only_shared_user = intervention.is_only_shared_with(_user)
if requesting_user_is_only_shared_user:
messages.info(
request,
DO_NOT_FORGET_TO_SHARE
)
context = {
"obj": intervention,
"last_checked": last_checked,
"last_checked_tooltip": last_checked_tooltip,
"compensations": compensations,
"is_entry_shared": is_data_shared,
"geom_form": geom_form,
"is_default_member": _user.in_group(DEFAULT_GROUP),
"is_zb_member": _user.in_group(ZB_GROUP),
"is_ets_member": _user.in_group(ETS_GROUP),
"LANIS_LINK": intervention.get_LANIS_link(),
"has_payment_without_document": has_payment_without_document,
TAB_TITLE_IDENTIFIER: f"{intervention.identifier} - {intervention.title}",
}
request = intervention.set_status_messages(request)
context = BaseContext(request, context).context
return render(request, template, context)
return context
@login_required

View File

@@ -5,19 +5,12 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22
"""
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from django.contrib.auth.mixins import LoginRequiredMixin
from intervention.models import Intervention
from konova.decorators import conservation_office_group_required, shared_access_required
from konova.views.record import AbstractRecordView
class InterventionRecordView(AbstractRecordView):
model = Intervention
@method_decorator(login_required)
@method_decorator(conservation_office_group_required)
@method_decorator(shared_access_required(Intervention, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
class InterventionRecordView(LoginRequiredMixin, AbstractRecordView):
_MODEL_CLS = Intervention
_REDIRECT_URL = "intervention:detail"

View File

@@ -5,72 +5,41 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22
"""
from django.http import HttpRequest
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from intervention.models import Intervention
from konova.contexts import BaseContext
from konova.decorators import uuid_required
from konova.forms import SimpleGeomForm
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.generators import generate_qr_code
from konova.sub_settings.django_settings import BASE_URL
from konova.utils.qrcode import QrCode
from konova.views.report import BaseReportView
@uuid_required
def report_view(request: HttpRequest, id: str):
""" Renders the public report view
class InterventionReportView(BaseReportView):
_TEMPLATE = 'intervention/report/report.html'
_MODEL = Intervention
Args:
request (HttpRequest): The incoming request
id (str): The id of the intervention
def _get_report_context(self, obj: Intervention):
""" Returns the specific context needed for an intervention report
Returns:
Args:
obj (Intervention): The object for the report
"""
template = "intervention/report/report.html"
intervention = get_object_or_404(Intervention, id=id)
Returns:
dict: The object specific context for rendering the report
"""
distinct_deductions = obj.deductions.all().distinct("account")
report_url = BASE_URL + reverse("intervention:report", args=(obj.id,))
qrcode_report = QrCode(report_url, 10)
qrcode_lanis = QrCode(obj.get_LANIS_link(), 7)
tab_title = _("Report {}").format(intervention.identifier)
# If intervention is not recorded (yet or currently) we need to render another template without any data
if not intervention.is_ready_for_publish():
template = "report/unavailable.html"
context = {
TAB_TITLE_IDENTIFIER: tab_title,
return {
"deductions": distinct_deductions,
"qrcode": {
"img": qrcode_report.get_img(),
"url": qrcode_report.get_content(),
},
"qrcode_lanis": {
"img": qrcode_lanis.get_img(),
"url": qrcode_lanis.get_content(),
},
"tables_scrollable": False,
}
context = BaseContext(request, context).context
return render(request, template, context)
# Prepare data for map viewer
geom_form = SimpleGeomForm(
instance=intervention
)
parcels = intervention.get_underlying_parcels()
distinct_deductions = intervention.deductions.all().distinct(
"account"
)
qrcode_url = request.build_absolute_uri(reverse("intervention:report", args=(id,)))
qrcode_img = generate_qr_code(qrcode_url, 10)
qrcode_lanis_url = intervention.get_LANIS_link()
qrcode_img_lanis = generate_qr_code(qrcode_lanis_url, 7)
context = {
"obj": intervention,
"deductions": distinct_deductions,
"qrcode": {
"img": qrcode_img,
"url": qrcode_url,
},
"qrcode_lanis": {
"img": qrcode_img_lanis,
"url": qrcode_lanis_url,
},
"geom_form": geom_form,
"parcels": parcels,
"tables_scrollable": False,
TAB_TITLE_IDENTIFIER: tab_title,
}
context = BaseContext(request, context).context
return render(request, template, context)

View File

@@ -25,6 +25,7 @@ class BaseForm(forms.Form):
cancel_redirect = None
form_caption = None
instance = None # The data holding model object
user = None # The performing user
request = None
form_attrs = {} # Holds additional attributes, that can be used in the template
has_required_fields = False # Automatically set. Triggers hint rendering in templates
@@ -33,6 +34,7 @@ class BaseForm(forms.Form):
def __init__(self, *args, **kwargs):
self.instance = kwargs.pop("instance", None)
self.user = kwargs.pop("user", None)
super().__init__(*args, **kwargs)
if self.request is not None:
self.user = self.request.user
@@ -46,7 +48,7 @@ class BaseForm(forms.Form):
self.__check_valid_label_input_ratio()
@abstractmethod
def save(self):
def save(self, *arg, **kwargs):
# To be implemented in subclasses!
pass

View File

@@ -6,10 +6,11 @@ Created on: 15.08.22
"""
from django import forms
from django.shortcuts import get_object_or_404
from django.utils.translation import gettext_lazy as _
from konova.forms.modals.base_form import BaseModalForm
from konova.models import BaseObject
from konova.models import BaseObject, Deadline
class RemoveModalForm(BaseModalForm):
@@ -51,8 +52,8 @@ class RemoveDeadlineModalForm(RemoveModalForm):
deadline = None
def __init__(self, *args, **kwargs):
deadline = kwargs.pop("deadline", None)
self.deadline = deadline
deadline_id = kwargs.pop("deadline_id", None)
self.deadline = get_object_or_404(Deadline, id=deadline_id)
super().__init__(*args, **kwargs)
def save(self):

View File

@@ -288,4 +288,8 @@ Overwrites netgis.css attributes
Overwrites gradient used on default css of netgis map client
*/
background: var(--rlp-red) !important;
}
.netgis-menu{
z-index: 100 !important;
}

View File

@@ -103,7 +103,7 @@ class EditDeadlineModalFormTestCase(NewDeadlineModalFormTestCase):
data,
request=self.request,
instance=self.compensation,
deadline=self.finished_deadline,
deadline_id=self.finished_deadline.id,
)
self.assertTrue(form.is_valid(), msg=form.errors)

View File

@@ -256,7 +256,7 @@ class RemoveDeadlineTestCase(BaseTestCase):
form = RemoveDeadlineModalForm(
request=self.request,
instance=self.compensation,
deadline=self.finished_deadline
deadline_id=self.finished_deadline.id
)
self.assertEqual(form.form_title, str(_("Remove")))
self.assertEqual(form.form_caption, str(_("Are you sure?")))
@@ -273,7 +273,7 @@ class RemoveDeadlineTestCase(BaseTestCase):
data,
request=self.request,
instance=self.compensation,
deadline=self.finished_deadline
deadline_id=self.finished_deadline.id
)
self.assertTrue(form.is_valid(), msg=form.errors)
form.save()

View File

@@ -5,6 +5,11 @@ Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 17.09.21
"""
from uuid import UUID
from django.contrib import messages
from django.utils.translation import gettext_lazy as _
from django.http import HttpRequest, Http404
def format_german_float(num) -> str:
@@ -19,3 +24,27 @@ def format_german_float(num) -> str:
num (str): The number as german Gleitkommazahl
"""
return format(num, "0,.2f").replace(",", "X").replace(".", ",").replace("X", ".")
def check_user_is_in_any_group(request: HttpRequest):
"""
Checks for any group membership. Adds a message in case of having none.
"""
user = request.user
# Inform user about missing group privileges!
groups = user.groups.all()
if not groups:
messages.info(
request,
_("+++ Attention: You are not part of any group. You won't be able to create, edit or do anything. Please contact an administrator. +++")
)
return request
def check_id_is_valid_uuid(uuid: str):
if uuid:
try:
# Check whether the id is a proper uuid or something that would break a db fetch
UUID(uuid)
except ValueError:
raise Http404

View File

@@ -7,10 +7,6 @@ Created on: 09.11.20
"""
import random
import string
import qrcode
import qrcode.image.svg
from io import BytesIO
def generate_token() -> str:
@@ -42,23 +38,3 @@ def generate_random_string(length: int, use_numbers: bool = False, use_letters_l
ret_val = "".join(random.choice(elements) for i in range(length))
return ret_val
def generate_qr_code(content: str, size: int = 20) -> str:
""" Generates a qr code from given content
Args:
content (str): The content for the qr code
size (int): The image size
Returns:
qrcode_svg (str): The qr code as svg
"""
qrcode_factory = qrcode.image.svg.SvgImage
qrcode_img = qrcode.make(
content,
image_factory=qrcode_factory,
box_size=size
)
stream = BytesIO()
qrcode_img.save(stream)
return stream.getvalue().decode()

View File

@@ -19,7 +19,11 @@ IDENTIFIER_REPLACED = _("The identifier '{}' had to be changed to '{}' since ano
ENTRY_REMOVE_MISSING_PERMISSION = _("Only conservation or registration office users are allowed to remove entries.")
MISSING_GROUP_PERMISSION = _("You need to be part of another user group.")
CHECK_STATE_RESET = _("Status of Checked reset")
# RECORDING
RECORDED_BLOCKS_EDIT = _("Entry is recorded. To edit data, the entry first needs to be unrecorded.")
ENTRY_RECORDED = _("{} recorded")
ENTRY_UNRECORDED = _("{} unrecorded")
# SHARE
DATA_UNSHARED = _("This data is not shared with you")

47
konova/utils/qrcode.py Normal file
View File

@@ -0,0 +1,47 @@
"""
Author: Michel Peltriaux
Created on: 17.10.25
"""
from io import BytesIO
import qrcode
import qrcode.image.svg as svg
class QrCode:
""" A wrapping class for creating a qr code with content
"""
_content = None
_img = None
def __init__(self, content: str, size: int):
self._content = content
self._img = self._generate_qr_code(content, size)
def _generate_qr_code(self, content: str, size: int = 20) -> str:
""" Generates a qr code from given content
Args:
content (str): The content for the qr code
size (int): The image size
Returns:
qrcode_svg (str): The qr code as svg
"""
img_factory = svg.SvgImage
qrcode_img = qrcode.make(
content,
image_factory=img_factory,
box_size=size
)
stream = BytesIO()
qrcode_img.save(stream)
return stream.getvalue().decode()
def get_img(self):
return self._img
def get_content(self):
return self._content

View File

@@ -5,104 +5,47 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 22.08.22
"""
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.views import View
from django.contrib.auth.mixins import LoginRequiredMixin
from compensation.forms.modals.compensation_action import NewCompensationActionModalForm, \
EditCompensationActionModalForm, RemoveCompensationActionModalForm
from compensation.models import CompensationAction
from konova.utils.message_templates import COMPENSATION_STATE_ADDED, COMPENSATION_STATE_EDITED, \
COMPENSATION_STATE_REMOVED
from konova.views.base import BaseModalFormView
class AbstractCompensationActionView(View):
model = None
redirect_url = None
class AbstractCompensationActionView(LoginRequiredMixin, BaseModalFormView):
_MODEL_CLS = None
_REDIRECT_URL = None
class Meta:
abstract = True
def _user_has_permission(self, user):
return user.is_default_user()
def _get_redirect_url(self, *args, **kwargs):
return super()._get_redirect_url(*args, **kwargs) + "#related_data"
class AbstractNewCompensationActionView(AbstractCompensationActionView):
_FORM_CLS = NewCompensationActionModalForm
_MSG_SUCCESS = COMPENSATION_STATE_ADDED
class Meta:
abstract = True
def get(self, request, id: str):
""" Renders a form for adding new actions
Args:
request (HttpRequest): The incoming request
id (str): The object's id to which the new action will be related
Returns:
"""
obj = get_object_or_404(self.model, id=id)
form = NewCompensationActionModalForm(request.POST or None, instance=obj, request=request)
return form.process_request(
request,
msg_success=COMPENSATION_STATE_ADDED,
redirect_url=reverse(self.redirect_url, args=(id,)) + "#related_data"
)
def post(self, request, id: str):
return self.get(request, id)
class AbstractEditCompensationActionView(AbstractCompensationActionView):
_FORM_CLS = EditCompensationActionModalForm
_MSG_SUCCESS = COMPENSATION_STATE_EDITED
class Meta:
abstract = True
def get(self, request, id: str, action_id: str):
""" Renders a form for editing a action
Args:
request (HttpRequest): The incoming request
id (str): The object id
action_id (str): The action's id
Returns:
"""
obj = get_object_or_404(self.model, id=id)
action = get_object_or_404(CompensationAction, id=action_id)
form = EditCompensationActionModalForm(request.POST or None, instance=obj, action=action, request=request)
return form.process_request(
request,
msg_success=COMPENSATION_STATE_EDITED,
redirect_url=reverse(self.redirect_url, args=(id,)) + "#related_data"
)
def post(self, request, id: str, action_id: str):
return self.get(request, id, action_id)
class AbstractRemoveCompensationActionView(AbstractCompensationActionView):
_FORM_CLS = RemoveCompensationActionModalForm
_MSG_SUCCESS = COMPENSATION_STATE_REMOVED
class Meta:
abstract = True
def get(self, request, id: str, action_id: str):
""" Renders a form for removing aaction
Args:
request (HttpRequest): The incoming request
id (str): The object id
action_id (str): The action's id
Returns:
"""
obj = get_object_or_404(self.model, id=id)
action = get_object_or_404(CompensationAction, id=action_id)
form = RemoveCompensationActionModalForm(request.POST or None, instance=obj, action=action, request=request)
return form.process_request(
request,
msg_success=COMPENSATION_STATE_REMOVED,
redirect_url=reverse(self.redirect_url, args=(id,)) + "#related_data"
)
def post(self, request, id: str, action_id: str):
return self.get(request, id, action_id)

378
konova/views/base.py Normal file
View File

@@ -0,0 +1,378 @@
"""
Author: Michel Peltriaux
Created on: 15.10.25
"""
from abc import abstractmethod
from bootstrap_modal_forms.mixins import is_ajax
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpRequest, JsonResponse, HttpResponseRedirect
from django.shortcuts import render, redirect, get_object_or_404
from django.urls import reverse
from django.views import View
from django.utils.translation import gettext_lazy as _
from konova.contexts import BaseContext
from konova.forms import BaseForm, SimpleGeomForm
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.general import check_user_is_in_any_group, check_id_is_valid_uuid
from konova.utils.message_templates import MISSING_GROUP_PERMISSION, DATA_UNSHARED, IDENTIFIER_REPLACED, \
GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE, RECORDED_BLOCKS_EDIT, FORM_INVALID
class BaseView(View):
_TEMPLATE: str = "CHANGE_ME"
_TAB_TITLE: str = "CHANGE_ME"
_REDIRECT_URL: str = "CHANGE_ME"
_REDIRECT_URL_ERROR: str = "home"
class Meta:
abstract = True
def dispatch(self, request, *args, **kwargs):
request = check_user_is_in_any_group(request)
if not self._user_has_permission(request.user):
messages.info(request, MISSING_GROUP_PERMISSION)
return redirect(reverse(self._REDIRECT_URL_ERROR))
if not self._user_has_shared_access(request.user, **kwargs):
messages.info(request, DATA_UNSHARED)
return redirect(reverse(self._REDIRECT_URL_ERROR))
return super().dispatch(request, *args, **kwargs)
def _user_has_permission(self, user):
""" Has to be implemented properly by inheriting classes
Args:
user ():
Returns:
"""
return False
def _user_has_shared_access(self, user, **kwargs):
""" Has to be implemented properly by inheriting classes
Args:
user ():
Returns:
"""
return False
def _get_redirect_url(self, *args, **kwargs):
return self._REDIRECT_URL
def _get_redirect_url_error(self, *args, **kwargs):
return self._REDIRECT_URL_ERROR
class BaseModalFormView(BaseView):
_TEMPLATE = "modal/modal_form.html"
_MODEL_CLS = None
_FORM_CLS = None
_TAB_TITLE = None
_MSG_SUCCESS = None
_MSG_ERROR = None
class Meta:
abstract = True
def _user_has_shared_access(self, user, **kwargs):
obj = get_object_or_404(self._MODEL_CLS, id=kwargs.get("id"))
return obj.is_shared_with(user)
def get(self, request: HttpRequest, id: str, *args, **kwargs):
obj = self._MODEL_CLS.objects.get(id=id)
form = self._FORM_CLS(request.POST or None, instance=obj, request=request, **kwargs)
context = {
"form": form,
}
context = BaseContext(request, context).context
return render(request, self._TEMPLATE, context)
def post(self, request: HttpRequest, id: str, *args, **kwargs):
obj = self._MODEL_CLS.objects.get(id=id)
form = self._FORM_CLS(request.POST or None, instance=obj, request=request, **kwargs)
redirect_url = self._get_redirect_url(obj=obj)
if form.is_valid():
if not is_ajax(request.META):
# Modal forms send one POST for checking on data validity. This can be used to return possible errors
# on the form. A second POST (if no errors occured) is sent afterwards and needs to process the
# saving/commiting of the data to the database. is_ajax() performs this check. The first request is
# an ajax call, the second is a regular form POST.
form.save()
messages.success(
request,
self._get_msg_success(obj=obj)
)
return HttpResponseRedirect(redirect_url)
else:
context = {
"form": form,
}
context = BaseContext(request, context).context
return render(request, self._TEMPLATE, context)
def _get_redirect_url(self, *args, **kwargs):
obj = kwargs.get("obj", None)
assert obj is not None
return reverse(self._REDIRECT_URL, args=(obj.id,))
def _get_msg_success(self, *args, **kwargs):
return self._MSG_SUCCESS
class BaseIndexView(BaseView):
""" Base class for index views
"""
_TEMPLATE = "generic_index.html"
_INDEX_TABLE_CLS = None
_REDIRECT_URL = "home"
class Meta:
abstract = True
def get(self, request: HttpRequest):
qs = self._get_queryset()
table = self._INDEX_TABLE_CLS(
request=request,
queryset=qs
)
context = {
"table": table,
TAB_TITLE_IDENTIFIER: self._TAB_TITLE,
}
context = BaseContext(request, context).context
return render(request, self._TEMPLATE, context)
@abstractmethod
def _get_queryset(self):
raise NotImplementedError
def _user_has_permission(self, user):
# No specific permissions needed for opening base index view
return True
def _user_has_shared_access(self, user, **kwargs):
# No specific constraints for shared access of index views
return True
class BaseIdentifierGeneratorView(BaseView):
_MODEL_CLS = None
_REDIRECT_URL: str = "home"
class Meta:
abstract = True
def get(self, request: HttpRequest):
tmp_obj = self._MODEL_CLS()
identifier = tmp_obj.generate_new_identifier()
while self._MODEL_CLS.objects.filter(identifier=identifier).exists():
identifier = tmp_obj.generate_new_identifier()
return JsonResponse(
data={
"gen_data": identifier
}
)
def _user_has_permission(self, user):
""" Should be overwritten in inheriting classes!
Args:
user ():
Returns:
"""
return user.is_default_user()
def _user_has_shared_access(self, user, **kwargs):
# No specific constraints for shared access
return True
class BaseFormView(BaseView):
_MODEL_CLS = None
_FORM_CLS = None
class Meta:
abstract = True
def _get_additional_context(self, **kwargs):
"""
Args:
**kwargs ():
Returns:
"""
return {}
class BaseSpatialLocatedObjectFormView(LoginRequiredMixin, BaseFormView):
_GEOMETRY_FORM_CLS = SimpleGeomForm
class Meta:
abstract = True
class BaseNewSpatialLocatedObjectFormView(BaseSpatialLocatedObjectFormView):
def _user_has_permission(self, user):
# User has to have default privilege to call this endpoint
return user.is_default_user()
def _user_has_shared_access(self, user, **kwargs):
# There is no shared access control since nothing exists yet
return True
def get(self, request: HttpRequest, **kwargs):
form: BaseForm = self._FORM_CLS(None, **kwargs, user=request.user)
geom_form: SimpleGeomForm = self._GEOMETRY_FORM_CLS(None, user=request.user, read_only=False)
context = self._get_additional_context()
context = BaseContext(request, additional_context=context).context
context.update(
{
"form": form,
"geom_form": geom_form,
TAB_TITLE_IDENTIFIER: self._TAB_TITLE,
}
)
return render(request, self._TEMPLATE, context)
def post(self, request: HttpRequest, **kwargs):
form: BaseForm = self._FORM_CLS(request.POST or None, **kwargs, user=request.user)
geom_form: SimpleGeomForm = self._GEOMETRY_FORM_CLS(request.POST or None, user=request.user, read_only=False)
if form.is_valid() and geom_form.is_valid():
obj = form.save(request.user, geom_form)
obj_redirect_url = reverse(self._REDIRECT_URL, args=(obj.id,))
generated_identifier = form.cleaned_data.get("identifier", None)
if generated_identifier != obj.identifier:
messages.info(
request,
IDENTIFIER_REPLACED.format(
generated_identifier,
obj.identifier
)
)
messages.success(request, _("{} added").format(obj.identifier))
if geom_form.has_geometry_simplified():
messages.info(
request,
GEOMETRY_SIMPLIFIED
)
num_ignored_geometries = geom_form.get_num_geometries_ignored()
if num_ignored_geometries > 0:
messages.info(
request,
GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries)
)
return redirect(obj_redirect_url)
else:
context = self._get_additional_context()
messages.error(request, FORM_INVALID, extra_tags="danger",)
context = BaseContext(request, additional_context=context).context
context.update(
{
"form": form,
"geom_form": geom_form,
TAB_TITLE_IDENTIFIER: self._TAB_TITLE,
}
)
return render(request, self._TEMPLATE, context)
class BaseEditSpatialLocatedObjectFormView(BaseSpatialLocatedObjectFormView):
_TAB_TITLE = _("Edit {}")
def get(self, request: HttpRequest, id: str):
obj = get_object_or_404(
self._MODEL_CLS,
id=id
)
obj_redirect_url = reverse(self._REDIRECT_URL, args=(obj.id,))
if obj.is_recorded:
messages.info(
request,
RECORDED_BLOCKS_EDIT
)
return redirect(obj_redirect_url)
form: BaseForm = self._FORM_CLS(None, instance=obj, user=request.user)
geom_form: SimpleGeomForm = self._GEOMETRY_FORM_CLS(None, instance=obj, read_only=False)
context = self._get_additional_context()
context = BaseContext(request, additional_context=context).context
context.update(
{
"form": form,
"geom_form": geom_form,
TAB_TITLE_IDENTIFIER: self._TAB_TITLE.format(obj.identifier),
}
)
return render(request, self._TEMPLATE, context)
def post(self, request: HttpRequest, id: str):
obj = get_object_or_404(
self._MODEL_CLS,
id=id
)
obj_redirect_url = reverse(self._REDIRECT_URL, args=(obj.id,))
form: BaseForm = self._FORM_CLS(request.POST or None, instance=obj, user=request.user)
geom_form: SimpleGeomForm = self._GEOMETRY_FORM_CLS(request.POST or None, instance=obj, read_only=False)
if form.is_valid() and geom_form.is_valid():
obj = form.save(request.user, geom_form)
messages.success(request, _("{} edited").format(obj.identifier))
if geom_form.has_geometry_simplified():
messages.info(
request,
GEOMETRY_SIMPLIFIED
)
num_ignored_geometries = geom_form.get_num_geometries_ignored()
if num_ignored_geometries > 0:
messages.info(
request,
GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries)
)
return redirect(obj_redirect_url)
else:
context = self._get_additional_context()
messages.error(request, FORM_INVALID, extra_tags="danger",)
context = BaseContext(request, additional_context=context).context
context.update(
{
"form": form,
"geom_form": geom_form,
TAB_TITLE_IDENTIFIER: self._TAB_TITLE.format(obj.identifier),
}
)
return render(request, self._TEMPLATE, context)
def _user_has_shared_access(self, user, **kwargs):
obj = get_object_or_404(self._MODEL_CLS, id=kwargs.get('id', None))
return obj.is_shared_with(user)
def _user_has_permission(self, user):
return user.is_default_user()

View File

@@ -5,102 +5,57 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 22.08.22
"""
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.views import View
from django.contrib.auth.mixins import LoginRequiredMixin
from compensation.forms.modals.deadline import NewDeadlineModalForm, EditDeadlineModalForm
from konova.forms.modals import RemoveDeadlineModalForm
from konova.models import Deadline
from konova.utils.message_templates import DEADLINE_ADDED, DEADLINE_EDITED, DEADLINE_REMOVED
from konova.views.base import BaseModalFormView
class AbstractNewDeadlineView(View):
model = None
redirect_url = None
class AbstractNewDeadlineView(LoginRequiredMixin, BaseModalFormView):
_MODEL_CLS = None
_FORM_CLS = NewDeadlineModalForm
_REDIRECT_URL = None
_MSG_SUCCESS = DEADLINE_ADDED
class Meta:
abstract = True
def get(self, request, id: str):
""" Renders a form for adding new deadlines
def _get_redirect_url(self, *args, **kwargs):
return super()._get_redirect_url(*args, **kwargs) + "#related_data"
Args:
request (HttpRequest): The incoming request
id (str): The account's id to which the new state will be related
Returns:
"""
obj = get_object_or_404(self.model, id=id)
form = NewDeadlineModalForm(request.POST or None, instance=obj, request=request)
return form.process_request(
request,
msg_success=DEADLINE_ADDED,
redirect_url=reverse(self.redirect_url, args=(id,)) + "#related_data"
)
def post(self, request, id: str):
return self.get(request, id)
def _user_has_permission(self, user):
return user.is_default_user()
class AbstractEditDeadlineView(View):
model = None
redirect_url = None
class AbstractEditDeadlineView(LoginRequiredMixin, BaseModalFormView):
_MODEL_CLS = None
_FORM_CLS = EditDeadlineModalForm
_REDIRECT_URL = None
_MSG_SUCCESS = DEADLINE_EDITED
class Meta:
abstract = True
def get(self, request, id: str, deadline_id: str):
""" Renders a form for editing deadlines
def _get_redirect_url(self, *args, **kwargs):
return super()._get_redirect_url(*args, **kwargs) + "#related_data"
Args:
request (HttpRequest): The incoming request
id (str): The compensation's id
deadline_id (str): The deadline's id
Returns:
"""
obj = get_object_or_404(self.model, id=id)
deadline = get_object_or_404(Deadline, id=deadline_id)
form = EditDeadlineModalForm(request.POST or None, instance=obj, deadline=deadline, request=request)
return form.process_request(
request,
msg_success=DEADLINE_EDITED,
redirect_url=reverse(self.redirect_url, args=(id,)) + "#related_data"
)
def post(self, request, id: str, deadline_id: str):
return self.get(request, id, deadline_id)
def _user_has_permission(self, user):
return user.is_default_user()
class AbstractRemoveDeadlineView(View):
model = None
redirect_url = None
class AbstractRemoveDeadlineView(LoginRequiredMixin, BaseModalFormView):
_MODEL_CLS = None
_FORM_CLS = RemoveDeadlineModalForm
_REDIRECT_URL = None
_MSG_SUCCESS = DEADLINE_REMOVED
class Meta:
abstract = True
def get(self, request, id: str, deadline_id: str):
""" Renders a form for removing deadlines
def _get_redirect_url(self, *args, **kwargs):
return super()._get_redirect_url(*args, **kwargs) + "#related_data"
Args:
request (HttpRequest): The incoming request
id (str): The compensation's id
deadline_id (str): The deadline's id
Returns:
"""
obj = get_object_or_404(self.model, id=id)
deadline = get_object_or_404(Deadline, id=deadline_id)
form = RemoveDeadlineModalForm(request.POST or None, instance=obj, deadline=deadline, request=request)
return form.process_request(
request,
msg_success=DEADLINE_REMOVED,
redirect_url=reverse(self.redirect_url, args=(id,)) + "#related_data"
)
def post(self, request, id: str, deadline_id: str):
return self.get(request, id, deadline_id)
def _user_has_permission(self, user):
return user.is_default_user()

View File

@@ -6,126 +6,88 @@ Created on: 22.08.22
"""
from django.core.exceptions import ObjectDoesNotExist
from django.http import Http404
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.views import View
from intervention.forms.modals.deduction import NewEcoAccountDeductionModalForm, EditEcoAccountDeductionModalForm, \
RemoveEcoAccountDeductionModalForm
from konova.utils.message_templates import DEDUCTION_ADDED, DEDUCTION_EDITED, DEDUCTION_REMOVED, DEDUCTION_UNKNOWN
from konova.utils.general import check_id_is_valid_uuid
from konova.views.base import BaseModalFormView
class AbstractDeductionView(View):
model = None
redirect_url = None
class AbstractDeductionView(BaseModalFormView):
_REDIRECT_URL = None
def dispatch(self, request, *args, **kwargs):
check_id_is_valid_uuid(kwargs.get("id"))
return super().dispatch(request, *args, **kwargs)
def _custom_check(self, obj):
"""
Can be used by inheriting classes to provide custom checks before further processing
"""
raise NotImplementedError("Must be implemented in subclasses")
pass
def _user_has_permission(self, user) -> bool:
"""
Args:
user ():
Returns:
"""
return user.is_default_user()
def _user_has_shared_access(self, user, **kwargs) -> bool:
""" A user has shared access on
Args:
user (User): The performing user
kwargs (dict): Parameters
Returns:
bool: True if the user has access to the requested object, False otherwise
"""
ret_val: bool = False
try:
obj = self._MODEL_CLS.objects.get(
id=kwargs.get("id")
)
ret_val = obj.is_shared_with(user)
except ObjectDoesNotExist:
ret_val = False
return ret_val
def _get_redirect_url(self, *args, **kwargs):
obj = kwargs.get("obj", None)
assert obj is not None
return reverse(self._REDIRECT_URL, args=(obj.id,)) + "#related_data"
class AbstractNewDeductionView(AbstractDeductionView):
_FORM_CLS = NewEcoAccountDeductionModalForm
class Meta:
abstract = True
def get(self, request, id: str):
""" Renders a modal form view for creating deductions
Args:
request (HttpRequest): The incoming request
id (str): The obj's id which shall benefit from this deduction
Returns:
"""
obj = get_object_or_404(self.model, id=id)
self._custom_check(obj)
form = NewEcoAccountDeductionModalForm(request.POST or None, instance=obj, request=request)
return form.process_request(
request,
msg_success=DEDUCTION_ADDED,
redirect_url=reverse(self.redirect_url, args=(id,)) + "#related_data",
)
def post(self, request, id: str):
return self.get(request, id)
class AbstractEditDeductionView(AbstractDeductionView):
_FORM_CLS = EditEcoAccountDeductionModalForm
def _custom_check(self, obj):
pass
def dispatch(self, request, *args, **kwargs):
check_id_is_valid_uuid(kwargs.get("deduction_id"))
return super().dispatch(request, *args, **kwargs)
class Meta:
abstract = True
def get(self, request, id: str, deduction_id: str):
""" Renders a modal view for editing deductions
Args:
request (HttpRequest): The incoming request
id (str): The object's id
deduction_id (str): The deduction's id
Returns:
"""
obj = get_object_or_404(self.model, id=id)
self._custom_check(obj)
try:
eco_deduction = obj.deductions.get(id=deduction_id)
except ObjectDoesNotExist:
raise Http404(DEDUCTION_UNKNOWN)
form = EditEcoAccountDeductionModalForm(request.POST or None, instance=obj, deduction=eco_deduction,
request=request)
return form.process_request(
request=request,
msg_success=DEDUCTION_EDITED,
redirect_url=reverse(self.redirect_url, args=(id,)) + "#related_data"
)
def post(self, request, id: str, deduction_id: str):
return self.get(request, id, deduction_id)
class AbstractRemoveDeductionView(AbstractDeductionView):
_FORM_CLS = RemoveEcoAccountDeductionModalForm
def _custom_check(self, obj):
pass
def dispatch(self, request, *args, **kwargs):
check_id_is_valid_uuid(kwargs.get("deduction_id"))
return super().dispatch(request, *args, **kwargs)
class Meta:
abstract = True
def get(self, request, id: str, deduction_id: str):
""" Renders a modal view for removing deductions
Args:
request (HttpRequest): The incoming request
id (str): The object's id
deduction_id (str): The deduction's id
Returns:
"""
obj = get_object_or_404(self.model, id=id)
self._custom_check(obj)
try:
eco_deduction = obj.deductions.get(id=deduction_id)
except ObjectDoesNotExist:
raise Http404(DEDUCTION_UNKNOWN)
form = RemoveEcoAccountDeductionModalForm(request.POST or None, instance=obj, deduction=eco_deduction,
request=request)
return form.process_request(
request=request,
msg_success=DEDUCTION_REMOVED,
redirect_url=reverse(self.redirect_url, args=(id,)) + "#related_data"
)
def post(self, request, id: str, deduction_id: str):
return self.get(request, id, deduction_id)

107
konova/views/detail.py Normal file
View File

@@ -0,0 +1,107 @@
"""
Author: Michel Peltriaux
Created on: 17.10.25
"""
from abc import abstractmethod
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpRequest
from django.shortcuts import render
from konova.contexts import BaseContext
from konova.forms import SimpleGeomForm
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.general import check_id_is_valid_uuid
from konova.utils.message_templates import DO_NOT_FORGET_TO_SHARE
from konova.views.base import BaseView
class BaseDetailView(LoginRequiredMixin, BaseView):
_MODEL_CLS = None
class Meta:
abstract = True
def dispatch(self, request, *args, **kwargs):
check_id_is_valid_uuid(kwargs.get('id'))
return super().dispatch(request, *args, **kwargs)
def _user_has_shared_access(self, user, **kwargs):
""" Check if user has shared access to this object
Args:
user ():
**kwargs ():
Returns:
"""
# Access to an entry's detail view is not restricted by the state of being-shared or not
return True
def _user_has_permission(self, user):
# Detail views have no restrictions
return True
def get(self, request: HttpRequest, id: str):
""" Get endpoint for detail view
Args:
request (HttpRequest): The incoming request
id (str): The record's id
Returns:
"""
obj = self._get_object(id)
geom_form = SimpleGeomForm(instance=obj)
user = request.user
requesting_user_is_only_shared_user = obj.is_only_shared_with(user)
if requesting_user_is_only_shared_user:
messages.info(request, DO_NOT_FORGET_TO_SHARE)
obj.set_status_messages(request)
detail_context = self._get_detail_context(obj)
context = BaseContext(request, detail_context).context
context.update(
{
"obj": obj,
"geom_form": geom_form,
"is_default_member": user.in_group(DEFAULT_GROUP),
"is_zb_member": user.in_group(ZB_GROUP),
"is_ets_member": user.in_group(ETS_GROUP),
"LANIS_LINK": obj.get_LANIS_link(),
"is_entry_shared": obj.is_shared_with(user=user),
TAB_TITLE_IDENTIFIER: f"{obj.identifier} - {obj.title}"
}
)
return render(request,self._TEMPLATE, context)
@abstractmethod
def _get_detail_context(self, obj):
""" Generate object specific detail context for view
Args:
obj (): The record
Returns:
"""
raise NotImplementedError
@abstractmethod
def _get_object(self, id: str):
""" Fetch object for detail view
Args:
id (str): The record's id'
Returns:
"""
raise NotImplementedError

View File

@@ -9,21 +9,19 @@ from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import Q
from django.http import HttpRequest
from django.shortcuts import render
from django.utils.decorators import method_decorator
from django.utils.translation import gettext_lazy as _
from django.views import View
from compensation.models import EcoAccount, Compensation
from intervention.models import Intervention
from konova.contexts import BaseContext
from konova.decorators import any_group_check
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.views.base import BaseView
from news.models import ServerMessage
class HomeView(LoginRequiredMixin, View):
class HomeView(LoginRequiredMixin, BaseView):
_TEMPLATE = "konova/home.html"
@method_decorator(any_group_check)
def get(self, request: HttpRequest):
"""
Renders the landing page
@@ -34,7 +32,6 @@ class HomeView(LoginRequiredMixin, View):
Returns:
A redirect
"""
template = "konova/home.html"
user = request.user
user_teams = user.shared_teams
@@ -75,5 +72,12 @@ class HomeView(LoginRequiredMixin, View):
TAB_TITLE_IDENTIFIER: _("Home"),
}
context = BaseContext(request, additional_context).context
return render(request, template, context)
return render(request, self._TEMPLATE, context)
def _user_has_permission(self, user):
# No specific permission needed for home view
return True
def _user_has_shared_access(self, user, **kwargs):
# No specific constraint needed for home view
return True

View File

@@ -10,9 +10,8 @@ from json import JSONDecodeError
import requests
import urllib3.util
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import JsonResponse, HttpRequest, HttpResponse
from django.utils.decorators import method_decorator
from django.utils.http import urlencode
from django.views import View
@@ -22,17 +21,13 @@ from konova.sub_settings.lanis_settings import MAP_PROXY_HOST_WHITELIST
from konova.sub_settings.proxy_settings import PROXIES, GEOPORTAL_RLP_USER, GEOPORTAL_RLP_PASSWORD
class BaseClientProxyView(View):
class BaseClientProxyView(LoginRequiredMixin, View):
""" Provides proxy functionality for NETGIS map client.
"""
class Meta:
abstract = True
@method_decorator(login_required)
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
def _check_with_whitelist(self, url):
parsed_url = urllib3.util.parse_url(url)
parsed_url_host = parsed_url.host
@@ -67,7 +62,6 @@ class BaseClientProxyView(View):
class ClientProxyParcelSearch(BaseClientProxyView):
def get(self, request: HttpRequest):
url = request.META.get("QUERY_STRING")

View File

@@ -5,46 +5,24 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22
"""
from django.shortcuts import get_object_or_404
from django.views import View
from django.utils.translation import gettext_lazy as _
from konova.forms.modals import RecordModalForm
from konova.utils.message_templates import ENTRY_RECORDED, ENTRY_UNRECORDED
from konova.views.base import BaseModalFormView
class AbstractRecordView(View):
model = None
class AbstractRecordView(BaseModalFormView):
_MODEL_CLS = None
_FORM_CLS = RecordModalForm
_MSG_SUCCESS = None
def get(self, request, id: str):
""" Renders a modal form for recording an object
def _user_has_permission(self, user):
return user.is_ets_user()
Args:
request (HttpRequest): The incoming request
id (str): The object's id
def _get_msg_success(self, *args, **kwargs):
obj = kwargs.get("obj")
assert obj is not None
Returns:
"""
obj = get_object_or_404(self.model, id=id)
form = RecordModalForm(request.POST or None, instance=obj, request=request)
msg_succ = _("{} unrecorded") if obj.recorded else _("{} recorded")
msg_succ = msg_succ.format(obj.identifier)
return form.process_request(
request,
msg_succ,
msg_error=_("Errors found:")
)
def post(self, request, id: str):
"""
BaseModalForm provides the method process_request() which handles GET as well as POST requests. It was written
for easier handling of function based views. To support process_request() on class based views, the post()
call needs to be treated the same way as the get() call.
Args:
request (HttpRequest): The incoming request
id (str): Intervention's id
"""
return self.get(request, id)
if obj.is_recorded:
return ENTRY_RECORDED.format(obj.identifier)
else:
return ENTRY_UNRECORDED.format(obj.identifier)

106
konova/views/report.py Normal file
View File

@@ -0,0 +1,106 @@
"""
Author: Michel Peltriaux
Created on: 17.10.25
"""
from abc import abstractmethod
from uuid import UUID
from django.http import HttpRequest, Http404
from django.shortcuts import get_object_or_404, render
from django.utils.translation import gettext_lazy as _
from konova.contexts import BaseContext
from konova.forms import SimpleGeomForm
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.views.base import BaseView
class BaseReportView(BaseView):
_TEMPLATE = None
_TAB_TITLE = _("Report {}")
_MODEL = None
class Meta:
abstract = True
def dispatch(self, request, *args, **kwargs):
# If the given id is not a uuid we act as the result was not found
try:
UUID(kwargs.get('id'))
except ValueError:
raise Http404()
return super().dispatch(request, *args, **kwargs)
def _return_unpublishable_content_response(self, request: HttpRequest, tab_title: str):
""" Handles HttpResponse return in case the object is not ready for publish
Args:
request ():
tab_title ():
Returns:
"""
template = "report/unavailable.html"
context = {
TAB_TITLE_IDENTIFIER: tab_title,
}
context = BaseContext(request, context).context
return render(request, template, context)
def get(self, request: HttpRequest, id: str):
""" Renders the public report view
Args:
request (HttpRequest): The incoming request
id (str): The id of the intervention
Returns:
"""
obj = get_object_or_404(self._MODEL, id=id)
tab_title = self._TAB_TITLE.format(obj.identifier)
# If object is not recorded we need to render another template without any data
if not obj.is_ready_for_publish():
return self._return_unpublishable_content_response(request, tab_title)
# First get specific report context for different types of objects due to inheritance
report_context = self._get_report_context(obj)
# Then generate and add default report context (the same for all models)
geom_form = SimpleGeomForm(instance=obj)
parcels = obj.get_underlying_parcels()
report_context.update(
{
TAB_TITLE_IDENTIFIER: tab_title,
"parcels": parcels,
"geom_form": geom_form,
"obj": obj
}
)
# Then generate the general context based on the report specific data
context = BaseContext(request, report_context).context
return render(request, self._TEMPLATE, context)
@abstractmethod
def _get_report_context(self, obj):
""" Returns the specific context needed for this report view
Args:
obj (RecordableObjectMixin): The object for the report
Returns:
dict: The object specific context for rendering the report
"""
raise NotImplementedError
def _user_has_permission(self, user):
# Reports do not need specific permissions to be callable
return True
def _user_has_shared_access(self, user, **kwargs):
# Reports do not need specific share states to be callable
return True

Binary file not shown.

View File

@@ -45,7 +45,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-15 09:11+0200\n"
"POT-Creation-Date: 2025-10-19 13:56+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -448,11 +448,19 @@ msgid "Select the intervention for which this compensation compensates"
msgstr "Wählen Sie den Eingriff, für den diese Kompensation bestimmt ist"
#: compensation/forms/compensation.py:114
#: compensation/views/compensation/compensation.py:120
#: compensation/views/compensation/compensation.py:161
msgid "New compensation"
msgstr "Neue Kompensation"
#: compensation/forms/compensation.py:190
#: compensation/forms/compensation.py:179
msgid ""
"This intervention is currently recorded. You cannot add further "
"compensations as long as it is recorded."
msgstr ""
"Dieser Eingriff ist derzeit verzeichnet. "
"Sie können keine weiteren Kompensationen hinzufügen, so lange er verzeichnet ist."
#: compensation/forms/compensation.py:202
msgid "Edit compensation"
msgstr "Bearbeite Kompensation"
@@ -475,7 +483,7 @@ msgid "When did the parties agree on this?"
msgstr "Wann wurde dieses Ökokonto offiziell vereinbart?"
#: compensation/forms/eco_account.py:72
#: compensation/views/eco_account/eco_account.py:101
#: compensation/views/eco_account/eco_account.py:93
msgid "New Eco-Account"
msgstr "Neues Ökokonto"
@@ -1288,44 +1296,45 @@ msgstr ""
msgid "Responsible data"
msgstr "Daten zu den verantwortlichen Stellen"
#: compensation/views/compensation/compensation.py:58
#: compensation/views/compensation/compensation.py:35
msgid "Compensations - Overview"
msgstr "Kompensationen - Übersicht"
#: compensation/views/compensation/compensation.py:181
#: compensation/views/compensation/compensation.py:52
#, fuzzy
#| msgid "New compensation"
msgid "New Compensation"
msgstr "Neue Kompensation"
#: compensation/views/compensation/compensation.py:208
#: konova/utils/message_templates.py:40
msgid "Compensation {} edited"
msgstr "Kompensation {} bearbeitet"
#: compensation/views/compensation/compensation.py:196
#: compensation/views/eco_account/eco_account.py:173 ema/views/ema.py:238
#: intervention/views/intervention.py:253
#: compensation/views/compensation/compensation.py:231
#: compensation/views/eco_account/eco_account.py:159 ema/views/ema.py:59
#: intervention/views/intervention.py:59 intervention/views/intervention.py:179
#: konova/views/base.py:239
msgid "Edit {}"
msgstr "Bearbeite {}"
#: compensation/views/compensation/report.py:35
#: compensation/views/eco_account/report.py:36 ema/views/report.py:35
#: intervention/views/report.py:35
msgid "Report {}"
msgstr "Bericht {}"
#: compensation/views/eco_account/eco_account.py:53
#: compensation/views/eco_account/eco_account.py:32
msgid "Eco-account - Overview"
msgstr "Ökokonten - Übersicht"
#: compensation/views/eco_account/eco_account.py:86
#: compensation/views/eco_account/eco_account.py:70
msgid "Eco-Account {} added"
msgstr "Ökokonto {} hinzugefügt"
#: compensation/views/eco_account/eco_account.py:158
#: compensation/views/eco_account/eco_account.py:136
msgid "Eco-Account {} edited"
msgstr "Ökokonto {} bearbeitet"
#: compensation/views/eco_account/eco_account.py:288
#: compensation/views/eco_account/eco_account.py:260
msgid "Eco-account removed"
msgstr "Ökokonto entfernt"
#: ema/forms.py:42 ema/tests/unit/test_forms.py:27 ema/views/ema.py:108
#: ema/forms.py:42 ema/tests/unit/test_forms.py:27 ema/views/ema.py:42
msgid "New EMA"
msgstr "Neue EMA hinzufügen"
@@ -1353,19 +1362,11 @@ msgstr ""
msgid "Payment funded compensation"
msgstr "Ersatzzahlungsmaßnahme"
#: ema/views/ema.py:53
#: ema/views/ema.py:26
msgid "EMAs - Overview"
msgstr "EMAs - Übersicht"
#: ema/views/ema.py:86
msgid "EMA {} added"
msgstr "EMA {} hinzugefügt"
#: ema/views/ema.py:223
msgid "EMA {} edited"
msgstr "EMA {} bearbeitet"
#: ema/views/ema.py:262
#: ema/views/ema.py:138
msgid "EMA removed"
msgstr "EMA entfernt"
@@ -1429,7 +1430,7 @@ msgstr "Datum Bestandskraft bzw. Rechtskraft"
#: intervention/forms/intervention.py:216
#: intervention/tests/unit/test_forms.py:36
#: intervention/views/intervention.py:105
#: intervention/views/intervention.py:51
msgid "New intervention"
msgstr "Neuer Eingriff"
@@ -1665,19 +1666,15 @@ msgstr ""
msgid "Check performed"
msgstr "Prüfung durchgeführt"
#: intervention/views/intervention.py:57
#: intervention/views/intervention.py:33
msgid "Interventions - Overview"
msgstr "Eingriffe - Übersicht"
#: intervention/views/intervention.py:90
msgid "Intervention {} added"
msgstr "Eingriff {} hinzugefügt"
#: intervention/views/intervention.py:236
#: intervention/views/intervention.py:154
msgid "Intervention {} edited"
msgstr "Eingriff {} bearbeitet"
#: intervention/views/intervention.py:278
#: intervention/views/intervention.py:204
msgid "{} removed"
msgstr "{} entfernt"
@@ -1689,7 +1686,7 @@ msgstr "Hierfür müssen Sie Mitarbeiter sein!"
msgid "You need to be administrator to perform this action!"
msgstr "Hierfür müssen Sie Administrator sein!"
#: konova/decorators.py:65
#: konova/decorators.py:65 konova/utils/general.py:40
msgid ""
"+++ Attention: You are not part of any group. You won't be able to create, "
"edit or do anything. Please contact an administrator. +++"
@@ -1801,7 +1798,7 @@ msgstr "Sucht nach Einträgen, an denen diese Person gearbeitet hat"
msgid "Save"
msgstr "Speichern"
#: konova/forms/base_form.py:72
#: konova/forms/base_form.py:74
msgid "Not editable"
msgstr "Nicht editierbar"
@@ -1810,7 +1807,7 @@ msgstr "Nicht editierbar"
msgid "Geometry"
msgstr "Geometrie"
#: konova/forms/geometry_form.py:100
#: konova/forms/geometry_form.py:101
msgid "Only surfaces allowed. Points or lines must be buffered."
msgstr ""
"Nur Flächen erlaubt. Punkte oder Linien müssen zu Flächen gepuffert werden."
@@ -2268,8 +2265,9 @@ msgid ""
"too small to be valid). These parts have been removed. Please check the "
"stored geometry."
msgstr ""
"Die Geometrie enthielt {} invalide Bestandteile (z.B. unaussagekräftige Kleinstflächen)."
"Diese Bestandteile wurden automatisch entfernt. Bitte überprüfen Sie die angepasste Geometrie."
"Die Geometrie enthielt {} invalide Bestandteile (z.B. unaussagekräftige "
"Kleinstflächen).Diese Bestandteile wurden automatisch entfernt. Bitte "
"überprüfen Sie die angepasste Geometrie."
#: konova/utils/message_templates.py:89
msgid "This intervention has {} revocations"
@@ -2310,7 +2308,15 @@ msgstr ""
"Dieses Datum ist unrealistisch. Geben Sie bitte das korrekte Datum ein "
"(>1950)."
#: konova/views/home.py:75 templates/navbars/navbar.html:16
#: konova/views/base.py:209
msgid "{} added"
msgstr "{} hinzugefügt"
#: konova/views/base.py:281
msgid "{} edited"
msgstr "{} bearbeitet"
#: konova/views/home.py:72 templates/navbars/navbar.html:16
msgid "Home"
msgstr "Home"
@@ -2330,6 +2336,10 @@ msgstr "{} verzeichnet"
msgid "Errors found:"
msgstr "Fehler gefunden:"
#: konova/views/report.py:21
msgid "Report {}"
msgstr "Bericht {}"
#: konova/views/resubmission.py:39
msgid "Resubmission set"
msgstr "Wiedervorlage gesetzt"
@@ -3056,7 +3066,7 @@ msgid "Manage teams"
msgstr ""
#: user/templates/user/index.html:53 user/templates/user/team/index.html:19
#: user/views/views.py:135
#: user/views/views.py:134
msgid "Teams"
msgstr ""
@@ -3116,34 +3126,40 @@ msgstr "Läuft ab am"
msgid "User API token"
msgstr "API Nutzer Token"
#: user/views/views.py:33
#: user/views/views.py:31
msgid "User settings"
msgstr "Einstellungen"
#: user/views/views.py:59
msgid "Notifications edited"
msgstr "Benachrichtigungen bearbeitet"
#: user/views/views.py:71
#: user/views/views.py:44
msgid "User notifications"
msgstr "Benachrichtigungen"
#: user/views/views.py:147
#: user/views/views.py:64
msgid "Notifications edited"
msgstr "Benachrichtigungen bearbeitet"
#: user/views/views.py:152
msgid "New team added"
msgstr "Neues Team hinzugefügt"
#: user/views/views.py:162
#: user/views/views.py:167
msgid "Team edited"
msgstr "Team bearbeitet"
#: user/views/views.py:177
#: user/views/views.py:182
msgid "Team removed"
msgstr "Team gelöscht"
#: user/views/views.py:192
#: user/views/views.py:197
msgid "You are not a member of this team"
msgstr "Sie sind kein Mitglied dieses Teams"
#: user/views/views.py:199
#: user/views/views.py:204
msgid "Left Team"
msgstr "Team verlassen"
#~ msgid "EMA {} added"
#~ msgstr "EMA {} hinzugefügt"
#~ msgid "EMA {} edited"
#~ msgstr "EMA {} bearbeitet"

View File

@@ -56,7 +56,7 @@
{% if user.is_staff or user.is_superuser %}
<a class="dropdown-item" target="_blank" href="{% url 'admin:index' %}">{% fa5_icon 'tools' %} {% trans 'Admin' %}</a>
{% endif %}
<a class="dropdown-item" href="{% url 'user:index' %}">{% fa5_icon 'cogs' %} {% trans 'Settings' %}</a>
<a class="dropdown-item" href="{% url 'user:detail' %}">{% fa5_icon 'cogs' %} {% trans 'Settings' %}</a>
<a class="dropdown-item" href="{% url 'logout' %}">{% fa5_icon 'sign-out-alt' %} {% trans 'Logout' %}</a>
</div>
</li>

View File

@@ -38,7 +38,7 @@ class UserNotificationForm(BaseForm):
self.form_title = _("Edit notifications")
self.form_caption = _("")
self.action_url = reverse("user:notifications")
self.cancel_redirect = reverse("user:index")
self.cancel_redirect = reverse("user:detail")
# Insert all notifications into form field by creating choices as tuples
notifications = UserNotification.objects.filter(

View File

@@ -26,7 +26,7 @@ class UserViewTestCase(BaseViewTestCase):
self.team.users.add(self.superuser)
self.team.admins.add(self.superuser)
# Prepare urls
self.index_url = reverse("user:index", args=())
self.index_url = reverse("user:detail", args=())
self.notification_url = reverse("user:notifications", args=())
self.api_token_url = reverse("user:api-token", args=())
self.contact_url = reverse("user:contact", args=(self.superuser.id,))

View File

@@ -233,7 +233,7 @@ class UserNotificationFormTestCase(BaseTestCase):
self.assertEqual(form.form_title, str(_("Edit notifications")))
self.assertEqual(form.form_caption, "")
self.assertEqual(form.action_url, reverse("user:notifications"))
self.assertEqual(form.cancel_redirect, reverse("user:index"))
self.assertEqual(form.cancel_redirect, reverse("user:detail"))
def test_save(self):
selected_notification = UserNotification.objects.first()

View File

@@ -15,15 +15,15 @@ from user.views.views import *
app_name = "user"
urlpatterns = [
path("", index_view, name="index"),
path("", UserDetailView.as_view(), name="detail"),
path("propagate/", PropagateUserView.as_view(), name="propagate"),
path("notifications/", notifications_view, name="notifications"),
path("notifications/", NotificationsView.as_view(), name="notifications"),
path("token/api", APITokenView.as_view(), name="api-token"),
path("token/api/new", new_api_token_view, name="api-token-new"),
path("contact/<id>", contact_view, name="contact"),
path("team/", index_team_view, name="team-index"),
path("contact/<id>", ContactView.as_view(), name="contact"),
path("team/", TeamIndexView.as_view(), name="team-index"),
path("team/new", new_team_view, name="team-new"),
path("team/<id>", data_team_view, name="team-data"),
path("team/<id>", TeamDetailModalView.as_view(), name="team-data"),
path("team/<id>/edit", edit_team_view, name="team-edit"),
path("team/<id>/remove", remove_team_view, name="team-remove"),
path("team/<id>/leave", leave_team_view, name="team-leave"),

View File

@@ -1,8 +1,10 @@
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.views.base import BaseView, BaseModalFormView
from user.forms.modals.team import NewTeamModalForm, EditTeamModalForm, RemoveTeamModalForm, LeaveTeamModalForm
from user.forms.modals.user import UserContactForm
from user.forms.team import TeamDataForm
@@ -13,129 +15,132 @@ from django.shortcuts import render, redirect, get_object_or_404
from django.utils.translation import gettext_lazy as _
from konova.contexts import BaseContext
from konova.decorators import any_group_check, login_required_modal
from konova.decorators import login_required_modal
@login_required
@any_group_check
def index_view(request: HttpRequest):
""" Renders the user's data index view
class UserBaseView(BaseView):
def _user_has_shared_access(self, user, **kwargs):
return True
Args:
request ():
Returns:
"""
template = "user/index.html"
context = {
"user": request.user,
TAB_TITLE_IDENTIFIER: _("User settings"),
}
context = BaseContext(request, context).context
return render(request, template, context)
def _user_has_permission(self, user):
return True
@login_required
@any_group_check
def notifications_view(request: HttpRequest):
""" Renders the notifications settings view
class UserDetailView(LoginRequiredMixin, UserBaseView):
_TEMPLATE = "user/index.html"
_TAB_TITLE = _("User settings")
Args:
request ():
def get(self, request: HttpRequest):
context = {
"user": request.user,
TAB_TITLE_IDENTIFIER: self._TAB_TITLE,
}
context = BaseContext(request, context).context
return render(request, self._TEMPLATE, context)
Returns:
"""
template = "user/notifications.html"
user = request.user
class NotificationsView(LoginRequiredMixin, UserBaseView):
_TEMPLATE = "user/notifications.html"
_TAB_TITLE = _("User notifications")
form = UserNotificationForm(user=user, data=request.POST or None)
if request.method == "POST":
def get(self, request: HttpRequest):
user = request.user
form = UserNotificationForm(user=user, data=None)
context = {
"user": user,
"form": form,
TAB_TITLE_IDENTIFIER: self._TAB_TITLE,
}
context = BaseContext(request, context).context
return render(request, self._TEMPLATE, context)
def post(self, request: HttpRequest):
user = request.user
form = UserNotificationForm(user=user, data=request.POST)
if form.is_valid():
form.save()
messages.success(
request,
_("Notifications edited")
)
return redirect("user:index")
elif request.method == "GET":
# Implicit
pass
else:
raise NotImplementedError
context = {
"user": user,
"form": form,
TAB_TITLE_IDENTIFIER: _("User notifications"),
}
context = BaseContext(request, context).context
return render(request, template, context)
return redirect("user:detail")
context = {
"user": user,
"form": form,
TAB_TITLE_IDENTIFIER: self._TAB_TITLE,
}
context = BaseContext(request, context).context
return render(request, self._TEMPLATE, context)
@login_required_modal
@login_required
def contact_view(request: HttpRequest, id: str):
""" Renders contact modal view of a users contact data
class ContactView(LoginRequiredMixin, BaseModalFormView):
def get(self, request: HttpRequest, id: str):
""" Renders contact modal view of a users contact data
Args:
request (HttpRequest): The incoming request
id (str): The user's id
Args:
request (HttpRequest): The incoming request
id (str): The user's id
Returns:
Returns:
"""
user = get_object_or_404(User, id=id)
form = UserContactForm(request.POST or None, instance=user, request=request)
template = "modal/modal_form.html"
context = {
"form": form,
}
context = BaseContext(request, context).context
return render(
request,
template,
context
)
"""
user = get_object_or_404(User, id=id)
form = UserContactForm(request.POST or None, instance=user, request=request)
context = {
"form": form,
}
context = BaseContext(request, context).context
return render(request, self._TEMPLATE, context)
def _user_has_shared_access(self, user, **kwargs):
# No specific constraints
return True
def _user_has_permission(self, user):
# No specific constraints
return True
@login_required_modal
@login_required
def data_team_view(request: HttpRequest, id: str):
""" Renders team data
class TeamDetailModalView(LoginRequiredMixin, BaseModalFormView):
def get(self, request: HttpRequest, id: str):
""" Renders team data
Args:
request (HttpRequest): The incoming request
id (str): The team's id
Args:
request (HttpRequest): The incoming request
id (str): The team's id
Returns:
Returns:
"""
team = get_object_or_404(Team, id=id)
form = TeamDataForm(request.POST or None, instance=team, request=request)
template = "modal/modal_form.html"
context = {
"form": form,
}
context = BaseContext(request, context).context
return render(
request,
template,
context
)
"""
team = get_object_or_404(Team, id=id)
form = TeamDataForm(request.POST or None, instance=team, request=request)
context = {
"form": form,
}
context = BaseContext(request, context).context
return render(request, self._TEMPLATE, context)
def _user_has_shared_access(self, user, **kwargs):
# No specific constraints
return True
def _user_has_permission(self, user):
# No specific constraints
return True
@login_required
def index_team_view(request: HttpRequest):
template = "user/team/index.html"
user = request.user
context = {
"teams": user.shared_teams,
"tab_title": _("Teams"),
}
context = BaseContext(request, context).context
return render(request, template, context)
class TeamIndexView(LoginRequiredMixin, UserBaseView):
_TEMPLATE = "user/team/index.html"
_TAB_TITLE = _("Teams")
def get(self, request: HttpRequest):
user = request.user
context = {
"teams": user.shared_teams,
"tab_title": self._TAB_TITLE,
}
context = BaseContext(request, context).context
return render(request, self._TEMPLATE, context)
@login_required_modal