Compare commits

...

11 Commits

Author SHA1 Message Date
afbdf221c3 # User view refactoring
* refactors majority of user views into class based views
* introduces BaseModalFormView and BaseView for even more generic usage
* renames url identifier user:index into user:detail for more clarity
2025-10-15 17:09:40 +02:00
be9f6f1b7e # Identifier Generator View EcoAccount refactoring
* refactors identifier generator view for ecoaccount
* simplifies base identifier generator view even further
2025-10-15 16:46:07 +02:00
80e8925a63 # Identifier Generator View Compensation refactoring
* refactors identifier generator view for compensation
2025-10-15 16:42:42 +02:00
c597e1934b # Identifier Generator View refactoring
* refactors identifier generator view for interventions
* simplifies same view for ema
2025-10-15 16:40:35 +02:00
a44d8658d4 # NewId Generator Ema refactoring
* introduces BaseNewIdentifierGeneratorView class
* refactors new identifier generator view for ema
2025-10-15 16:29:05 +02:00
bb71c0fcc8 # Index Ema refactoring
* refactors index view for ema
2025-10-15 16:14:03 +02:00
67acddf701 # Index EcoAccount refactoring
* refactors index view for eco account
2025-10-15 16:12:21 +02:00
21bb988d86 # Index Compensation refactoring
* refactors index view for compensations
2025-10-15 16:03:53 +02:00
1ceffccd40 # Index Intervention refactoring
* introduces BaseIndexView class
* refactors index view for interventions
2025-10-15 16:00:51 +02:00
e5db7f6b13 Merge pull request '# Small geometry processing' (#488) from 487_Small_geometry_processing into master
Reviewed-on: #488
2025-10-15 09:51:27 +02:00
442f3ceb37 # Small geometry processing
* changes SimpleGeomForm behaviour on small geometries (<1m²): These geometries will now be dismissed on processing
* adds a new info message in case of automatically removed geometries on saving
* updates tests
2025-10-15 09:50:59 +02:00
27 changed files with 434 additions and 371 deletions

View File

@@ -54,7 +54,7 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
post_data = {
"identifier": test_id,
"title": test_title,
"geom": geom_json,
"output": geom_json,
"intervention": self.intervention.id,
}
pre_creation_intervention_log_count = self.intervention.log.count()
@@ -94,7 +94,7 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
post_data = {
"identifier": test_id,
"title": test_title,
"geom": geom_json,
"output": geom_json,
}
pre_creation_intervention_log_count = self.intervention.log.count()
@@ -150,7 +150,7 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
"title": new_title,
"intervention": self.intervention.id, # just keep the intervention as it is
"comment": new_comment,
"geom": geojson,
"output": geojson,
}
self.client_user.post(url, post_data)
self.compensation.refresh_from_db()

View File

@@ -46,7 +46,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
post_data = {
"identifier": test_id,
"title": test_title,
"geom": geom_json,
"output": geom_json,
"surface": test_deductable_surface,
"conservation_office": test_conservation_office.id
}
@@ -103,7 +103,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
"identifier": new_identifier,
"title": new_title,
"comment": new_comment,
"geom": self.create_geojson(new_geometry),
"output": self.create_geojson(new_geometry),
"surface": test_deductable_surface,
"conservation_office": test_conservation_office.id
}

View File

@@ -17,14 +17,14 @@ from compensation.views.compensation.action import NewCompensationActionView, Ed
RemoveCompensationActionView
from compensation.views.compensation.state import NewCompensationStateView, EditCompensationStateView, \
RemoveCompensationStateView
from compensation.views.compensation.compensation import index_view, new_view, new_id_view, detail_view, edit_view, \
remove_view
from compensation.views.compensation.compensation import new_view, detail_view, edit_view, \
remove_view, CompensationIndexView, CompensationIdentifierGeneratorView
from compensation.views.compensation.log import CompensationLogView
urlpatterns = [
# Main compensation
path("", index_view, name="index"),
path('new/id', new_id_view, name='new-id'),
path("", CompensationIndexView.as_view(), name="index"),
path('new/id', CompensationIdentifierGeneratorView.as_view(), name='new-id'),
path('new/<intervention_id>', new_view, name='new'),
path('new', new_view, name='new'),
path('<id>', detail_view, name='detail'),

View File

@@ -8,8 +8,8 @@ Created on: 24.08.21
from django.urls import path
from compensation.autocomplete.eco_account import EcoAccountAutocomplete
from compensation.views.eco_account.eco_account import index_view, new_view, new_id_view, edit_view, remove_view, \
detail_view
from compensation.views.eco_account.eco_account import new_view, edit_view, remove_view, \
detail_view, EcoAccountIndexView, EcoAccountIdentifierGeneratorView
from compensation.views.eco_account.log import EcoAccountLogView
from compensation.views.eco_account.record import EcoAccountRecordView
from compensation.views.eco_account.report import report_view
@@ -28,9 +28,9 @@ from compensation.views.eco_account.deduction import NewEcoAccountDeductionView,
app_name = "acc"
urlpatterns = [
path("", index_view, name="index"),
path("", EcoAccountIndexView.as_view(), name="index"),
path('new/', new_view, name='new'),
path('new/id', new_id_view, name='new-id'),
path('new/id', EcoAccountIdentifierGeneratorView.as_view(), name='new-id'),
path('<id>', detail_view, name='detail'),
path('<id>/log', EcoAccountLogView.as_view(), name='log'),
path('<id>/record', EcoAccountRecordView.as_view(), name='record'),

View File

@@ -7,8 +7,8 @@ Created on: 19.08.22
"""
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Sum
from django.http import HttpRequest, JsonResponse
from django.shortcuts import get_object_or_404, render, redirect
from django.urls import reverse
@@ -27,38 +27,22 @@ from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.message_templates import COMPENSATION_REMOVED_TEMPLATE, DATA_CHECKED_PREVIOUSLY_TEMPLATE, \
RECORDED_BLOCKS_EDIT, CHECK_STATE_RESET, FORM_INVALID, PARAMS_INVALID, IDENTIFIER_REPLACED, \
COMPENSATION_ADDED_TEMPLATE, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED
COMPENSATION_ADDED_TEMPLATE, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE
from konova.views.base import BaseIndexView, BaseIdentifierGeneratorView
@login_required
@any_group_check
def index_view(request: HttpRequest):
"""
Renders the index view for compensation
class CompensationIndexView(LoginRequiredMixin, BaseIndexView):
_TAB_TITLE = _("Compensations - Overview")
_INDEX_TABLE_CLS = CompensationTable
Args:
request (HttpRequest): The incoming request
Returns:
A rendered view
"""
template = "generic_index.html"
compensations = Compensation.objects.filter(
deleted=None, # only show those which are not deleted individually
intervention__deleted=None, # and don't show the ones whose intervention has been deleted
).order_by(
"-modified__timestamp"
)
table = CompensationTable(
request=request,
queryset=compensations
)
context = {
"table": table,
TAB_TITLE_IDENTIFIER: _("Compensations - Overview"),
}
context = BaseContext(request, context).context
return render(request, template, context)
def _get_queryset(self):
qs = Compensation.objects.filter(
deleted=None, # only show those which are not deleted individually
intervention__deleted=None, # and don't show the ones whose intervention has been deleted
).order_by(
"-modified__timestamp"
)
return qs
@login_required
@@ -103,11 +87,19 @@ def new_view(request: HttpRequest, intervention_id: str = None):
)
)
messages.success(request, COMPENSATION_ADDED_TEMPLATE.format(comp.identifier))
if geom_form.geometry_simplified:
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",)
@@ -123,23 +115,9 @@ def new_view(request: HttpRequest, intervention_id: str = None):
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
}
)
class CompensationIdentifierGeneratorView(LoginRequiredMixin, BaseIdentifierGeneratorView):
_MODEL_CLS = Compensation
_REDIRECT_URL_NAME = "compensation:index"
@login_required
@@ -179,11 +157,19 @@ def edit_view(request: HttpRequest, id: str):
if intervention_is_checked:
messages.info(request, CHECK_STATE_RESET)
messages.success(request, _("Compensation {} edited").format(comp.identifier))
if geom_form.geometry_simplified:
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",)

View File

@@ -7,7 +7,7 @@ Created on: 19.08.22
"""
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.db.models import Sum
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpRequest, JsonResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
@@ -23,37 +23,21 @@ from konova.forms import SimpleGeomForm
from konova.settings import ETS_GROUP, DEFAULT_GROUP, ZB_GROUP
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.message_templates import CANCEL_ACC_RECORDED_OR_DEDUCTED, RECORDED_BLOCKS_EDIT, FORM_INVALID, \
IDENTIFIER_REPLACED, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED
IDENTIFIER_REPLACED, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE
from konova.views.base import BaseIndexView, BaseIdentifierGeneratorView
@login_required
@any_group_check
def index_view(request: HttpRequest):
"""
Renders the index view for eco accounts
class EcoAccountIndexView(LoginRequiredMixin, BaseIndexView):
_INDEX_TABLE_CLS = EcoAccountTable
_TAB_TITLE = _("Eco-account - Overview")
Args:
request (HttpRequest): The incoming request
Returns:
A rendered view
"""
template = "generic_index.html"
eco_accounts = EcoAccount.objects.filter(
deleted=None,
).order_by(
"-modified__timestamp"
)
table = EcoAccountTable(
request=request,
queryset=eco_accounts
)
context = {
"table": table,
TAB_TITLE_IDENTIFIER: _("Eco-account - Overview"),
}
context = BaseContext(request, context).context
return render(request, template, context)
def _get_queryset(self):
qs = EcoAccount.objects.filter(
deleted=None,
).order_by(
"-modified__timestamp"
)
return qs
@login_required
@@ -84,11 +68,19 @@ def new_view(request: HttpRequest):
)
)
messages.success(request, _("Eco-Account {} added").format(acc.identifier))
if geom_form.geometry_simplified:
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:acc:detail", id=acc.id)
else:
messages.error(request, FORM_INVALID, extra_tags="danger",)
@@ -104,23 +96,9 @@ def new_view(request: HttpRequest):
return render(request, template, context)
@login_required
@default_group_required
def new_id_view(request: HttpRequest):
""" JSON endpoint
Provides fetching of free identifiers for e.g. AJAX calls
"""
tmp = EcoAccount()
identifier = tmp.generate_new_identifier()
while EcoAccount.objects.filter(identifier=identifier).exists():
identifier = tmp.generate_new_identifier()
return JsonResponse(
data={
"gen_data": identifier
}
)
class EcoAccountIdentifierGeneratorView(LoginRequiredMixin, BaseIdentifierGeneratorView):
_MODEL_CLS = EcoAccount
_REDIRECT_URL_NAME = "compensation:acc:index"
@login_required
@@ -156,11 +134,19 @@ def edit_view(request: HttpRequest, id: str):
# The data form takes the geom form for processing, as well as the performing user
acc = data_form.save(request.user, geom_form)
messages.success(request, _("Eco-Account {} edited").format(acc.identifier))
if geom_form.geometry_simplified:
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:acc:detail", id=acc.id)
else:
messages.error(request, FORM_INVALID, extra_tags="danger",)

View File

@@ -46,7 +46,7 @@ class EmaWorkflowTestCase(BaseWorkflowTestCase):
post_data = {
"identifier": test_id,
"title": test_title,
"geom": geom_json,
"output": geom_json,
"conservation_office": test_conservation_office.id
}
self.client_user.post(new_url, post_data)
@@ -99,7 +99,7 @@ class EmaWorkflowTestCase(BaseWorkflowTestCase):
"identifier": new_identifier,
"title": new_title,
"comment": new_comment,
"geom": self.create_geojson(new_geometry),
"output": self.create_geojson(new_geometry),
"conservation_office": test_conservation_office.id
}
self.client_user.post(url, post_data)

View File

@@ -48,7 +48,7 @@ class NewEmaFormTestCase(BaseTestCase):
)
geom_form_data = json.loads(geom_form_data)
geom_form_data = {
"geom": json.dumps(geom_form_data)
"output": json.dumps(geom_form_data)
}
geom_form = SimpleGeomForm(geom_form_data)
@@ -116,7 +116,7 @@ class EditEmaFormTestCase(BaseTestCase):
)
geom_form_data = json.loads(geom_form_data)
geom_form_data = {
"geom": json.dumps(geom_form_data)
"output": json.dumps(geom_form_data)
}
geom_form = SimpleGeomForm(geom_form_data)

View File

@@ -10,7 +10,8 @@ from django.urls import path
from ema.views.action import NewEmaActionView, EditEmaActionView, RemoveEmaActionView
from ema.views.deadline import NewEmaDeadlineView, EditEmaDeadlineView, RemoveEmaDeadlineView
from ema.views.document import NewEmaDocumentView, EditEmaDocumentView, RemoveEmaDocumentView, GetEmaDocumentView
from ema.views.ema import index_view, new_view, new_id_view, detail_view, edit_view, remove_view
from ema.views.ema import new_view, detail_view, edit_view, remove_view, EmaIndexView, \
EmaIdentifierGeneratorView
from ema.views.log import EmaLogView
from ema.views.record import EmaRecordView
from ema.views.report import report_view
@@ -20,9 +21,9 @@ from ema.views.state import NewEmaStateView, EditEmaStateView, RemoveEmaStateVie
app_name = "ema"
urlpatterns = [
path("", index_view, name="index"),
path("", EmaIndexView.as_view(), name="index"),
path("new/", new_view, name="new"),
path("new/id", new_id_view, name="new-id"),
path("new/id", EmaIdentifierGeneratorView.as_view(), name="new-id"),
path("<id>", detail_view, name="detail"),
path('<id>/log', EmaLogView.as_view(), name='log'),
path('<id>/edit', edit_view, name='edit'),

View File

@@ -7,7 +7,7 @@ Created on: 19.08.22
"""
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.db.models import Sum
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpRequest, JsonResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
@@ -24,36 +24,21 @@ from konova.forms.modals import RemoveModalForm
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.message_templates import RECORDED_BLOCKS_EDIT, IDENTIFIER_REPLACED, FORM_INVALID, \
DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED
DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE, MISSING_GROUP_PERMISSION
from konova.views.base import BaseIndexView, BaseIdentifierGeneratorView
@login_required
def index_view(request: HttpRequest):
""" Renders the index view for EMAs
class EmaIndexView(LoginRequiredMixin, BaseIndexView):
_TAB_TITLE = _("EMAs - Overview")
_INDEX_TABLE_CLS = EmaTable
Args:
request (HttpRequest): The incoming request
Returns:
"""
template = "generic_index.html"
emas = Ema.objects.filter(
deleted=None,
).order_by(
"-modified__timestamp"
)
table = EmaTable(
request,
queryset=emas
)
context = {
"table": table,
TAB_TITLE_IDENTIFIER: _("EMAs - Overview"),
}
context = BaseContext(request, context).context
return render(request, template, context)
def _get_queryset(self):
qs = Ema.objects.filter(
deleted=None,
).order_by(
"-modified__timestamp"
)
return qs
@login_required
@@ -84,11 +69,17 @@ def new_view(request: HttpRequest):
)
)
messages.success(request, _("EMA {} added").format(ema.identifier))
if geom_form.geometry_simplified:
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:
@@ -105,23 +96,12 @@ def new_view(request: HttpRequest):
return render(request, template, context)
@login_required
@conservation_office_group_required
def new_id_view(request: HttpRequest):
""" JSON endpoint
class EmaIdentifierGeneratorView(LoginRequiredMixin, BaseIdentifierGeneratorView):
_MODEL_CLS = Ema
_REDIRECT_URL_NAME = "ema:index"
Provides fetching of free identifiers for e.g. AJAX calls
"""
tmp = Ema()
identifier = tmp.generate_new_identifier()
while Ema.objects.filter(identifier=identifier).exists():
identifier = tmp.generate_new_identifier()
return JsonResponse(
data={
"gen_data": identifier
}
)
def _user_has_permission(self, user):
return user.is_ets_user()
@login_required
@@ -215,11 +195,19 @@ def edit_view(request: HttpRequest, id: str):
# 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.geometry_simplified:
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",)

View File

@@ -60,7 +60,7 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase):
post_data = {
"identifier": test_id,
"title": test_title,
"geom": geom_json,
"output": geom_json,
}
response = self.client_user.post(
new_url,

View File

@@ -62,7 +62,7 @@ class NewInterventionFormTestCase(BaseTestCase):
)
geom_form_data = json.loads(geom_form_data)
geom_form_data = {
"geom": json.dumps(geom_form_data)
"output": json.dumps(geom_form_data)
}
geom_form = SimpleGeomForm(geom_form_data)
@@ -104,7 +104,7 @@ class EditInterventionFormTestCase(NewInterventionFormTestCase):
)
geom_form_data = json.loads(geom_form_data)
geom_form_data = {
"geom": json.dumps(geom_form_data)
"output": json.dumps(geom_form_data)
}
geom_form = SimpleGeomForm(geom_form_data)

View File

@@ -14,7 +14,8 @@ from intervention.views.deduction import NewInterventionDeductionView, EditInter
RemoveInterventionDeductionView
from intervention.views.document import NewInterventionDocumentView, GetInterventionDocumentView, \
RemoveInterventionDocumentView, EditInterventionDocumentView
from intervention.views.intervention import index_view, new_view, new_id_view, detail_view, edit_view, remove_view
from intervention.views.intervention import new_view, detail_view, edit_view, remove_view, \
InterventionIndexView, InterventionIdentifierGeneratorView
from intervention.views.log import InterventionLogView
from intervention.views.record import InterventionRecordView
from intervention.views.report import report_view
@@ -25,9 +26,9 @@ from intervention.views.share import InterventionShareFormView, InterventionShar
app_name = "intervention"
urlpatterns = [
path("", index_view, name="index"),
path("", InterventionIndexView.as_view(), name="index"),
path('new/', new_view, name='new'),
path('new/id', new_id_view, name='new-id'),
path('new/id', InterventionIdentifierGeneratorView.as_view(), name='new-id'),
path('<id>', detail_view, name='detail'),
path('<id>/log', InterventionLogView.as_view(), name='log'),
path('<id>/edit', edit_view, name='edit'),

View File

@@ -7,6 +7,7 @@ Created on: 19.08.22
"""
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import JsonResponse, HttpRequest
from django.shortcuts import get_object_or_404, render, redirect
from django.urls import reverse
@@ -23,41 +24,24 @@ from konova.forms.modals import RemoveModalForm
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.message_templates import DATA_CHECKED_PREVIOUSLY_TEMPLATE, RECORDED_BLOCKS_EDIT, \
CHECK_STATE_RESET, FORM_INVALID, IDENTIFIER_REPLACED, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED
CHECK_STATE_RESET, FORM_INVALID, IDENTIFIER_REPLACED, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED, \
GEOMETRIES_IGNORED_TEMPLATE
from konova.views.base import BaseIndexView, BaseIdentifierGeneratorView
@login_required
@any_group_check
def index_view(request: HttpRequest):
"""
Renders the index view for Interventions
class InterventionIndexView(LoginRequiredMixin, BaseIndexView):
_INDEX_TABLE_CLS = InterventionTable
_TAB_TITLE = _("Interventions - Overview")
Args:
request (HttpRequest): The incoming request
Returns:
A rendered view
"""
template = "generic_index.html"
# Filtering by user access is performed in table filter inside InterventionTableFilter class
interventions = Intervention.objects.filter(
deleted=None, # not deleted
).select_related(
"legal"
).order_by(
"-modified__timestamp"
)
table = InterventionTable(
request=request,
queryset=interventions
)
context = {
"table": table,
TAB_TITLE_IDENTIFIER: _("Interventions - Overview"),
}
context = BaseContext(request, context).context
return render(request, template, context)
def _get_queryset(self):
qs = Intervention.objects.filter(
deleted=None,
).select_related(
"legal"
).order_by(
"-modified__timestamp"
)
return qs
@login_required
@@ -88,11 +72,19 @@ def new_view(request: HttpRequest):
)
)
messages.success(request, _("Intervention {} added").format(intervention.identifier))
if geom_form.geometry_simplified:
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",)
@@ -108,23 +100,9 @@ def new_view(request: HttpRequest):
return render(request, template, context)
@login_required
@default_group_required
def new_id_view(request: HttpRequest):
""" JSON endpoint
Provides fetching of free identifiers for e.g. AJAX calls
"""
tmp_intervention = Intervention()
identifier = tmp_intervention.generate_new_identifier()
while Intervention.objects.filter(identifier=identifier).exists():
identifier = tmp_intervention.generate_new_identifier()
return JsonResponse(
data={
"gen_data": identifier
}
)
class InterventionIdentifierGeneratorView(LoginRequiredMixin, BaseIdentifierGeneratorView):
_MODEL_CLS = Intervention
_REDIRECT_URL_NAME = "intervention:index"
@login_required
@@ -236,11 +214,19 @@ def edit_view(request: HttpRequest, id: str):
messages.success(request, _("Intervention {} edited").format(intervention.identifier))
if intervention_is_checked:
messages.info(request, CHECK_STATE_RESET)
if geom_form.geometry_simplified:
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",)

View File

@@ -25,8 +25,8 @@ class SimpleGeomForm(BaseForm):
""" A geometry form for rendering geometry read-only using a widget
"""
read_only = True
geometry_simplified = False
read_only: bool = True
_geometry_simplified: bool = False
output = JSONField(
label=_("Geometry"),
help_text=_(""),
@@ -34,6 +34,7 @@ class SimpleGeomForm(BaseForm):
required=False,
disabled=False,
)
_num_geometries_ignored: int = 0
def __init__(self, *args, **kwargs):
self.read_only = kwargs.pop("read_only", True)
@@ -48,6 +49,7 @@ class SimpleGeomForm(BaseForm):
raise AttributeError
geojson = self.instance.geometry.as_feature_collection(srid=DEFAULT_SRID_RLP)
self._set_geojson_properties(geojson, title=self.instance.identifier or None)
geom = json.dumps(geojson)
except AttributeError:
# If no geometry exists for this form, we simply set the value to None and zoom to the maximum level
@@ -61,7 +63,7 @@ class SimpleGeomForm(BaseForm):
is_valid = True
# Get geojson from form
geom = self.data["output"]
geom = self.data.get("output", None)
if geom is None or len(geom) == 0:
# empty geometry is a valid geometry
self.cleaned_data["output"] = MultiPolygon(srid=DEFAULT_SRID_RLP).ewkt
@@ -100,7 +102,13 @@ class SimpleGeomForm(BaseForm):
is_valid &= False
return is_valid
is_valid &= self.__is_area_valid(g)
is_area_valid = self.__is_area_valid(g)
if not is_area_valid:
# Geometries with an invalid size will not be saved to the db
# We assume these are malicious snippets which are not supposed to be in the geometry in the first place
self._num_geometries_ignored += 1
continue
g = Polygon.from_ewkt(g.ewkt)
is_valid &= g.valid
if not g.valid:
@@ -147,15 +155,6 @@ class SimpleGeomForm(BaseForm):
"""
is_area_valid = geom.area > 1 # > 1m² (SRID:25832)
if not is_area_valid:
self.add_error(
"output",
_("Geometry must be greater than 1m². Currently is {}").format(
float(geom.area)
)
)
return is_area_valid
def __simplify_geometry(self, geom, max_vert: int):
@@ -208,13 +207,29 @@ class SimpleGeomForm(BaseForm):
if not is_vertices_num_valid:
geometry.geom = self.__simplify_geometry(geometry.geom, max_vert=GEOM_MAX_VERTICES)
geometry.save()
self.geometry_simplified = True
self._geometry_simplified = True
# Start parcel update and geometry conflict checking procedure in a background process
celery_update_parcels.delay(geometry.id)
celery_check_for_geometry_conflicts.delay(geometry.id)
return geometry
def get_num_geometries_ignored(self):
""" Returns the number of geometries which had to be ignored for various reasons
Returns:
"""
return self._num_geometries_ignored
def has_geometry_simplified(self):
""" Returns whether the geometry has been simplified or not.
Returns:
"""
return self._geometry_simplified
def __flatten_geom_to_2D(self, geom):
"""
Enforces a given OGRGeometry from higher dimensions into 2D
@@ -225,11 +240,12 @@ class SimpleGeomForm(BaseForm):
geom = gdal.OGRGeometry(g_wkt)
return geom
def _set_properties(self, geojson: dict, title: str):
def _set_geojson_properties(self, geojson: dict, title: str = None):
""" Toggles the editable property of the geojson for proper handling in map client
Args:
geojson (dict): The GeoJson
title (str): An alternative title for the geometry
Returns:
geojson (dict): The altered GeoJson

View File

@@ -124,6 +124,7 @@ class GeometryTestCase(BaseTestCase):
{
"type": "Feature",
"geometry": json.loads(p.json),
"properties": {}
}
for p in polygons
]

View File

@@ -5,6 +5,9 @@ Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 17.09.21
"""
from django.contrib import messages
from django.utils.translation import gettext_lazy as _
from django.http import HttpRequest
def format_german_float(num) -> str:
@@ -19,3 +22,19 @@ def format_german_float(num) -> str:
num (str): The number as german Gleitkommazahl
"""
return format(num, "0,.2f").replace(",", "X").replace(".", ",").replace("X", ".")
def check_user_is_in_any_group(request: HttpRequest):
"""
Checks for any group membership. Adds a message in case of having none.
"""
user = request.user
# Inform user about missing group privileges!
groups = user.groups.all()
if not groups:
messages.info(
request,
_("+++ Attention: You are not part of any group. You won't be able to create, edit or do anything. Please contact an administrator. +++")
)
return request

View File

@@ -83,6 +83,7 @@ EDITED_GENERAL_DATA = _("Edited general data")
# Geometry
GEOMETRY_CONFLICT_WITH_TEMPLATE = _("Geometry conflict detected with {}")
GEOMETRY_SIMPLIFIED = _("The geometry contained more than {} vertices. It had to be simplified to match the allowed limit of {} vertices.").format(GEOM_MAX_VERTICES, GEOM_MAX_VERTICES)
GEOMETRIES_IGNORED_TEMPLATE = _("The geometry contained {} parts which have been detected as invalid (e.g. too small to be valid). These parts have been removed. Please check the stored geometry.")
# INTERVENTION
INTERVENTION_HAS_REVOCATIONS_TEMPLATE = _("This intervention has {} revocations")

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

@@ -0,0 +1,98 @@
"""
Author: Michel Peltriaux
Created on: 15.10.25
"""
from abc import abstractmethod
from django.contrib import messages
from django.http import HttpRequest, JsonResponse
from django.shortcuts import render, redirect
from django.urls import reverse
from django.views import View
from konova.contexts import BaseContext
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
class BaseView(View):
_TEMPLATE: str = "CHANGE_ME"
_TAB_TITLE: str = "CHANGE_ME"
class Meta:
abstract = True
class BaseModalFormView(BaseView):
_TEMPLATE = "modal/modal_form.html"
_TAB_TITLE = None
class BaseIndexView(BaseView):
""" Base class for index views
"""
_TEMPLATE = "generic_index.html"
_INDEX_TABLE_CLS = None
class Meta:
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):
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
class BaseIdentifierGeneratorView(View):
_MODEL_CLS = None
_REDIRECT_URL_NAME: str = "home"
class Meta:
abstract = True
def dispatch(self, request, *args, **kwargs):
if not self._user_has_permission(request.user):
messages.info(request, MISSING_GROUP_PERMISSION)
return redirect(reverse(self._REDIRECT_URL_NAME))
return super().dispatch(request, *args, **kwargs)
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()

Binary file not shown.

View File

@@ -45,7 +45,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-05-12 14:22+0200\n"
"POT-Creation-Date: 2025-10-15 09:11+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -1298,13 +1298,13 @@ msgid "Compensation {} edited"
msgstr "Kompensation {} bearbeitet"
#: compensation/views/compensation/compensation.py:196
#: compensation/views/eco_account/eco_account.py:173 ema/views/ema.py:232
#: compensation/views/eco_account/eco_account.py:173 ema/views/ema.py:238
#: intervention/views/intervention.py:253
msgid "Edit {}"
msgstr "Bearbeite {}"
#: compensation/views/compensation/report.py:35
#: compensation/views/eco_account/report.py:35 ema/views/report.py:35
#: compensation/views/eco_account/report.py:36 ema/views/report.py:35
#: intervention/views/report.py:35
msgid "Report {}"
msgstr "Bericht {}"
@@ -1325,7 +1325,7 @@ msgstr "Ökokonto {} bearbeitet"
msgid "Eco-account removed"
msgstr "Ökokonto entfernt"
#: ema/forms.py:42 ema/tests/unit/test_forms.py:27 ema/views/ema.py:102
#: ema/forms.py:42 ema/tests/unit/test_forms.py:27 ema/views/ema.py:108
msgid "New EMA"
msgstr "Neue EMA hinzufügen"
@@ -1361,11 +1361,11 @@ msgstr "EMAs - Übersicht"
msgid "EMA {} added"
msgstr "EMA {} hinzugefügt"
#: ema/views/ema.py:217
#: ema/views/ema.py:223
msgid "EMA {} edited"
msgstr "EMA {} bearbeitet"
#: ema/views/ema.py:256
#: ema/views/ema.py:262
msgid "EMA removed"
msgstr "EMA entfernt"
@@ -1815,10 +1815,6 @@ msgid "Only surfaces allowed. Points or lines must be buffered."
msgstr ""
"Nur Flächen erlaubt. Punkte oder Linien müssen zu Flächen gepuffert werden."
#: konova/forms/geometry_form.py:153
msgid "Geometry must be greater than 1m². Currently is {}m²"
msgstr "Geometrie muss größer als 1m² sein. Aktueller Flächeninhalt: {}m²"
#: konova/forms/modals/document_form.py:37
msgid "When has this file been created? Important for photos."
msgstr "Wann wurde diese Datei erstellt oder das Foto aufgenommen?"
@@ -2266,24 +2262,33 @@ msgstr ""
"Die Geometrie enthielt mehr als {} Eckpunkte. Sie musste vereinfacht werden "
"um die Obergrenze von {} erlaubten Eckpunkten einzuhalten."
#: konova/utils/message_templates.py:88
#: konova/utils/message_templates.py:86
msgid ""
"The geometry contained {} parts which have been detected as invalid (e.g. "
"too small to be valid). These parts have been removed. Please check the "
"stored geometry."
msgstr ""
"Die Geometrie enthielt {} invalide Bestandteile (z.B. unaussagekräftige Kleinstflächen)."
"Diese Bestandteile wurden automatisch entfernt. Bitte überprüfen Sie die angepasste Geometrie."
#: konova/utils/message_templates.py:89
msgid "This intervention has {} revocations"
msgstr "Dem Eingriff liegen {} Widersprüche vor"
#: konova/utils/message_templates.py:91
#: konova/utils/message_templates.py:92
msgid "Checked on {} by {}"
msgstr "Am {} von {} geprüft worden"
#: konova/utils/message_templates.py:92
#: konova/utils/message_templates.py:93
msgid "Data has changed since last check on {} by {}"
msgstr ""
"Daten wurden nach der letzten Prüfung geändert. Letzte Prüfung am {} durch {}"
#: konova/utils/message_templates.py:93
#: konova/utils/message_templates.py:94
msgid "Current data not checked yet"
msgstr "Momentane Daten noch nicht geprüft"
#: konova/utils/message_templates.py:96
#: konova/utils/message_templates.py:97
msgid "New token generated. Administrators need to validate."
msgstr "Neuer Token generiert. Administratoren sind informiert."
@@ -2313,14 +2318,6 @@ msgstr "Home"
msgid "Log"
msgstr "Log"
#: konova/views/map_proxy.py:84
msgid ""
"The external service is currently unavailable.<br>Please try again in a few "
"moments..."
msgstr ""
"Der externe Dienst ist zur Zeit nicht erreichbar.<br>Versuchen Sie es in ein "
"paar Sekunden nochmal."
#: konova/views/record.py:30
msgid "{} unrecorded"
msgstr "{} entzeichnet"
@@ -2377,17 +2374,19 @@ msgstr "Alle"
msgid "News"
msgstr "Neuigkeiten"
#: templates/400.html:7
#: templates/400.html:12
msgid "Request was invalid"
msgstr "Anfrage fehlerhaft"
#: templates/400.html:10
#: templates/400.html:17
msgid "There seems to be a problem with the link you opened."
msgstr "Es scheint ein Problem mit dem Link zu geben, den Sie geöffnet haben."
#: templates/400.html:11
#: templates/400.html:18
msgid "Make sure the URL is valid (no whitespaces, properly copied, ...)."
msgstr "Stellen Sie sicher, dass die URL gültig ist (keine Leerzeichen, fehlerfrei kopiert, ...)."
msgstr ""
"Stellen Sie sicher, dass die URL gültig ist (keine Leerzeichen, fehlerfrei "
"kopiert, ...)."
#: templates/404.html:7
msgid "Not found"
@@ -2401,11 +2400,11 @@ msgstr "Die angeforderten Daten existieren nicht."
msgid "Make sure the URL is valid (no whitespaces, ...)."
msgstr "Stellen Sie sicher, dass die URL gültig ist (keine Leerzeichen, ...)."
#: templates/500.html:7
#: templates/500.html:12
msgid "Server Error"
msgstr ""
#: templates/500.html:10
#: templates/500.html:17
msgid "Something happened. Admins have been informed. We are working on it!"
msgstr ""
"Irgendetwas ist passiert. Die Administratoren wurden informiert. Wir "

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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