# Detail View

* introduces BaseDetailView
* refactors detail views for EIV, KOM, OEK, EMA from function based to class based
* refactors already class based HomeView to inherit from new BaseView
This commit is contained in:
mpeltriaux 2025-10-17 15:40:45 +02:00
parent 61ec9c8c9b
commit a9b402862b
12 changed files with 357 additions and 294 deletions

View File

@ -17,8 +17,8 @@ from compensation.views.compensation.action import NewCompensationActionView, Ed
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 new_view, detail_view, edit_view, \ from compensation.views.compensation.compensation import new_view, edit_view, \
remove_view, CompensationIndexView, CompensationIdentifierGeneratorView remove_view, CompensationIndexView, CompensationIdentifierGeneratorView, CompensationDetailView
from compensation.views.compensation.log import CompensationLogView from compensation.views.compensation.log import CompensationLogView
urlpatterns = [ urlpatterns = [
@ -27,7 +27,7 @@ urlpatterns = [
path('new/id', CompensationIdentifierGeneratorView.as_view(), name='new-id'), path('new/id', CompensationIdentifierGeneratorView.as_view(), name='new-id'),
path('new/<intervention_id>', new_view, name='new'), path('new/<intervention_id>', new_view, name='new'),
path('new', new_view, name='new'), path('new', new_view, name='new'),
path('<id>', detail_view, name='detail'), path('<id>', CompensationDetailView.as_view(), name='detail'),
path('<id>/log', CompensationLogView.as_view(), name='log'), path('<id>/log', CompensationLogView.as_view(), name='log'),
path('<id>/edit', edit_view, name='edit'), path('<id>/edit', edit_view, name='edit'),
path('<id>/remove', remove_view, name='remove'), path('<id>/remove', remove_view, name='remove'),

View File

@ -9,7 +9,7 @@ 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 new_view, edit_view, remove_view, \ from compensation.views.eco_account.eco_account import new_view, edit_view, remove_view, \
detail_view, EcoAccountIndexView, EcoAccountIdentifierGeneratorView EcoAccountIndexView, EcoAccountIdentifierGeneratorView, EcoAccountDetailView
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 EcoAccountReportView
@ -31,7 +31,7 @@ urlpatterns = [
path("", EcoAccountIndexView.as_view(), name="index"), path("", EcoAccountIndexView.as_view(), name="index"),
path('new/', new_view, name='new'), path('new/', new_view, name='new'),
path('new/id', EcoAccountIdentifierGeneratorView.as_view(), name='new-id'), path('new/id', EcoAccountIdentifierGeneratorView.as_view(), name='new-id'),
path('<id>', detail_view, name='detail'), path('<id>', EcoAccountDetailView.as_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', EcoAccountReportView.as_view(), name='report'),

View File

@ -19,16 +19,15 @@ 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.contexts import BaseContext from konova.contexts import BaseContext
from konova.decorators import shared_access_required, default_group_required, any_group_check, login_required_modal, \ from konova.decorators import shared_access_required, default_group_required, 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 COMPENSATION_REMOVED_TEMPLATE, DATA_CHECKED_PREVIOUSLY_TEMPLATE, \ from konova.utils.message_templates import COMPENSATION_REMOVED_TEMPLATE, DATA_CHECKED_PREVIOUSLY_TEMPLATE, \
RECORDED_BLOCKS_EDIT, CHECK_STATE_RESET, FORM_INVALID, PARAMS_INVALID, IDENTIFIER_REPLACED, \ RECORDED_BLOCKS_EDIT, CHECK_STATE_RESET, FORM_INVALID, PARAMS_INVALID, IDENTIFIER_REPLACED, \
COMPENSATION_ADDED_TEMPLATE, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE COMPENSATION_ADDED_TEMPLATE, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE
from konova.views.base import BaseIndexView, BaseIdentifierGeneratorView from konova.views.base import BaseIndexView, BaseIdentifierGeneratorView
from konova.views.detail import BaseDetailView
class CompensationIndexView(LoginRequiredMixin, BaseIndexView): class CompensationIndexView(LoginRequiredMixin, BaseIndexView):
@ -185,82 +184,71 @@ def edit_view(request: HttpRequest, id: str):
return render(request, template, context) return render(request, template, context)
@login_required class CompensationDetailView(BaseDetailView):
@any_group_check _MODEL_CLS = Compensation
@uuid_required _TEMPLATE = "compensation/detail/compensation/view.html"
def detail_view(request: HttpRequest, id: str):
""" Renders a detail view for a compensation
Args: def _get_object(self, id: str):
request (HttpRequest): The incoming request """ Returns the compensation
id (str): The compensation's id
Returns: Args:
id (str): The compensation's id
""" Returns:
template = "compensation/detail/compensation/view.html" obj (Compensation): The compensation
comp = get_object_or_404( """
Compensation.objects.select_related( comp = get_object_or_404(
"modified", Compensation.objects.select_related(
"created", "modified",
"geometry" "created",
), "geometry"
id=id, ),
deleted=None, id=id,
intervention__deleted=None, 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
) )
return comp
context = { def _get_detail_context(self, obj: Compensation):
"obj": comp, """ Generate object specific detail context for view
"last_checked": last_checked,
"last_checked_tooltip": last_checked_tooltip, Args:
"geom_form": geom_form, obj (): The record
"parcels": parcels,
"is_entry_shared": is_data_shared, Returns:
"actions": actions,
"before_states": before_states, """
"after_states": after_states, # Order states according to surface
"sum_before_states": sum_before_states, before_states = obj.before_states.all().prefetch_related("biotope_type").order_by("-surface")
"sum_after_states": sum_after_states, after_states = obj.after_states.all().prefetch_related("biotope_type").order_by("-surface")
"diff_states": diff_states, actions = obj.actions.all().prefetch_related("action_type")
"is_default_member": _user.in_group(DEFAULT_GROUP),
"is_zb_member": _user.in_group(ZB_GROUP), # Precalculate logical errors between before- and after-states
"is_ets_member": _user.in_group(ETS_GROUP), # Sum() returns None in case of no states, so we catch that and replace it with 0 for easier handling
"LANIS_LINK": comp.get_LANIS_link(), sum_before_states = obj.get_surface_before_states()
TAB_TITLE_IDENTIFIER: f"{comp.identifier} - {comp.title}", sum_after_states = obj.get_surface_after_states()
"has_finished_deadlines": comp.get_finished_deadlines().exists(), diff_states = abs(sum_before_states - sum_after_states)
}
context = BaseContext(request, context).context last_checked = obj.intervention.get_last_checked_action()
return render(request, template, context) last_checked_tooltip = ""
if last_checked:
last_checked_tooltip = DATA_CHECKED_PREVIOUSLY_TEMPLATE.format(
last_checked.get_timestamp_str_formatted(),
last_checked.user
)
context = {
"last_checked": last_checked,
"last_checked_tooltip": last_checked_tooltip,
"actions": actions,
"before_states": before_states,
"after_states": after_states,
"sum_before_states": sum_before_states,
"sum_after_states": sum_after_states,
"diff_states": diff_states,
"has_finished_deadlines": obj.get_finished_deadlines().exists(),
}
return context
@login_required_modal @login_required_modal

View File

@ -8,7 +8,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.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpRequest, JsonResponse from django.http import HttpRequest
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,14 +17,14 @@ 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, any_group_check, login_required_modal, \ from konova.decorators import shared_access_required, default_group_required, login_required_modal
uuid_required
from konova.forms import SimpleGeomForm from konova.forms import SimpleGeomForm
from konova.settings import ETS_GROUP, DEFAULT_GROUP, ZB_GROUP from konova.settings import ETS_GROUP
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.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, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE IDENTIFIER_REPLACED, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE
from konova.views.base import BaseIndexView, BaseIdentifierGeneratorView from konova.views.base import BaseIndexView, BaseIdentifierGeneratorView
from konova.views.detail import BaseDetailView
class EcoAccountIndexView(LoginRequiredMixin, BaseIndexView): class EcoAccountIndexView(LoginRequiredMixin, BaseIndexView):
@ -162,86 +162,72 @@ def edit_view(request: HttpRequest, id: str):
return render(request, template, context) return render(request, template, context)
@login_required class EcoAccountDetailView(BaseDetailView):
@any_group_check _MODEL_CLS = EcoAccount
@uuid_required _TEMPLATE = "compensation/detail/eco_account/view.html"
def detail_view(request: HttpRequest, id: str):
""" Renders a detail view for a compensation
Args: def _get_object(self, id: str):
request (HttpRequest): The incoming request """ Fetch object for detail view
id (str): The compensation's id
Returns: Args:
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 """
before_states = acc.before_states.order_by("-surface") acc = get_object_or_404(
after_states = acc.after_states.order_by("-surface") EcoAccount.objects.prefetch_related(
"deadlines",
# Precalculate logical errors between before- and after-states ).select_related(
# Sum() returns None in case of no states, so we catch that and replace it with 0 for easier handling 'geometry',
sum_before_states = acc.get_surface_before_states() 'responsible',
sum_after_states = acc.get_surface_after_states() ),
diff_states = abs(sum_before_states - sum_after_states) id=id,
# Calculate rest of available surface for deductions deleted=None,
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
context = { def _get_detail_context(self, obj: EcoAccount):
"obj": acc, """ Generate object specific detail context for view
"geom_form": geom_form,
"parcels": parcels, Args:
"is_entry_shared": is_data_shared, obj (): The record
"before_states": before_states,
"after_states": after_states, Returns:
"sum_before_states": sum_before_states,
"sum_after_states": sum_after_states, """
"diff_states": diff_states, # Order states according to surface
"available": available_relative, before_states = obj.before_states.order_by("-surface")
"available_total": available_total, after_states = obj.after_states.order_by("-surface")
"is_default_member": _user.in_group(DEFAULT_GROUP),
"is_zb_member": _user.in_group(ZB_GROUP), # Precalculate logical errors between before- and after-states
"is_ets_member": _user.in_group(ETS_GROUP), # Sum() returns None in case of no states, so we catch that and replace it with 0 for easier handling
"LANIS_LINK": acc.get_LANIS_link(), sum_before_states = obj.get_surface_before_states()
"deductions": deductions, sum_after_states = obj.get_surface_after_states()
"actions": actions, diff_states = abs(sum_before_states - sum_after_states)
TAB_TITLE_IDENTIFIER: f"{acc.identifier} - {acc.title}", # Calculate rest of available surface for deductions
"has_finished_deadlines": acc.get_finished_deadlines().exists(), available_total = obj.deductable_rest
} available_relative = obj.get_deductable_rest_relative()
context = BaseContext(request, context).context
return render(request, template, context) # Prefetch related data to decrease the amount of db connections
deductions = obj.deductions.filter(
intervention__deleted=None,
)
actions = obj.actions.all()
context = {
"before_states": before_states,
"after_states": after_states,
"sum_before_states": sum_before_states,
"sum_after_states": sum_after_states,
"diff_states": diff_states,
"available": available_relative,
"available_total": available_total,
"deductions": deductions,
"actions": actions,
"has_finished_deadlines": obj.get_finished_deadlines().exists(),
}
return context
@login_required_modal @login_required_modal

View File

@ -10,8 +10,8 @@ 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 new_view, detail_view, edit_view, remove_view, EmaIndexView, \ from ema.views.ema import new_view, edit_view, remove_view, EmaIndexView, \
EmaIdentifierGeneratorView EmaIdentifierGeneratorView, EmaDetailView
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 EmaReportView
@ -24,7 +24,7 @@ urlpatterns = [
path("", EmaIndexView.as_view(), name="index"), path("", EmaIndexView.as_view(), name="index"),
path("new/", new_view, name="new"), path("new/", new_view, name="new"),
path("new/id", EmaIdentifierGeneratorView.as_view(), name="new-id"), path("new/id", EmaIdentifierGeneratorView.as_view(), name="new-id"),
path("<id>", detail_view, name="detail"), path("<id>", EmaDetailView.as_view(), name="detail"),
path('<id>/log', EmaLogView.as_view(), name='log'), path('<id>/log', EmaLogView.as_view(), name='log'),
path('<id>/edit', edit_view, name='edit'), path('<id>/edit', edit_view, name='edit'),
path('<id>/remove', remove_view, name='remove'), path('<id>/remove', remove_view, name='remove'),

View File

@ -8,7 +8,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.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpRequest, JsonResponse from django.http import HttpRequest
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,15 +17,14 @@ 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.contexts import BaseContext from konova.contexts import BaseContext
from konova.decorators import shared_access_required, conservation_office_group_required, login_required_modal, \ from konova.decorators import shared_access_required, conservation_office_group_required, 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 RECORDED_BLOCKS_EDIT, IDENTIFIER_REPLACED, FORM_INVALID, \ from konova.utils.message_templates import RECORDED_BLOCKS_EDIT, IDENTIFIER_REPLACED, FORM_INVALID, \
DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE, MISSING_GROUP_PERMISSION GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE
from konova.views.base import BaseIndexView, BaseIdentifierGeneratorView from konova.views.base import BaseIndexView, BaseIdentifierGeneratorView
from konova.views.detail import BaseDetailView
class EmaIndexView(LoginRequiredMixin, BaseIndexView): class EmaIndexView(LoginRequiredMixin, BaseIndexView):
@ -104,64 +103,50 @@ class EmaIdentifierGeneratorView(LoginRequiredMixin, BaseIdentifierGeneratorView
return user.is_ets_user() return user.is_ets_user()
@login_required class EmaDetailView(BaseDetailView):
@uuid_required _MODEL_CLS = Ema
def detail_view(request: HttpRequest, id: str): _TEMPLATE = "ema/detail/view.html"
""" Renders the detail view of an EMA
Args: def _get_object(self, id: str):
request (HttpRequest): The incoming request """ Fetch object for detail view
id (str): The EMA id
Returns: Args:
id (str): The record's 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() ema = get_object_or_404(Ema, id=id, deleted=None)
_user = request.user return ema
is_entry_shared = ema.is_shared_with(_user)
# Order states according to surface def _get_detail_context(self, obj: Ema):
before_states = ema.before_states.all().order_by("-surface") """ Generate object specific detail context for view
after_states = ema.after_states.all().order_by("-surface")
# Precalculate logical errors between before- and after-states Args:
# Sum() returns None in case of no states, so we catch that and replace it with 0 for easier handling obj (): The record
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) Returns:
requesting_user_is_only_shared_user = ema.is_only_shared_with(_user) """
if requesting_user_is_only_shared_user: # Order states according to surface
messages.info( before_states = obj.before_states.all().order_by("-surface")
request, after_states = obj.after_states.all().order_by("-surface")
DO_NOT_FORGET_TO_SHARE
)
context = { # Precalculate logical errors between before- and after-states
"obj": ema, # Sum() returns None in case of no states, so we catch that and replace it with 0 for easier handling
"geom_form": geom_form, sum_before_states = obj.get_surface_before_states()
"parcels": parcels, sum_after_states = obj.get_surface_after_states()
"is_entry_shared": is_entry_shared, diff_states = abs(sum_before_states - sum_after_states)
"before_states": before_states,
"after_states": after_states, context = {
"sum_before_states": sum_before_states, "before_states": before_states,
"sum_after_states": sum_after_states, "after_states": after_states,
"diff_states": diff_states, "sum_before_states": sum_before_states,
"is_default_member": _user.in_group(DEFAULT_GROUP), "sum_after_states": sum_after_states,
"is_zb_member": _user.in_group(ZB_GROUP), "diff_states": diff_states,
"is_ets_member": _user.in_group(ETS_GROUP), "has_finished_deadlines": obj.get_finished_deadlines().exists(),
"LANIS_LINK": ema.get_LANIS_link(), }
TAB_TITLE_IDENTIFIER: f"{ema.identifier} - {ema.title}", return context
"has_finished_deadlines": ema.get_finished_deadlines().exists(),
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required @login_required

View File

@ -14,8 +14,8 @@ 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 new_view, detail_view, edit_view, remove_view, \ from intervention.views.intervention import new_view, edit_view, remove_view, \
InterventionIndexView, InterventionIdentifierGeneratorView InterventionIndexView, InterventionIdentifierGeneratorView, InterventionDetailView
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 InterventionReportView
@ -29,7 +29,7 @@ urlpatterns = [
path("", InterventionIndexView.as_view(), name="index"), path("", InterventionIndexView.as_view(), name="index"),
path('new/', new_view, name='new'), path('new/', new_view, name='new'),
path('new/id', InterventionIdentifierGeneratorView.as_view(), name='new-id'), path('new/id', InterventionIdentifierGeneratorView.as_view(), name='new-id'),
path('<id>', detail_view, name='detail'), path('<id>', InterventionDetailView.as_view(), name='detail'),
path('<id>/log', InterventionLogView.as_view(), name='log'), path('<id>/log', InterventionLogView.as_view(), name='log'),
path('<id>/edit', edit_view, name='edit'), path('<id>/edit', edit_view, name='edit'),
path('<id>/remove', remove_view, name='remove'), path('<id>/remove', remove_view, name='remove'),

View File

@ -27,6 +27,7 @@ from konova.utils.message_templates import DATA_CHECKED_PREVIOUSLY_TEMPLATE, REC
CHECK_STATE_RESET, FORM_INVALID, IDENTIFIER_REPLACED, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED, \ CHECK_STATE_RESET, FORM_INVALID, IDENTIFIER_REPLACED, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED, \
GEOMETRIES_IGNORED_TEMPLATE GEOMETRIES_IGNORED_TEMPLATE
from konova.views.base import BaseIndexView, BaseIdentifierGeneratorView from konova.views.base import BaseIndexView, BaseIdentifierGeneratorView
from konova.views.detail import BaseDetailView
class InterventionIndexView(LoginRequiredMixin, BaseIndexView): class InterventionIndexView(LoginRequiredMixin, BaseIndexView):
@ -105,78 +106,59 @@ class InterventionIdentifierGeneratorView(LoginRequiredMixin, BaseIdentifierGene
_REDIRECT_URL = "intervention:index" _REDIRECT_URL = "intervention:index"
@login_required class InterventionDetailView(BaseDetailView):
@any_group_check _MODEL_CLS = Intervention
@uuid_required _TEMPLATE = "intervention/detail/view.html"
def detail_view(request: HttpRequest, id: str):
""" Renders a detail view for viewing an intervention's data
Args: def _get_object(self, id: str):
request (HttpRequest): The incoming request """ Returns the intervention
id (str): The intervention's id
Returns: Args:
id (str): The intervention's id
""" Returns:
template = "intervention/detail/view.html" obj (Intervention): The intervention
"""
# Fetch data, filter out deleted related data # Fetch data, filter out deleted related data
intervention = get_object_or_404( obj = get_object_or_404(
Intervention.objects.select_related( self._MODEL_CLS.objects.select_related(
"geometry", "geometry",
"legal", "legal",
"responsible", "responsible",
).prefetch_related( ).prefetch_related(
"legal__revocations", "legal__revocations",
), ),
id=id, id=id,
deleted=None 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
) )
return obj
has_payment_without_document = intervention.payments.exists() and not intervention.get_documents()[1].exists() def _get_detail_context(self, obj: Intervention):
""" Generate object specific detail context for view
requesting_user_is_only_shared_user = intervention.is_only_shared_with(_user) Args:
if requesting_user_is_only_shared_user: obj (): The record
messages.info(
request,
DO_NOT_FORGET_TO_SHARE
)
context = { Returns:
"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) """
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
)
context = BaseContext(request, context).context has_payment_without_document = obj.payments.exists() and not obj.get_documents()[1].exists()
return render(request, template, context) 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 @login_required

View File

@ -5,9 +5,11 @@ 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.contrib import messages
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.http import HttpRequest from django.http import HttpRequest, Http404
def format_german_float(num) -> str: def format_german_float(num) -> str:
@ -38,3 +40,12 @@ def check_user_is_in_any_group(request: HttpRequest):
_("+++ Attention: You are not part of any group. You won't be able to create, edit or do anything. Please contact an administrator. +++") _("+++ 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 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

@ -27,12 +27,16 @@ class BaseView(View):
abstract = True abstract = True
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
request = check_user_is_in_any_group(request)
if not self._user_has_permission(request.user): if not self._user_has_permission(request.user):
messages.info(request, MISSING_GROUP_PERMISSION) messages.info(request, MISSING_GROUP_PERMISSION)
return redirect(reverse(self._REDIRECT_URL_ERROR)) return redirect(reverse(self._REDIRECT_URL_ERROR))
if not self._user_has_shared_access(request.user, **kwargs): if not self._user_has_shared_access(request.user, **kwargs):
messages.info(request, DATA_UNSHARED) messages.info(request, DATA_UNSHARED)
return redirect(reverse(self._REDIRECT_URL_ERROR)) return redirect(reverse(self._REDIRECT_URL_ERROR))
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
def _user_has_permission(self, user): def _user_has_permission(self, user):
@ -77,10 +81,6 @@ class BaseIndexView(BaseView):
class Meta: class Meta:
abstract = True abstract = True
def dispatch(self, request, *args, **kwargs):
request = check_user_is_in_any_group(request)
return super().dispatch(request, *args, **kwargs)
def get(self, request: HttpRequest): def get(self, request: HttpRequest):
qs = self._get_queryset() qs = self._get_queryset()
table = self._INDEX_TABLE_CLS( table = self._INDEX_TABLE_CLS(

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

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

View File

@ -9,21 +9,19 @@ from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import Q from django.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, View): class HomeView(LoginRequiredMixin, BaseView):
_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
@ -34,7 +32,6 @@ class HomeView(LoginRequiredMixin, View):
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
@ -75,5 +72,12 @@ class HomeView(LoginRequiredMixin, View):
TAB_TITLE_IDENTIFIER: _("Home"), TAB_TITLE_IDENTIFIER: _("Home"),
} }
context = BaseContext(request, additional_context).context context = BaseContext(request, additional_context).context
return render(request, template, context) return render(request, self._TEMPLATE, context)
def _user_has_permission(self, user):
# No specific permission needed for home view
return True
def _user_has_shared_access(self, user, **kwargs):
# No specific constraint needed for home view
return True