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

View File

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

View File

@@ -17,14 +17,14 @@ 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 index_view, new_view, new_id_view, detail_view, edit_view, \ from compensation.views.compensation.compensation import new_view, detail_view, edit_view, \
remove_view remove_view, CompensationIndexView, CompensationIdentifierGeneratorView
from compensation.views.compensation.log import CompensationLogView from compensation.views.compensation.log import CompensationLogView
urlpatterns = [ urlpatterns = [
# Main compensation # Main compensation
path("", index_view, name="index"), path("", CompensationIndexView.as_view(), name="index"),
path('new/id', new_id_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>', detail_view, name='detail'),

View File

@@ -8,8 +8,8 @@ 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 index_view, new_view, new_id_view, edit_view, remove_view, \ from compensation.views.eco_account.eco_account import new_view, edit_view, remove_view, \
detail_view detail_view, EcoAccountIndexView, EcoAccountIdentifierGeneratorView
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 report_view from compensation.views.eco_account.report import report_view
@@ -28,9 +28,9 @@ from compensation.views.eco_account.deduction import NewEcoAccountDeductionView,
app_name = "acc" app_name = "acc"
urlpatterns = [ urlpatterns = [
path("", index_view, name="index"), path("", EcoAccountIndexView.as_view(), name="index"),
path('new/', new_view, name='new'), 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>', 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'),

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
@@ -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.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 COMPENSATION_ADDED_TEMPLATE, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE
from konova.views.base import BaseIndexView, BaseIdentifierGeneratorView
@login_required class CompensationIndexView(LoginRequiredMixin, BaseIndexView):
@any_group_check _TAB_TITLE = _("Compensations - Overview")
def index_view(request: HttpRequest): _INDEX_TABLE_CLS = CompensationTable
"""
Renders the index view for compensation
Args: def _get_queryset(self):
request (HttpRequest): The incoming request qs = Compensation.objects.filter(
Returns:
A rendered view
"""
template = "generic_index.html"
compensations = Compensation.objects.filter(
deleted=None, # only show those which are not deleted individually deleted=None, # only show those which are not deleted individually
intervention__deleted=None, # and don't show the ones whose intervention has been deleted intervention__deleted=None, # and don't show the ones whose intervention has been deleted
).order_by( ).order_by(
"-modified__timestamp" "-modified__timestamp"
) )
table = CompensationTable( return qs
request=request,
queryset=compensations
)
context = {
"table": table,
TAB_TITLE_IDENTIFIER: _("Compensations - Overview"),
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required @login_required
@@ -103,11 +87,19 @@ def new_view(request: HttpRequest, intervention_id: str = None):
) )
) )
messages.success(request, COMPENSATION_ADDED_TEMPLATE.format(comp.identifier)) messages.success(request, COMPENSATION_ADDED_TEMPLATE.format(comp.identifier))
if geom_form.geometry_simplified: if geom_form.has_geometry_simplified():
messages.info( messages.info(
request, request,
GEOMETRY_SIMPLIFIED 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) return redirect("compensation:detail", id=comp.id)
else: else:
messages.error(request, FORM_INVALID, extra_tags="danger",) 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) return render(request, template, context)
@login_required class CompensationIdentifierGeneratorView(LoginRequiredMixin, BaseIdentifierGeneratorView):
@default_group_required _MODEL_CLS = Compensation
def new_id_view(request: HttpRequest): _REDIRECT_URL_NAME = "compensation:index"
""" 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
}
)
@login_required @login_required
@@ -179,11 +157,19 @@ def edit_view(request: HttpRequest, id: str):
if intervention_is_checked: if intervention_is_checked:
messages.info(request, CHECK_STATE_RESET) messages.info(request, CHECK_STATE_RESET)
messages.success(request, _("Compensation {} edited").format(comp.identifier)) messages.success(request, _("Compensation {} edited").format(comp.identifier))
if geom_form.geometry_simplified: if geom_form.has_geometry_simplified():
messages.info( messages.info(
request, request,
GEOMETRY_SIMPLIFIED 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) return redirect("compensation:detail", id=comp.id)
else: else:
messages.error(request, FORM_INVALID, extra_tags="danger",) 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 import messages
from django.contrib.auth.decorators import login_required 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.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
@@ -23,37 +23,21 @@ from konova.forms import SimpleGeomForm
from konova.settings import ETS_GROUP, DEFAULT_GROUP, ZB_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, 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 class EcoAccountIndexView(LoginRequiredMixin, BaseIndexView):
@any_group_check _INDEX_TABLE_CLS = EcoAccountTable
def index_view(request: HttpRequest): _TAB_TITLE = _("Eco-account - Overview")
"""
Renders the index view for eco accounts
Args: def _get_queryset(self):
request (HttpRequest): The incoming request qs = EcoAccount.objects.filter(
Returns:
A rendered view
"""
template = "generic_index.html"
eco_accounts = EcoAccount.objects.filter(
deleted=None, deleted=None,
).order_by( ).order_by(
"-modified__timestamp" "-modified__timestamp"
) )
table = EcoAccountTable( return qs
request=request,
queryset=eco_accounts
)
context = {
"table": table,
TAB_TITLE_IDENTIFIER: _("Eco-account - Overview"),
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required @login_required
@@ -84,11 +68,19 @@ def new_view(request: HttpRequest):
) )
) )
messages.success(request, _("Eco-Account {} added").format(acc.identifier)) messages.success(request, _("Eco-Account {} added").format(acc.identifier))
if geom_form.geometry_simplified: if geom_form.has_geometry_simplified():
messages.info( messages.info(
request, request,
GEOMETRY_SIMPLIFIED 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) return redirect("compensation:acc:detail", id=acc.id)
else: else:
messages.error(request, FORM_INVALID, extra_tags="danger",) messages.error(request, FORM_INVALID, extra_tags="danger",)
@@ -104,23 +96,9 @@ def new_view(request: HttpRequest):
return render(request, template, context) return render(request, template, context)
@login_required class EcoAccountIdentifierGeneratorView(LoginRequiredMixin, BaseIdentifierGeneratorView):
@default_group_required _MODEL_CLS = EcoAccount
def new_id_view(request: HttpRequest): _REDIRECT_URL_NAME = "compensation:acc:index"
""" 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
@@ -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 # The data form takes the geom form for processing, as well as the performing user
acc = data_form.save(request.user, geom_form) acc = data_form.save(request.user, geom_form)
messages.success(request, _("Eco-Account {} edited").format(acc.identifier)) messages.success(request, _("Eco-Account {} edited").format(acc.identifier))
if geom_form.geometry_simplified: if geom_form.has_geometry_simplified():
messages.info( messages.info(
request, request,
GEOMETRY_SIMPLIFIED 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) return redirect("compensation:acc:detail", id=acc.id)
else: else:
messages.error(request, FORM_INVALID, extra_tags="danger",) messages.error(request, FORM_INVALID, extra_tags="danger",)

View File

@@ -46,7 +46,7 @@ class EmaWorkflowTestCase(BaseWorkflowTestCase):
post_data = { post_data = {
"identifier": test_id, "identifier": test_id,
"title": test_title, "title": test_title,
"geom": geom_json, "output": geom_json,
"conservation_office": test_conservation_office.id "conservation_office": test_conservation_office.id
} }
self.client_user.post(new_url, post_data) self.client_user.post(new_url, post_data)
@@ -99,7 +99,7 @@ class EmaWorkflowTestCase(BaseWorkflowTestCase):
"identifier": new_identifier, "identifier": new_identifier,
"title": new_title, "title": new_title,
"comment": new_comment, "comment": new_comment,
"geom": self.create_geojson(new_geometry), "output": self.create_geojson(new_geometry),
"conservation_office": test_conservation_office.id "conservation_office": test_conservation_office.id
} }
self.client_user.post(url, post_data) 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 = json.loads(geom_form_data)
geom_form_data = { geom_form_data = {
"geom": json.dumps(geom_form_data) "output": json.dumps(geom_form_data)
} }
geom_form = SimpleGeomForm(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 = json.loads(geom_form_data)
geom_form_data = { geom_form_data = {
"geom": json.dumps(geom_form_data) "output": json.dumps(geom_form_data)
} }
geom_form = SimpleGeomForm(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.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 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.log import EmaLogView
from ema.views.record import EmaRecordView from ema.views.record import EmaRecordView
from ema.views.report import report_view from ema.views.report import report_view
@@ -20,9 +21,9 @@ from ema.views.state import NewEmaStateView, EditEmaStateView, RemoveEmaStateVie
app_name = "ema" app_name = "ema"
urlpatterns = [ urlpatterns = [
path("", index_view, name="index"), path("", EmaIndexView.as_view(), name="index"),
path("new/", new_view, name="new"), 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>", detail_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'),

View File

@@ -7,7 +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.db.models import Sum from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpRequest, JsonResponse 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
@@ -24,36 +24,21 @@ from konova.forms.modals import RemoveModalForm
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP 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 DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE, MISSING_GROUP_PERMISSION
from konova.views.base import BaseIndexView, BaseIdentifierGeneratorView
@login_required class EmaIndexView(LoginRequiredMixin, BaseIndexView):
def index_view(request: HttpRequest): _TAB_TITLE = _("EMAs - Overview")
""" Renders the index view for EMAs _INDEX_TABLE_CLS = EmaTable
Args: def _get_queryset(self):
request (HttpRequest): The incoming request qs = Ema.objects.filter(
Returns:
"""
template = "generic_index.html"
emas = Ema.objects.filter(
deleted=None, deleted=None,
).order_by( ).order_by(
"-modified__timestamp" "-modified__timestamp"
) )
return qs
table = EmaTable(
request,
queryset=emas
)
context = {
"table": table,
TAB_TITLE_IDENTIFIER: _("EMAs - Overview"),
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required @login_required
@@ -84,11 +69,17 @@ def new_view(request: HttpRequest):
) )
) )
messages.success(request, _("EMA {} added").format(ema.identifier)) messages.success(request, _("EMA {} added").format(ema.identifier))
if geom_form.geometry_simplified: if geom_form.has_geometry_simplified():
messages.info( messages.info(
request, request,
GEOMETRY_SIMPLIFIED 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) return redirect("ema:detail", id=ema.id)
else: else:
@@ -105,23 +96,12 @@ def new_view(request: HttpRequest):
return render(request, template, context) return render(request, template, context)
@login_required class EmaIdentifierGeneratorView(LoginRequiredMixin, BaseIdentifierGeneratorView):
@conservation_office_group_required _MODEL_CLS = Ema
def new_id_view(request: HttpRequest): _REDIRECT_URL_NAME = "ema:index"
""" JSON endpoint
Provides fetching of free identifiers for e.g. AJAX calls def _user_has_permission(self, user):
return user.is_ets_user()
"""
tmp = Ema()
identifier = tmp.generate_new_identifier()
while Ema.objects.filter(identifier=identifier).exists():
identifier = tmp.generate_new_identifier()
return JsonResponse(
data={
"gen_data": identifier
}
)
@login_required @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 # The data form takes the geom form for processing, as well as the performing user
ema = data_form.save(request.user, geom_form) ema = data_form.save(request.user, geom_form)
messages.success(request, _("EMA {} edited").format(ema.identifier)) messages.success(request, _("EMA {} edited").format(ema.identifier))
if geom_form.geometry_simplified: if geom_form.has_geometry_simplified():
messages.info( messages.info(
request, request,
GEOMETRY_SIMPLIFIED 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) return redirect("ema:detail", id=ema.id)
else: else:
messages.error(request, FORM_INVALID, extra_tags="danger",) messages.error(request, FORM_INVALID, extra_tags="danger",)

View File

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

View File

@@ -62,7 +62,7 @@ class NewInterventionFormTestCase(BaseTestCase):
) )
geom_form_data = json.loads(geom_form_data) geom_form_data = json.loads(geom_form_data)
geom_form_data = { geom_form_data = {
"geom": json.dumps(geom_form_data) "output": json.dumps(geom_form_data)
} }
geom_form = SimpleGeomForm(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 = json.loads(geom_form_data)
geom_form_data = { geom_form_data = {
"geom": json.dumps(geom_form_data) "output": json.dumps(geom_form_data)
} }
geom_form = SimpleGeomForm(geom_form_data) geom_form = SimpleGeomForm(geom_form_data)

View File

@@ -14,7 +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 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.log import InterventionLogView
from intervention.views.record import InterventionRecordView from intervention.views.record import InterventionRecordView
from intervention.views.report import report_view from intervention.views.report import report_view
@@ -25,9 +26,9 @@ from intervention.views.share import InterventionShareFormView, InterventionShar
app_name = "intervention" app_name = "intervention"
urlpatterns = [ urlpatterns = [
path("", index_view, name="index"), path("", InterventionIndexView.as_view(), name="index"),
path('new/', new_view, name='new'), 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>', detail_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'),

View File

@@ -7,6 +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 JsonResponse, 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
@@ -23,41 +24,24 @@ from konova.forms.modals import RemoveModalForm
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP 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, 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 class InterventionIndexView(LoginRequiredMixin, BaseIndexView):
@any_group_check _INDEX_TABLE_CLS = InterventionTable
def index_view(request: HttpRequest): _TAB_TITLE = _("Interventions - Overview")
"""
Renders the index view for Interventions
Args: def _get_queryset(self):
request (HttpRequest): The incoming request qs = Intervention.objects.filter(
deleted=None,
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( ).select_related(
"legal" "legal"
).order_by( ).order_by(
"-modified__timestamp" "-modified__timestamp"
) )
table = InterventionTable( return qs
request=request,
queryset=interventions
)
context = {
"table": table,
TAB_TITLE_IDENTIFIER: _("Interventions - Overview"),
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required @login_required
@@ -88,11 +72,19 @@ def new_view(request: HttpRequest):
) )
) )
messages.success(request, _("Intervention {} added").format(intervention.identifier)) messages.success(request, _("Intervention {} added").format(intervention.identifier))
if geom_form.geometry_simplified: if geom_form.has_geometry_simplified():
messages.info( messages.info(
request, request,
GEOMETRY_SIMPLIFIED 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) return redirect("intervention:detail", id=intervention.id)
else: else:
messages.error(request, FORM_INVALID, extra_tags="danger",) messages.error(request, FORM_INVALID, extra_tags="danger",)
@@ -108,23 +100,9 @@ def new_view(request: HttpRequest):
return render(request, template, context) return render(request, template, context)
@login_required class InterventionIdentifierGeneratorView(LoginRequiredMixin, BaseIdentifierGeneratorView):
@default_group_required _MODEL_CLS = Intervention
def new_id_view(request: HttpRequest): _REDIRECT_URL_NAME = "intervention:index"
""" 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
}
)
@login_required @login_required
@@ -236,11 +214,19 @@ def edit_view(request: HttpRequest, id: str):
messages.success(request, _("Intervention {} edited").format(intervention.identifier)) messages.success(request, _("Intervention {} edited").format(intervention.identifier))
if intervention_is_checked: if intervention_is_checked:
messages.info(request, CHECK_STATE_RESET) messages.info(request, CHECK_STATE_RESET)
if geom_form.geometry_simplified: if geom_form.has_geometry_simplified():
messages.info( messages.info(
request, request,
GEOMETRY_SIMPLIFIED 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) return redirect("intervention:detail", id=intervention.id)
else: else:
messages.error(request, FORM_INVALID, extra_tags="danger",) 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 """ A geometry form for rendering geometry read-only using a widget
""" """
read_only = True read_only: bool = True
geometry_simplified = False _geometry_simplified: bool = False
output = JSONField( output = JSONField(
label=_("Geometry"), label=_("Geometry"),
help_text=_(""), help_text=_(""),
@@ -34,6 +34,7 @@ class SimpleGeomForm(BaseForm):
required=False, required=False,
disabled=False, disabled=False,
) )
_num_geometries_ignored: int = 0
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.read_only = kwargs.pop("read_only", True) self.read_only = kwargs.pop("read_only", True)
@@ -48,6 +49,7 @@ class SimpleGeomForm(BaseForm):
raise AttributeError raise AttributeError
geojson = self.instance.geometry.as_feature_collection(srid=DEFAULT_SRID_RLP) 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) geom = json.dumps(geojson)
except AttributeError: except AttributeError:
# If no geometry exists for this form, we simply set the value to None and zoom to the maximum level # 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 is_valid = True
# Get geojson from form # Get geojson from form
geom = self.data["output"] geom = self.data.get("output", None)
if geom is None or len(geom) == 0: if geom is None or len(geom) == 0:
# empty geometry is a valid geometry # empty geometry is a valid geometry
self.cleaned_data["output"] = MultiPolygon(srid=DEFAULT_SRID_RLP).ewkt self.cleaned_data["output"] = MultiPolygon(srid=DEFAULT_SRID_RLP).ewkt
@@ -100,7 +102,13 @@ class SimpleGeomForm(BaseForm):
is_valid &= False is_valid &= False
return is_valid 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) g = Polygon.from_ewkt(g.ewkt)
is_valid &= g.valid is_valid &= g.valid
if not g.valid: if not g.valid:
@@ -147,15 +155,6 @@ class SimpleGeomForm(BaseForm):
""" """
is_area_valid = geom.area > 1 # > 1m² (SRID:25832) 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 return is_area_valid
def __simplify_geometry(self, geom, max_vert: int): def __simplify_geometry(self, geom, max_vert: int):
@@ -208,13 +207,29 @@ class SimpleGeomForm(BaseForm):
if not is_vertices_num_valid: if not is_vertices_num_valid:
geometry.geom = self.__simplify_geometry(geometry.geom, max_vert=GEOM_MAX_VERTICES) geometry.geom = self.__simplify_geometry(geometry.geom, max_vert=GEOM_MAX_VERTICES)
geometry.save() geometry.save()
self.geometry_simplified = True self._geometry_simplified = True
# Start parcel update and geometry conflict checking procedure in a background process # Start parcel update and geometry conflict checking procedure in a background process
celery_update_parcels.delay(geometry.id) celery_update_parcels.delay(geometry.id)
celery_check_for_geometry_conflicts.delay(geometry.id) celery_check_for_geometry_conflicts.delay(geometry.id)
return geometry 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): def __flatten_geom_to_2D(self, geom):
""" """
Enforces a given OGRGeometry from higher dimensions into 2D Enforces a given OGRGeometry from higher dimensions into 2D
@@ -225,11 +240,12 @@ class SimpleGeomForm(BaseForm):
geom = gdal.OGRGeometry(g_wkt) geom = gdal.OGRGeometry(g_wkt)
return geom 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 """ Toggles the editable property of the geojson for proper handling in map client
Args: Args:
geojson (dict): The GeoJson geojson (dict): The GeoJson
title (str): An alternative title for the geometry
Returns: Returns:
geojson (dict): The altered GeoJson geojson (dict): The altered GeoJson

View File

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

View File

@@ -5,6 +5,9 @@ Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 17.09.21 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: def format_german_float(num) -> str:
@@ -19,3 +22,19 @@ 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

View File

@@ -83,6 +83,7 @@ EDITED_GENERAL_DATA = _("Edited general data")
# Geometry # Geometry
GEOMETRY_CONFLICT_WITH_TEMPLATE = _("Geometry conflict detected with {}") 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) 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
INTERVENTION_HAS_REVOCATIONS_TEMPLATE = _("This intervention has {} revocations") 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 "" 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-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" "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"
@@ -1298,13 +1298,13 @@ msgid "Compensation {} edited"
msgstr "Kompensation {} bearbeitet" msgstr "Kompensation {} bearbeitet"
#: compensation/views/compensation/compensation.py:196 #: 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 #: intervention/views/intervention.py:253
msgid "Edit {}" msgid "Edit {}"
msgstr "Bearbeite {}" msgstr "Bearbeite {}"
#: compensation/views/compensation/report.py:35 #: 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 #: intervention/views/report.py:35
msgid "Report {}" msgid "Report {}"
msgstr "Bericht {}" msgstr "Bericht {}"
@@ -1325,7 +1325,7 @@ msgstr "Ökokonto {} bearbeitet"
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:102 #: 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"
@@ -1361,11 +1361,11 @@ msgstr "EMAs - Übersicht"
msgid "EMA {} added" msgid "EMA {} added"
msgstr "EMA {} hinzugefügt" msgstr "EMA {} hinzugefügt"
#: ema/views/ema.py:217 #: ema/views/ema.py:223
msgid "EMA {} edited" msgid "EMA {} edited"
msgstr "EMA {} bearbeitet" msgstr "EMA {} bearbeitet"
#: ema/views/ema.py:256 #: ema/views/ema.py:262
msgid "EMA removed" msgid "EMA removed"
msgstr "EMA entfernt" msgstr "EMA entfernt"
@@ -1815,10 +1815,6 @@ 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."
#: 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 #: konova/forms/modals/document_form.py:37
msgid "When has this file been created? Important for photos." msgid "When has this file been created? Important for photos."
msgstr "Wann wurde diese Datei erstellt oder das Foto aufgenommen?" 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 " "Die Geometrie enthielt mehr als {} Eckpunkte. Sie musste vereinfacht werden "
"um die Obergrenze von {} erlaubten Eckpunkten einzuhalten." "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" msgid "This intervention has {} revocations"
msgstr "Dem Eingriff liegen {} Widersprüche vor" msgstr "Dem Eingriff liegen {} Widersprüche vor"
#: konova/utils/message_templates.py:91 #: konova/utils/message_templates.py:92
msgid "Checked on {} by {}" msgid "Checked on {} by {}"
msgstr "Am {} von {} geprüft worden" 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 {}" msgid "Data has changed since last check on {} by {}"
msgstr "" msgstr ""
"Daten wurden nach der letzten Prüfung geändert. Letzte Prüfung am {} durch {}" "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" msgid "Current data not checked yet"
msgstr "Momentane Daten noch nicht geprüft" 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." msgid "New token generated. Administrators need to validate."
msgstr "Neuer Token generiert. Administratoren sind informiert." msgstr "Neuer Token generiert. Administratoren sind informiert."
@@ -2313,14 +2318,6 @@ msgstr "Home"
msgid "Log" msgid "Log"
msgstr "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 #: konova/views/record.py:30
msgid "{} unrecorded" msgid "{} unrecorded"
msgstr "{} entzeichnet" msgstr "{} entzeichnet"
@@ -2377,17 +2374,19 @@ msgstr "Alle"
msgid "News" msgid "News"
msgstr "Neuigkeiten" msgstr "Neuigkeiten"
#: templates/400.html:7 #: templates/400.html:12
msgid "Request was invalid" msgid "Request was invalid"
msgstr "Anfrage fehlerhaft" msgstr "Anfrage fehlerhaft"
#: templates/400.html:10 #: templates/400.html:17
msgid "There seems to be a problem with the link you opened." 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." 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, ...)." 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 #: templates/404.html:7
msgid "Not found" msgid "Not found"
@@ -2401,11 +2400,11 @@ msgstr "Die angeforderten Daten existieren nicht."
msgid "Make sure the URL is valid (no whitespaces, ...)." msgid "Make sure the URL is valid (no whitespaces, ...)."
msgstr "Stellen Sie sicher, dass die URL gültig ist (keine Leerzeichen, ...)." msgstr "Stellen Sie sicher, dass die URL gültig ist (keine Leerzeichen, ...)."
#: templates/500.html:7 #: templates/500.html:12
msgid "Server Error" msgid "Server Error"
msgstr "" msgstr ""
#: templates/500.html:10 #: templates/500.html:17
msgid "Something happened. Admins have been informed. We are working on it!" msgid "Something happened. Admins have been informed. We are working on it!"
msgstr "" msgstr ""
"Irgendetwas ist passiert. Die Administratoren wurden informiert. Wir " "Irgendetwas ist passiert. Die Administratoren wurden informiert. Wir "

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: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> <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:index") self.cancel_redirect = reverse("user:detail")
# 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:index", args=()) self.index_url = reverse("user:detail", 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:index")) self.assertEqual(form.cancel_redirect, reverse("user:detail"))
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("", index_view, name="index"), path("", UserDetailView.as_view(), name="detail"),
path("propagate/", PropagateUserView.as_view(), name="propagate"), 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", 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>", contact_view, name="contact"), path("contact/<id>", ContactView.as_view(), name="contact"),
path("team/", index_team_view, name="team-index"), path("team/", TeamIndexView.as_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>", 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>/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,8 +1,10 @@
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
@@ -13,70 +15,58 @@ 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 any_group_check, login_required_modal from konova.decorators import login_required_modal
@login_required class UserDetailView(LoginRequiredMixin, BaseView):
@any_group_check _TAB_TITLE = _("User settings")
def index_view(request: HttpRequest): _TEMPLATE = "user/index.html"
""" Renders the user's data index view
Args: def get(self, request: HttpRequest):
request ():
Returns:
"""
template = "user/index.html"
context = { context = {
"user": request.user, "user": request.user,
TAB_TITLE_IDENTIFIER: _("User settings"), TAB_TITLE_IDENTIFIER: self._TAB_TITLE,
} }
context = BaseContext(request, context).context context = BaseContext(request, context).context
return render(request, template, context) return render(request, self._TEMPLATE, context)
@login_required class NotificationsView(LoginRequiredMixin, BaseView):
@any_group_check _TEMPLATE = "user/notifications.html"
def notifications_view(request: HttpRequest): _TAB_TITLE = _("User notifications")
""" Renders the notifications settings view
Args: def get(self, request: HttpRequest):
request ():
Returns:
"""
template = "user/notifications.html"
user = request.user 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)
form = UserNotificationForm(user=user, data=request.POST or None) def post(self, request: HttpRequest):
if request.method == "POST": 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:index") return redirect("user:detail")
elif request.method == "GET":
# Implicit
pass
else:
raise NotImplementedError
context = { context = {
"user": user, "user": user,
"form": form, "form": form,
TAB_TITLE_IDENTIFIER: _("User notifications"), TAB_TITLE_IDENTIFIER: self._TAB_TITLE,
} }
context = BaseContext(request, context).context context = BaseContext(request, context).context
return render(request, template, context) return render(request, self._TEMPLATE, context)
@login_required_modal class ContactView(LoginRequiredMixin, BaseModalFormView):
@login_required def get(self, request: HttpRequest, id: str):
def contact_view(request: HttpRequest, id: str):
""" Renders contact modal view of a users contact data """ Renders contact modal view of a users contact data
Args: Args:
@@ -88,21 +78,15 @@ def contact_view(request: HttpRequest, id: str):
""" """
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)
template = "modal/modal_form.html"
context = { context = {
"form": form, "form": form,
} }
context = BaseContext(request, context).context context = BaseContext(request, context).context
return render( return render(request, self._TEMPLATE, context)
request,
template,
context
)
@login_required_modal class TeamDetailModalView(LoginRequiredMixin, BaseModalFormView):
@login_required def get(self, request: HttpRequest, id: str):
def data_team_view(request: HttpRequest, id: str):
""" Renders team data """ Renders team data
Args: Args:
@@ -114,28 +98,25 @@ def data_team_view(request: HttpRequest, id: str):
""" """
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)
template = "modal/modal_form.html"
context = { context = {
"form": form, "form": form,
} }
context = BaseContext(request, context).context context = BaseContext(request, context).context
return render( return render(request, self._TEMPLATE, context)
request,
template,
context
)
@login_required class TeamIndexView(LoginRequiredMixin, BaseView):
def index_team_view(request: HttpRequest): _TEMPLATE = "user/team/index.html"
template = "user/team/index.html" _TAB_TITLE = _("Teams")
def get(self, request: HttpRequest):
user = request.user user = request.user
context = { context = {
"teams": user.shared_teams, "teams": user.shared_teams,
"tab_title": _("Teams"), "tab_title": self._TAB_TITLE,
} }
context = BaseContext(request, context).context context = BaseContext(request, context).context
return render(request, template, context) return render(request, self._TEMPLATE, context)
@login_required_modal @login_required_modal