Compare commits

..

6 Commits

Author SHA1 Message Date
210f3fcafa Merge pull request '# HOTFIX' (#495) from update_map_config into master
Reviewed-on: #495
2025-10-23 16:12:45 +02:00
e7d67560f2 # HOTFIX
* fixes bug on netgis map client where importing a geometry would result in an error message
* THIS IS JUST A WORKAROUND AND HAS TO BE REPLACED BY A PROPER FIX FROM THE DEVS ASAP
2025-10-23 16:12:05 +02:00
b5e991fb95 Merge pull request '# Map layer update' (#494) from update_map_config into master
Reviewed-on: #494
2025-10-22 16:02:02 +02:00
d3a555d406 # Map layer update
* updates rp_dop layer from deprecated to newest
2025-10-22 16:00:35 +02:00
9a374f50de Merge pull request '# CSS bugfix' (#492) from fix_css_map_menu_bar_overlap into master
Reviewed-on: #492
2025-10-21 15:59:37 +02:00
ce63dd30bc # CSS bugfix
* fixes a rendering bug where the menu bar of the map client overlapped modal forms
2025-10-21 15:57:25 +02:00
37 changed files with 9609 additions and 2015 deletions

0
codelist/views.py Normal file
View File

View File

@@ -168,17 +168,6 @@ class NewCompensationForm(AbstractCompensationForm,
comp.log.add(action) comp.log.add(action)
return comp, action return comp, action
def is_valid(self):
valid = super().is_valid()
intervention = self.cleaned_data.get("intervention", None)
valid &= not intervention.is_recorded
if not valid:
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): def save(self, user: User, geom_form: SimpleGeomForm):
with transaction.atomic(): with transaction.atomic():
comp, action = self.__create_comp(user) comp, action = self.__create_comp(user)

View File

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

View File

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

View File

@@ -7,8 +7,8 @@ Created on: 19.08.22
""" """
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Sum
from django.http import HttpRequest, JsonResponse from django.http import HttpRequest, JsonResponse
from django.shortcuts import get_object_or_404, render, redirect from django.shortcuts import get_object_or_404, render, redirect
from django.urls import reverse from django.urls import reverse
@@ -18,150 +18,279 @@ from compensation.forms.compensation import EditCompensationForm, NewCompensatio
from compensation.models import Compensation from compensation.models import Compensation
from compensation.tables.compensation import CompensationTable from compensation.tables.compensation import CompensationTable
from intervention.models import Intervention from intervention.models import Intervention
from konova.decorators import shared_access_required, default_group_required, login_required_modal 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.forms.modals import RemoveModalForm 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, \ from konova.utils.message_templates import COMPENSATION_REMOVED_TEMPLATE, DATA_CHECKED_PREVIOUSLY_TEMPLATE, \
RECORDED_BLOCKS_EDIT, PARAMS_INVALID RECORDED_BLOCKS_EDIT, CHECK_STATE_RESET, FORM_INVALID, PARAMS_INVALID, IDENTIFIER_REPLACED, \
from konova.views.base import BaseIndexView, BaseIdentifierGeneratorView, BaseNewSpatialLocatedObjectFormView, \ COMPENSATION_ADDED_TEMPLATE, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE
BaseEditSpatialLocatedObjectFormView
from konova.views.detail import BaseDetailView
class CompensationIndexView(LoginRequiredMixin, BaseIndexView): @login_required
_TAB_TITLE = _("Compensations - Overview") @any_group_check
_INDEX_TABLE_CLS = CompensationTable def index_view(request: HttpRequest):
"""
Renders the index view for compensation
def _get_queryset(self): Args:
qs = Compensation.objects.filter( request (HttpRequest): The incoming request
deleted=None, # only show those which are not deleted individually
intervention__deleted=None, # and don't show the ones whose intervention has been deleted Returns:
).order_by( A rendered view
"-modified__timestamp" """
) template = "generic_index.html"
return qs 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)
class NewCompensationFormView(BaseNewSpatialLocatedObjectFormView): @login_required
_FORM_CLS = NewCompensationForm @default_group_required
_MODEL_CLS = Compensation @shared_access_required(Intervention, "intervention_id")
_TEMPLATE = "compensation/form/view.html" def new_view(request: HttpRequest, intervention_id: str = None):
_TAB_TITLE = _("New Compensation") """
_REDIRECT_URL = "compensation:detail" Renders a view for a new compensation creation
def _user_has_shared_access(self, user, **kwargs): Args:
# On a new compensation make sure the intervention (if call came directly through an intervention's detail request (HttpRequest): The incoming request
# 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)
def _user_has_permission(self, user): Returns:
# User has to be an ets user
return user.is_default_user()
def dispatch(self, request, *args, **kwargs): """
# Make sure there is an existing intervention based on the given id template = "compensation/form/view.html"
# Compensations can not exist without an intervention if intervention_id is not None:
intervention_id = kwargs.get("intervention_id", None) try:
if intervention_id: intervention = Intervention.objects.get(id=intervention_id)
try: except ObjectDoesNotExist:
intervention = Intervention.objects.get(id=intervention_id) messages.error(request, PARAMS_INVALID)
if intervention.is_recorded: return redirect("home")
messages.info( if intervention.is_recorded:
request, messages.info(
RECORDED_BLOCKS_EDIT request,
) RECORDED_BLOCKS_EDIT
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)
class EditCompensationFormView(BaseEditSpatialLocatedObjectFormView):
_MODEL_CLS = Compensation
_FORM_CLS = EditCompensationForm
_TEMPLATE = "compensation/form/view.html"
_REDIRECT_URL = "compensation:detail"
def _user_has_permission(self, user):
# User has to be a default user
return user.is_default_user()
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
) )
return redirect("intervention:detail", id=intervention_id)
context = { data_form = NewCompensationForm(request.POST or None, intervention_id=intervention_id)
"last_checked": last_checked, geom_form = SimpleGeomForm(request.POST or None, read_only=False)
"last_checked_tooltip": last_checked_tooltip, if request.method == "POST":
"actions": actions, if data_form.is_valid() and geom_form.is_valid():
"before_states": before_states, generated_identifier = data_form.cleaned_data.get("identifier", None)
"after_states": after_states, comp = data_form.save(request.user, geom_form)
"sum_before_states": sum_before_states, if generated_identifier != comp.identifier:
"sum_after_states": sum_after_states, messages.info(
"diff_states": diff_states, request,
"has_finished_deadlines": obj.get_finished_deadlines().exists(), IDENTIFIER_REPLACED.format(
generated_identifier,
comp.identifier
)
)
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)
@login_required
@default_group_required
def new_id_view(request: HttpRequest):
""" JSON endpoint
Provides fetching of free identifiers for e.g. AJAX calls
"""
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
} }
return context )
@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)
@login_required_modal @login_required_modal

View File

@@ -5,48 +5,77 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 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.urls import reverse
from django.utils.translation import gettext_lazy as _
from compensation.models import Compensation from compensation.models import Compensation
from konova.sub_settings.django_settings import BASE_URL from konova.contexts import BaseContext
from konova.utils.qrcode import QrCode from konova.decorators import uuid_required
from konova.views.report import BaseReportView from konova.forms import SimpleGeomForm
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.generators import generate_qr_code
@uuid_required
def report_view(request: HttpRequest, id: str):
""" Renders the public report view
class BaseCompensationReportView(BaseReportView): Args:
def _get_compensation_report_context(self, obj): request (HttpRequest): The incoming request
# Order states by surface id (str): The id of the intervention
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")
return { Returns:
"before_states": before_states,
"after_states": after_states, """
"actions": actions, # 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,
} }
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()
class CompensationReportView(BaseCompensationReportView): qrcode_url = request.build_absolute_uri(reverse("compensation:report", args=(id,)))
_MODEL = Compensation qrcode_img = generate_qr_code(qrcode_url, 10)
_TEMPLATE = "compensation/report/compensation/report.html" qrcode_lanis_url = comp.get_LANIS_link()
qrcode_img_lanis = generate_qr_code(qrcode_lanis_url, 7)
def _get_report_context(self, obj): # Order states by surface
report_url = BASE_URL + reverse("compensation:report", args=(obj.id,)) before_states = comp.before_states.all().order_by("-surface").prefetch_related("biotope_type")
qrcode_report = QrCode(report_url, 10) after_states = comp.after_states.all().order_by("-surface").prefetch_related("biotope_type")
qrcode_lanis = QrCode(obj.get_LANIS_link(), 7) actions = comp.actions.all().prefetch_related("action_type")
report_context = { context = {
"qrcode": { "obj": comp,
"img": qrcode_report.get_img(), "qrcode": {
"url": qrcode_report.get_content(), "img": qrcode_img,
}, "url": qrcode_url,
"qrcode_lanis": { },
"img": qrcode_lanis.get_img(), "qrcode_lanis": {
"url": qrcode_lanis.get_content(), "img": qrcode_img_lanis,
}, "url": qrcode_lanis_url,
"is_entry_shared": False, # disables action buttons during rendering },
"tables_scrollable": False, "is_entry_shared": False, # disables action buttons during rendering
} "before_states": before_states,
report_context.update(self._get_compensation_report_context(obj)) "after_states": after_states,
return report_context "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)

View File

@@ -5,28 +5,54 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.decorators import login_required
from django.http import Http404 from django.http import Http404
from django.utils.decorators import method_decorator
from compensation.models import EcoAccount from compensation.models import EcoAccount
from konova.decorators import default_group_required, login_required_modal
from konova.views.deduction import AbstractNewDeductionView, AbstractEditDeductionView, AbstractRemoveDeductionView from konova.views.deduction import AbstractNewDeductionView, AbstractEditDeductionView, AbstractRemoveDeductionView
class NewEcoAccountDeductionView(LoginRequiredMixin, AbstractNewDeductionView): class NewEcoAccountDeductionView(AbstractNewDeductionView):
_MODEL = EcoAccount model = EcoAccount
_REDIRECT_URL = "compensation:acc:detail" 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)
def _custom_check(self, obj): def _custom_check(self, obj):
# New deductions can only be created if the eco account has been recorded
if not obj.recorded: if not obj.recorded:
raise Http404() raise Http404()
class EditEcoAccountDeductionView(LoginRequiredMixin, AbstractEditDeductionView): class EditEcoAccountDeductionView(AbstractEditDeductionView):
_MODEL = EcoAccount def _custom_check(self, obj):
_REDIRECT_URL = "compensation:acc:detail" 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): class RemoveEcoAccountDeductionView(AbstractRemoveDeductionView):
_MODEL = EcoAccount def _custom_check(self, obj):
_REDIRECT_URL = "compensation:acc:detail" 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)

View File

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

View File

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

View File

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

View File

@@ -5,112 +5,248 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from django.contrib import messages
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin from django.db.models import Sum
from django.http import HttpRequest from django.http import HttpRequest, JsonResponse
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from ema.forms import NewEmaForm, EditEmaForm from ema.forms import NewEmaForm, EditEmaForm
from ema.models import Ema from ema.models import Ema
from ema.tables import EmaTable from ema.tables import EmaTable
from konova.decorators import shared_access_required, conservation_office_group_required, login_required_modal 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.forms.modals import RemoveModalForm from konova.forms.modals import RemoveModalForm
from konova.views.base import BaseIndexView, BaseIdentifierGeneratorView, BaseNewSpatialLocatedObjectFormView, \ from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
BaseEditSpatialLocatedObjectFormView from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.views.detail import BaseDetailView from konova.utils.message_templates import RECORDED_BLOCKS_EDIT, IDENTIFIER_REPLACED, FORM_INVALID, \
DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE
class EmaIndexView(LoginRequiredMixin, BaseIndexView): @login_required
_TAB_TITLE = _("EMAs - Overview") def index_view(request: HttpRequest):
_INDEX_TABLE_CLS = EmaTable """ Renders the index view for EMAs
def _get_queryset(self): Args:
qs = Ema.objects.filter( request (HttpRequest): The incoming request
deleted=None,
).order_by( Returns:
"-modified__timestamp"
) """
return qs 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)
class NewEmaFormView(BaseNewSpatialLocatedObjectFormView): @login_required
_FORM_CLS = NewEmaForm @conservation_office_group_required
_MODEL_CLS = Ema def new_view(request: HttpRequest):
_TEMPLATE = "ema/form/view.html" """
_TAB_TITLE = _("New EMA") Renders a view for a new eco account creation
_REDIRECT_URL = "ema:detail"
def _user_has_permission(self, user): Args:
# User has to be an ets user request (HttpRequest): The incoming request
return user.is_ets_user()
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)
class EditEmaFormView(BaseEditSpatialLocatedObjectFormView): @login_required
_MODEL_CLS = Ema @conservation_office_group_required
_FORM_CLS = EditEmaForm def new_id_view(request: HttpRequest):
_TEMPLATE = "ema/form/view.html" """ JSON endpoint
_REDIRECT_URL = "ema:detail"
_TAB_TITLE = _("Edit {}")
def _user_has_permission(self, user): Provides fetching of free identifiers for e.g. AJAX calls
# User has to be an ets user
return user.is_ets_user()
"""
class EmaIdentifierGeneratorView(LoginRequiredMixin, BaseIdentifierGeneratorView): tmp = Ema()
_MODEL_CLS = Ema identifier = tmp.generate_new_identifier()
_REDIRECT_URL = "ema:index" while Ema.objects.filter(identifier=identifier).exists():
identifier = tmp.generate_new_identifier()
def _user_has_permission(self, user): return JsonResponse(
return user.is_ets_user() data={
"gen_data": identifier
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(),
} }
return context )
@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)
@login_required_modal @login_required_modal

View File

@@ -5,36 +5,77 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 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.urls import reverse
from django.utils.translation import gettext_lazy as _
from compensation.views.compensation.report import BaseCompensationReportView
from ema.models import Ema from ema.models import Ema
from konova.sub_settings.django_settings import BASE_URL from konova.contexts import BaseContext
from konova.utils.qrcode import QrCode 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
@uuid_required
def report_view(request:HttpRequest, id: str):
""" Renders the public report view
class EmaReportView(BaseCompensationReportView): Args:
_TEMPLATE = "ema/report/report.html" request (HttpRequest): The incoming request
_MODEL = Ema id (str): The id of the intervention
def _get_report_context(self, obj): Returns:
report_url = BASE_URL + reverse("ema:report", args=(obj.id,))
qrcode_report = QrCode(report_url, 10)
qrcode_lanis = QrCode(obj.get_LANIS_link(), 7)
generic_compensation_report_context = self._get_compensation_report_context(obj) """
# Reuse the compensation report template since EMAs are structurally identical
template = "ema/report/report.html"
ema = get_object_or_404(Ema, id=id)
report_context = { tab_title = _("Report {}").format(ema.identifier)
"qrcode": { # If intervention is not recorded (yet or currently) we need to render another template without any data
"img": qrcode_report.get_img(), if not ema.is_ready_for_publish():
"url": qrcode_report.get_content(), template = "report/unavailable.html"
}, context = {
"qrcode_lanis": { TAB_TITLE_IDENTIFIER: tab_title,
"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(generic_compensation_report_context) context = BaseContext(request, context).context
return report_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)

View File

@@ -14,12 +14,10 @@ from intervention.views.deduction import NewInterventionDeductionView, EditInter
RemoveInterventionDeductionView RemoveInterventionDeductionView
from intervention.views.document import NewInterventionDocumentView, GetInterventionDocumentView, \ from intervention.views.document import NewInterventionDocumentView, GetInterventionDocumentView, \
RemoveInterventionDocumentView, EditInterventionDocumentView RemoveInterventionDocumentView, EditInterventionDocumentView
from intervention.views.intervention import remove_view, \ from intervention.views.intervention import index_view, new_view, new_id_view, detail_view, edit_view, remove_view
InterventionIndexView, InterventionIdentifierGeneratorView, InterventionDetailView, NewInterventionFormView, \
EditInterventionFormView
from intervention.views.log import InterventionLogView from intervention.views.log import InterventionLogView
from intervention.views.record import InterventionRecordView from intervention.views.record import InterventionRecordView
from intervention.views.report import InterventionReportView from intervention.views.report import report_view
from intervention.views.resubmission import InterventionResubmissionView from intervention.views.resubmission import InterventionResubmissionView
from intervention.views.revocation import new_revocation_view, edit_revocation_view, remove_revocation_view, \ from intervention.views.revocation import new_revocation_view, edit_revocation_view, remove_revocation_view, \
get_revocation_view get_revocation_view
@@ -27,18 +25,18 @@ from intervention.views.share import InterventionShareFormView, InterventionShar
app_name = "intervention" app_name = "intervention"
urlpatterns = [ urlpatterns = [
path("", InterventionIndexView.as_view(), name="index"), path("", index_view, name="index"),
path('new/', NewInterventionFormView.as_view(), name='new'), path('new/', new_view, name='new'),
path('new/id', InterventionIdentifierGeneratorView.as_view(), name='new-id'), path('new/id', new_id_view, name='new-id'),
path('<id>', InterventionDetailView.as_view(), name='detail'), path('<id>', detail_view, name='detail'),
path('<id>/log', InterventionLogView.as_view(), name='log'), path('<id>/log', InterventionLogView.as_view(), name='log'),
path('<id>/edit', EditInterventionFormView.as_view(), name='edit'), path('<id>/edit', edit_view, name='edit'),
path('<id>/remove', remove_view, name='remove'), path('<id>/remove', remove_view, name='remove'),
path('<id>/share/<token>', InterventionShareByTokenView.as_view(), name='share-token'), path('<id>/share/<token>', InterventionShareByTokenView.as_view(), name='share-token'),
path('<id>/share', InterventionShareFormView.as_view(), name='share-form'), path('<id>/share', InterventionShareFormView.as_view(), name='share-form'),
path('<id>/check', check_view, name='check'), path('<id>/check', check_view, name='check'),
path('<id>/record', InterventionRecordView.as_view(), name='record'), path('<id>/record', InterventionRecordView.as_view(), name='record'),
path('<id>/report', InterventionReportView.as_view(), name='report'), path('<id>/report', report_view, name='report'),
path('<id>/resub', InterventionResubmissionView.as_view(), name='resubmission-create'), path('<id>/resub', InterventionResubmissionView.as_view(), name='resubmission-create'),
# Compensations # Compensations

View File

@@ -5,22 +5,51 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from intervention.models import Intervention from intervention.models import Intervention
from konova.decorators import default_group_required, shared_access_required
from konova.views.deduction import AbstractNewDeductionView, AbstractEditDeductionView, AbstractRemoveDeductionView from konova.views.deduction import AbstractNewDeductionView, AbstractEditDeductionView, AbstractRemoveDeductionView
class NewInterventionDeductionView(LoginRequiredMixin, AbstractNewDeductionView): class NewInterventionDeductionView(AbstractNewDeductionView):
_MODEL = Intervention def _custom_check(self, obj):
_REDIRECT_URL = "intervention:detail" 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): class EditInterventionDeductionView(AbstractEditDeductionView):
_MODEL = Intervention def _custom_check(self, obj):
_REDIRECT_URL = "intervention:detail" 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): class RemoveInterventionDeductionView(AbstractRemoveDeductionView):
_MODEL = Intervention def _custom_check(self, obj):
_REDIRECT_URL = "intervention:detail" 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)

View File

@@ -7,8 +7,7 @@ Created on: 19.08.22
""" """
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin from django.http import JsonResponse, HttpRequest
from django.http import HttpRequest
from django.shortcuts import get_object_or_404, render, redirect from django.shortcuts import get_object_or_404, render, redirect
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@@ -17,106 +16,198 @@ from intervention.forms.intervention import EditInterventionForm, NewInterventio
from intervention.models import Intervention from intervention.models import Intervention
from intervention.tables import InterventionTable from intervention.tables import InterventionTable
from konova.contexts import BaseContext from konova.contexts import BaseContext
from konova.decorators import default_group_required, shared_access_required, login_required_modal from konova.decorators import default_group_required, shared_access_required, any_group_check, login_required_modal, \
uuid_required
from konova.forms import SimpleGeomForm from konova.forms import SimpleGeomForm
from konova.forms.modals import RemoveModalForm 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.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.message_templates import DATA_CHECKED_PREVIOUSLY_TEMPLATE, RECORDED_BLOCKS_EDIT, \ from konova.utils.message_templates import DATA_CHECKED_PREVIOUSLY_TEMPLATE, RECORDED_BLOCKS_EDIT, \
CHECK_STATE_RESET, FORM_INVALID, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE CHECK_STATE_RESET, FORM_INVALID, IDENTIFIER_REPLACED, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED, \
from konova.views.base import BaseIndexView, BaseIdentifierGeneratorView, BaseNewSpatialLocatedObjectFormView, \ GEOMETRIES_IGNORED_TEMPLATE
BaseEditSpatialLocatedObjectFormView
from konova.views.detail import BaseDetailView
class InterventionIndexView(LoginRequiredMixin, BaseIndexView): @login_required
_INDEX_TABLE_CLS = InterventionTable @any_group_check
_TAB_TITLE = _("Interventions - Overview") def index_view(request: HttpRequest):
"""
Renders the index view for Interventions
def _get_queryset(self): Args:
qs = Intervention.objects.filter( request (HttpRequest): The incoming request
deleted=None,
).select_related( Returns:
"legal" A rendered view
).order_by( """
"-modified__timestamp" template = "generic_index.html"
)
return qs # 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)
class NewInterventionFormView(BaseNewSpatialLocatedObjectFormView): @login_required
_MODEL_CLS = Intervention @default_group_required
_FORM_CLS = NewInterventionForm def new_view(request: HttpRequest):
_TEMPLATE = "intervention/form/view.html" """
_REDIRECT_URL = "intervention:detail" Renders a view for a new intervention creation
_TAB_TITLE = _("New intervention")
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 EditInterventionFormView(BaseEditSpatialLocatedObjectFormView): @login_required
_MODEL_CLS = Intervention @default_group_required
_FORM_CLS = EditInterventionForm def new_id_view(request: HttpRequest):
_TEMPLATE = "intervention/form/view.html" """ JSON endpoint
_REDIRECT_URL = "intervention:detail"
_TAB_TITLE = _("Edit {}")
Provides fetching of free identifiers for e.g. AJAX calls
class InterventionIdentifierGeneratorView(LoginRequiredMixin, BaseIdentifierGeneratorView): """
_MODEL_CLS = Intervention tmp_intervention = Intervention()
_REDIRECT_URL = "intervention:index" identifier = tmp_intervention.generate_new_identifier()
while Intervention.objects.filter(identifier=identifier).exists():
identifier = tmp_intervention.generate_new_identifier()
class InterventionDetailView(BaseDetailView): return JsonResponse(
_MODEL_CLS = Intervention data={
_TEMPLATE = "intervention/detail/view.html" "gen_data": identifier
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,
} }
return context )
@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)
@login_required @login_required

View File

@@ -5,41 +5,72 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 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.urls import reverse
from django.utils.translation import gettext_lazy as _
from intervention.models import Intervention from intervention.models import Intervention
from konova.sub_settings.django_settings import BASE_URL from konova.contexts import BaseContext
from konova.utils.qrcode import QrCode from konova.decorators import uuid_required
from konova.views.report import BaseReportView from konova.forms import SimpleGeomForm
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.generators import generate_qr_code
class InterventionReportView(BaseReportView): @uuid_required
_TEMPLATE = 'intervention/report/report.html' def report_view(request: HttpRequest, id: str):
_MODEL = Intervention """ Renders the public report view
def _get_report_context(self, obj: Intervention): Args:
""" Returns the specific context needed for an intervention report request (HttpRequest): The incoming request
id (str): The id of the intervention
Args: Returns:
obj (Intervention): The object for the report
Returns: """
dict: The object specific context for rendering the report template = "intervention/report/report.html"
""" intervention = get_object_or_404(Intervention, id=id)
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)
return { tab_title = _("Report {}").format(intervention.identifier)
"deductions": distinct_deductions, # If intervention is not recorded (yet or currently) we need to render another template without any data
"qrcode": { if not intervention.is_ready_for_publish():
"img": qrcode_report.get_img(), template = "report/unavailable.html"
"url": qrcode_report.get_content(), context = {
}, TAB_TITLE_IDENTIFIER: tab_title,
"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,7 +25,6 @@ class BaseForm(forms.Form):
cancel_redirect = None cancel_redirect = None
form_caption = None form_caption = None
instance = None # The data holding model object instance = None # The data holding model object
user = None # The performing user
request = None request = None
form_attrs = {} # Holds additional attributes, that can be used in the template form_attrs = {} # Holds additional attributes, that can be used in the template
has_required_fields = False # Automatically set. Triggers hint rendering in templates has_required_fields = False # Automatically set. Triggers hint rendering in templates
@@ -34,7 +33,6 @@ class BaseForm(forms.Form):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.instance = kwargs.pop("instance", None) self.instance = kwargs.pop("instance", None)
self.user = kwargs.pop("user", None)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
if self.request is not None: if self.request is not None:
self.user = self.request.user self.user = self.request.user
@@ -48,7 +46,7 @@ class BaseForm(forms.Form):
self.__check_valid_label_input_ratio() self.__check_valid_label_input_ratio()
@abstractmethod @abstractmethod
def save(self, *arg, **kwargs): def save(self):
# To be implemented in subclasses! # To be implemented in subclasses!
pass pass

View File

@@ -290,6 +290,6 @@ Overwrites netgis.css attributes
background: var(--rlp-red) !important; background: var(--rlp-red) !important;
} }
.modal{ .netgis-menu{
z-index: 100000; z-index: 1 !important;
} }

View File

@@ -5,11 +5,6 @@ Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 17.09.21 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: def format_german_float(num) -> str:
@@ -24,28 +19,3 @@ def format_german_float(num) -> str:
num (str): The number as german Gleitkommazahl num (str): The number as german Gleitkommazahl
""" """
return format(num, "0,.2f").replace(",", "X").replace(".", ",").replace("X", ".") 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(**kwargs: dict):
uuid = kwargs.get("uuid", None) or kwargs.get("id", None)
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,6 +7,10 @@ Created on: 09.11.20
""" """
import random import random
import string import string
import qrcode
import qrcode.image.svg
from io import BytesIO
def generate_token() -> str: def generate_token() -> str:
@@ -38,3 +42,23 @@ def generate_random_string(length: int, use_numbers: bool = False, use_letters_l
ret_val = "".join(random.choice(elements) for i in range(length)) ret_val = "".join(random.choice(elements) for i in range(length))
return ret_val 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

@@ -1,47 +0,0 @@
"""
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

@@ -1,325 +0,0 @@
"""
Author: Michel Peltriaux
Created on: 15.10.25
"""
from abc import abstractmethod
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpRequest, JsonResponse
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
from konova.utils.message_templates import MISSING_GROUP_PERMISSION, DATA_UNSHARED, IDENTIFIER_REPLACED, \
GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE, RECORDED_BLOCKS_EDIT, CHECK_STATE_RESET, 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
class BaseModalFormView(BaseView):
_TEMPLATE = "modal/modal_form.html"
_TAB_TITLE = None
class Meta:
abstract = True
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

@@ -6,60 +6,30 @@ Created on: 22.08.22
""" """
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.http import Http404, HttpRequest from django.http import Http404
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.urls import reverse from django.urls import reverse
from django.views import View
from intervention.forms.modals.deduction import NewEcoAccountDeductionModalForm, EditEcoAccountDeductionModalForm, \ from intervention.forms.modals.deduction import NewEcoAccountDeductionModalForm, EditEcoAccountDeductionModalForm, \
RemoveEcoAccountDeductionModalForm RemoveEcoAccountDeductionModalForm
from konova.utils.message_templates import DEDUCTION_ADDED, DEDUCTION_EDITED, DEDUCTION_REMOVED, DEDUCTION_UNKNOWN from konova.utils.message_templates import DEDUCTION_ADDED, DEDUCTION_EDITED, DEDUCTION_REMOVED, DEDUCTION_UNKNOWN
from konova.views.base import BaseModalFormView
class AbstractDeductionView(BaseModalFormView): class AbstractDeductionView(View):
_MODEL = None model = None
_REDIRECT_URL = None redirect_url = None
def _custom_check(self, obj): def _custom_check(self, obj):
""" """
Can be used by inheriting classes to provide custom checks before further processing Can be used by inheriting classes to provide custom checks before further processing
""" """
pass raise NotImplementedError("Must be implemented in subclasses")
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.objects.get(
id=kwargs.get("id")
)
ret_val = obj.is_shared_with(user)
except ObjectDoesNotExist:
ret_val = False
return ret_val
class AbstractNewDeductionView(AbstractDeductionView): class AbstractNewDeductionView(AbstractDeductionView):
class Meta: class Meta:
abstract = True abstract = True
@@ -73,13 +43,13 @@ class AbstractNewDeductionView(AbstractDeductionView):
Returns: Returns:
""" """
obj = get_object_or_404(self._MODEL, id=id) obj = get_object_or_404(self.model, id=id)
self._custom_check(obj) self._custom_check(obj)
form = NewEcoAccountDeductionModalForm(request.POST or None, instance=obj, request=request) form = NewEcoAccountDeductionModalForm(request.POST or None, instance=obj, request=request)
return form.process_request( return form.process_request(
request, request,
msg_success=DEDUCTION_ADDED, msg_success=DEDUCTION_ADDED,
redirect_url=reverse(self._REDIRECT_URL, args=(id,)) + "#related_data", redirect_url=reverse(self.redirect_url, args=(id,)) + "#related_data",
) )
def post(self, request, id: str): def post(self, request, id: str):
@@ -87,6 +57,10 @@ class AbstractNewDeductionView(AbstractDeductionView):
class AbstractEditDeductionView(AbstractDeductionView): class AbstractEditDeductionView(AbstractDeductionView):
def _custom_check(self, obj):
pass
class Meta: class Meta:
abstract = True abstract = True
@@ -101,7 +75,7 @@ class AbstractEditDeductionView(AbstractDeductionView):
Returns: Returns:
""" """
obj = get_object_or_404(self._MODEL, id=id) obj = get_object_or_404(self.model, id=id)
self._custom_check(obj) self._custom_check(obj)
try: try:
eco_deduction = obj.deductions.get(id=deduction_id) eco_deduction = obj.deductions.get(id=deduction_id)
@@ -113,7 +87,7 @@ class AbstractEditDeductionView(AbstractDeductionView):
return form.process_request( return form.process_request(
request=request, request=request,
msg_success=DEDUCTION_EDITED, msg_success=DEDUCTION_EDITED,
redirect_url=reverse(self._REDIRECT_URL, args=(id,)) + "#related_data" redirect_url=reverse(self.redirect_url, args=(id,)) + "#related_data"
) )
def post(self, request, id: str, deduction_id: str): def post(self, request, id: str, deduction_id: str):
@@ -121,6 +95,10 @@ class AbstractEditDeductionView(AbstractDeductionView):
class AbstractRemoveDeductionView(AbstractDeductionView): class AbstractRemoveDeductionView(AbstractDeductionView):
def _custom_check(self, obj):
pass
class Meta: class Meta:
abstract = True abstract = True
@@ -135,7 +113,7 @@ class AbstractRemoveDeductionView(AbstractDeductionView):
Returns: Returns:
""" """
obj = get_object_or_404(self._MODEL, id=id) obj = get_object_or_404(self.model, id=id)
self._custom_check(obj) self._custom_check(obj)
try: try:
eco_deduction = obj.deductions.get(id=deduction_id) eco_deduction = obj.deductions.get(id=deduction_id)
@@ -146,7 +124,7 @@ class AbstractRemoveDeductionView(AbstractDeductionView):
return form.process_request( return form.process_request(
request=request, request=request,
msg_success=DEDUCTION_REMOVED, msg_success=DEDUCTION_REMOVED,
redirect_url=reverse(self._REDIRECT_URL, args=(id,)) + "#related_data" redirect_url=reverse(self.redirect_url, args=(id,)) + "#related_data"
) )
def post(self, request, id: str, deduction_id: str): def post(self, request, id: str, deduction_id: str):

View File

@@ -1,107 +0,0 @@
"""
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)
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,19 +9,21 @@ from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import Q from django.db.models import Q
from django.http import HttpRequest from django.http import HttpRequest
from django.shortcuts import render from django.shortcuts import render
from django.utils.decorators import method_decorator
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views import View
from compensation.models import EcoAccount, Compensation from compensation.models import EcoAccount, Compensation
from intervention.models import Intervention from intervention.models import Intervention
from konova.contexts import BaseContext from konova.contexts import BaseContext
from konova.decorators import any_group_check
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.views.base import BaseView
from news.models import ServerMessage from news.models import ServerMessage
class HomeView(LoginRequiredMixin, BaseView): class HomeView(LoginRequiredMixin, View):
_TEMPLATE = "konova/home.html"
@method_decorator(any_group_check)
def get(self, request: HttpRequest): def get(self, request: HttpRequest):
""" """
Renders the landing page Renders the landing page
@@ -32,6 +34,7 @@ class HomeView(LoginRequiredMixin, BaseView):
Returns: Returns:
A redirect A redirect
""" """
template = "konova/home.html"
user = request.user user = request.user
user_teams = user.shared_teams user_teams = user.shared_teams
@@ -72,12 +75,5 @@ class HomeView(LoginRequiredMixin, BaseView):
TAB_TITLE_IDENTIFIER: _("Home"), TAB_TITLE_IDENTIFIER: _("Home"),
} }
context = BaseContext(request, additional_context).context context = BaseContext(request, additional_context).context
return render(request, self._TEMPLATE, context) return render(request, 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,8 +10,9 @@ from json import JSONDecodeError
import requests import requests
import urllib3.util import urllib3.util
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.decorators import login_required
from django.http import JsonResponse, HttpRequest, HttpResponse from django.http import JsonResponse, HttpRequest, HttpResponse
from django.utils.decorators import method_decorator
from django.utils.http import urlencode from django.utils.http import urlencode
from django.views import View from django.views import View
@@ -21,13 +22,17 @@ 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 from konova.sub_settings.proxy_settings import PROXIES, GEOPORTAL_RLP_USER, GEOPORTAL_RLP_PASSWORD
class BaseClientProxyView(LoginRequiredMixin, View): class BaseClientProxyView(View):
""" Provides proxy functionality for NETGIS map client. """ Provides proxy functionality for NETGIS map client.
""" """
class Meta: class Meta:
abstract = True abstract = True
@method_decorator(login_required)
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
def _check_with_whitelist(self, url): def _check_with_whitelist(self, url):
parsed_url = urllib3.util.parse_url(url) parsed_url = urllib3.util.parse_url(url)
parsed_url_host = parsed_url.host parsed_url_host = parsed_url.host
@@ -62,6 +67,7 @@ class BaseClientProxyView(LoginRequiredMixin, View):
class ClientProxyParcelSearch(BaseClientProxyView): class ClientProxyParcelSearch(BaseClientProxyView):
def get(self, request: HttpRequest): def get(self, request: HttpRequest):
url = request.META.get("QUERY_STRING") url = request.META.get("QUERY_STRING")

View File

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

View File

@@ -58,7 +58,7 @@
[ [
{ "id": "webatlas_farbe", "folder": "bg", "type": "WMS", "order": -1, "title": "WebatlasRP farbig", "attribution": "LVermGeo", "url": "https://maps.service24.rlp.de/gisserver/services/RP/RP_WebAtlasRP/MapServer/WmsServer?", "name": "RP_WebAtlasRP", "active": true}, { "id": "webatlas_farbe", "folder": "bg", "type": "WMS", "order": -1, "title": "WebatlasRP farbig", "attribution": "LVermGeo", "url": "https://maps.service24.rlp.de/gisserver/services/RP/RP_WebAtlasRP/MapServer/WmsServer?", "name": "RP_WebAtlasRP", "active": true},
{ "id": "webatlas_grau", "folder": "bg", "type": "WMS", "order": -1, "title": "WebatlasRP grau", "attribution": "LVermGeo", "url": "https://maps.service24.rlp.de/gisserver/services/RP/RP_ETRS_Gt/MapServer/WmsServer?", "name": "0", "active": false }, { "id": "webatlas_grau", "folder": "bg", "type": "WMS", "order": -1, "title": "WebatlasRP grau", "attribution": "LVermGeo", "url": "https://maps.service24.rlp.de/gisserver/services/RP/RP_ETRS_Gt/MapServer/WmsServer?", "name": "0", "active": false },
{ "id": "luftbilder", "folder": "bg", "type": "WMS", "order": -1, "title": "Luftbilder", "attribution": "LVermGeo", "url": "https://geo4.service24.rlp.de/wms/dop_basis.fcgi?", "name": "rp_dop", "active": false }, { "id": "luftbilder", "folder": "bg", "type": "WMS", "order": -1, "title": "Luftbilder", "attribution": "LVermGeo", "url": "https://geo4.service24.rlp.de/wms/rp_dop20.fcgi?", "name": "rp_dop20", "active": false },
{ "id": "basemap_farbe", "folder": "bg", "type": "WMS", "order": -1, "title": "BasemapDE farbig", "attribution": "BKG", "url": "https://sgx.geodatenzentrum.de/wms_basemapde?", "name": "de_basemapde_web_raster_farbe", "active": false }, { "id": "basemap_farbe", "folder": "bg", "type": "WMS", "order": -1, "title": "BasemapDE farbig", "attribution": "BKG", "url": "https://sgx.geodatenzentrum.de/wms_basemapde?", "name": "de_basemapde_web_raster_farbe", "active": false },
{ "id": "basemap_grau", "folder": "bg", "type": "WMS", "order": -1, "title": "BasemapDE grau", "attribution": "BKG", "url": "https://sgx.geodatenzentrum.de/wms_basemapde?", "name": "de_basemapde_web_raster_grau", "active": false }, { "id": "basemap_grau", "folder": "bg", "type": "WMS", "order": -1, "title": "BasemapDE grau", "attribution": "BKG", "url": "https://sgx.geodatenzentrum.de/wms_basemapde?", "name": "de_basemapde_web_raster_grau", "active": false },
{ "id": "dtk_farbe", "folder": "bg", "type": "WMS", "order": -1, "title": "DTK5 farbig", "attribution": "LVermGeo", "url": "https://geo4.service24.rlp.de/wms/dtk5_rp.fcgi?", "name": "rp_dtk5", "active": false }, { "id": "dtk_farbe", "folder": "bg", "type": "WMS", "order": -1, "title": "DTK5 farbig", "attribution": "LVermGeo", "url": "https://geo4.service24.rlp.de/wms/dtk5_rp.fcgi?", "name": "rp_dtk5", "active": false },

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -26,7 +26,7 @@ class UserViewTestCase(BaseViewTestCase):
self.team.users.add(self.superuser) self.team.users.add(self.superuser)
self.team.admins.add(self.superuser) self.team.admins.add(self.superuser)
# Prepare urls # Prepare urls
self.index_url = reverse("user:detail", args=()) self.index_url = reverse("user:index", args=())
self.notification_url = reverse("user:notifications", args=()) self.notification_url = reverse("user:notifications", args=())
self.api_token_url = reverse("user:api-token", args=()) self.api_token_url = reverse("user:api-token", args=())
self.contact_url = reverse("user:contact", args=(self.superuser.id,)) 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_title, str(_("Edit notifications")))
self.assertEqual(form.form_caption, "") self.assertEqual(form.form_caption, "")
self.assertEqual(form.action_url, reverse("user:notifications")) self.assertEqual(form.action_url, reverse("user:notifications"))
self.assertEqual(form.cancel_redirect, reverse("user:detail")) self.assertEqual(form.cancel_redirect, reverse("user:index"))
def test_save(self): def test_save(self):
selected_notification = UserNotification.objects.first() selected_notification = UserNotification.objects.first()

View File

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

View File

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