# Refactoring team views

* refactors team views
* split views.py into users.py and teams.py in users app
* refactors method headers for _user_has_permission()
* adds method and class comments and documentation to base view classes
This commit is contained in:
mpeltriaux 2025-11-05 10:12:49 +01:00
parent bc2e901ca9
commit f122778232
33 changed files with 519 additions and 314 deletions

View File

@ -59,7 +59,7 @@ class NewCompensationFormView(BaseNewSpatialLocatedObjectFormView):
intervention = get_object_or_404(Intervention, id=intervention_id)
return intervention.is_shared_with(user)
def _user_has_permission(self, user):
def _user_has_permission(self, user, **kwargs):
# User has to be an ets user
return user.is_default_user()
@ -88,7 +88,7 @@ class EditCompensationFormView(BaseEditSpatialLocatedObjectFormView):
_TEMPLATE = "compensation/form/view.html"
_REDIRECT_URL = "compensation:detail"
def _user_has_permission(self, user):
def _user_has_permission(self, user, **kwargs):
# User has to be a default user
return user.is_default_user()
@ -170,5 +170,5 @@ class RemoveCompensationView(LoginRequiredMixin, BaseRemoveModalFormView):
_FORM_CLS = RemoveModalForm
_REDIRECT_URL = "compensation:index"
def _user_has_permission(self, user):
def _user_has_permission(self, user, **kwargs):
return user.is_default_user()

View File

@ -49,7 +49,7 @@ class NewEcoAccountFormView(BaseNewSpatialLocatedObjectFormView):
_TAB_TITLE = _("New Eco-Account")
_REDIRECT_URL = "compensation:acc:detail"
def _user_has_permission(self, user):
def _user_has_permission(self, user, **kwargs):
# User has to be a default user
return user.is_default_user()
@ -60,7 +60,7 @@ class EditEcoAccountFormView(BaseEditSpatialLocatedObjectFormView):
_TEMPLATE = "compensation/form/view.html"
_REDIRECT_URL = "compensation:acc:detail"
def _user_has_permission(self, user):
def _user_has_permission(self, user, **kwargs):
# User has to be a default user
return user.is_default_user()
@ -260,5 +260,5 @@ class RemoveEcoAccountView(LoginRequiredMixin, BaseRemoveModalFormView):
_FORM_CLS = RemoveEcoAccountModalForm
_REDIRECT_URL = "compensation:acc:index"
def _user_has_permission(self, user):
def _user_has_permission(self, user, **kwargs):
return user.is_default_user()

View File

@ -24,7 +24,7 @@ class BasePaymentView(LoginRequiredMixin, BaseModalFormView):
url = super()._get_redirect_url(*args, **kwargs)
return f"{url}#related_data"
def _user_has_permission(self, user):
def _user_has_permission(self, user, **kwargs):
return user.is_default_user()

View File

@ -16,14 +16,14 @@ class NewEmaActionView(AbstractNewCompensationActionView):
_MODEL_CLS = Ema
_REDIRECT_URL = _EMA_ACCOUNT_DETAIL_URL_NAME
def _user_has_permission(self, user):
def _user_has_permission(self, user, **kwargs):
return user.is_ets_user()
class EditEmaActionView(AbstractEditCompensationActionView):
_MODEL_CLS = Ema
_REDIRECT_URL = _EMA_ACCOUNT_DETAIL_URL_NAME
def _user_has_permission(self, user):
def _user_has_permission(self, user, **kwargs):
return user.is_ets_user()
@ -31,5 +31,5 @@ class RemoveEmaActionView(AbstractRemoveCompensationActionView):
_MODEL_CLS = Ema
_REDIRECT_URL = _EMA_ACCOUNT_DETAIL_URL_NAME
def _user_has_permission(self, user):
def _user_has_permission(self, user, **kwargs):
return user.is_ets_user()

View File

@ -14,7 +14,7 @@ class NewEmaDeadlineView(AbstractNewDeadlineView):
_MODEL_CLS = Ema
_REDIRECT_URL = _EMA_DETAIL_URL_NAME
def _user_has_permission(self, user):
def _user_has_permission(self, user, **kwargs):
return user.is_ets_user()
@ -22,7 +22,7 @@ class EditEmaDeadlineView(AbstractEditDeadlineView):
_MODEL_CLS = Ema
_REDIRECT_URL = _EMA_DETAIL_URL_NAME
def _user_has_permission(self, user):
def _user_has_permission(self, user, **kwargs):
return user.is_ets_user()
@ -30,5 +30,5 @@ class RemoveEmaDeadlineView(AbstractRemoveDeadlineView):
_MODEL_CLS = Ema
_REDIRECT_URL = _EMA_DETAIL_URL_NAME
def _user_has_permission(self, user):
def _user_has_permission(self, user, **kwargs):
return user.is_ets_user()

View File

@ -16,14 +16,14 @@ class NewEmaDocumentView(AbstractNewDocumentView):
_FORM_CLS = NewEmaDocumentModalForm
_REDIRECT_URL = "ema:detail"
def _user_has_permission(self, user):
def _user_has_permission(self, user, **kwargs):
return user.is_ets_user()
class GetEmaDocumentView(AbstractGetDocumentView):
_MODEL_CLS = Ema
_DOCUMENT_CLS = EmaDocument
def _user_has_permission(self, user):
def _user_has_permission(self, user, **kwargs):
return user.is_ets_user()
class RemoveEmaDocumentView(AbstractRemoveDocumentView):
@ -32,7 +32,7 @@ class RemoveEmaDocumentView(AbstractRemoveDocumentView):
_FORM_CLS = RemoveEmaDocumentModalForm
_REDIRECT_URL = "ema:detail"
def _user_has_permission(self, user):
def _user_has_permission(self, user, **kwargs):
return user.is_ets_user()
class EditEmaDocumentView(AbstractEditDocumentView):
@ -41,5 +41,5 @@ class EditEmaDocumentView(AbstractEditDocumentView):
_DOCUMENT_CLS = EmaDocument
_REDIRECT_URL = "ema:detail"
def _user_has_permission(self, user):
def _user_has_permission(self, user, **kwargs):
return user.is_ets_user()

View File

@ -38,7 +38,7 @@ class NewEmaFormView(BaseNewSpatialLocatedObjectFormView):
_TAB_TITLE = _("New EMA")
_REDIRECT_URL = "ema:detail"
def _user_has_permission(self, user):
def _user_has_permission(self, user, **kwargs):
# User has to be an ets user
return user.is_ets_user()
@ -50,7 +50,7 @@ class EditEmaFormView(BaseEditSpatialLocatedObjectFormView):
_REDIRECT_URL = "ema:detail"
_TAB_TITLE = _("Edit {}")
def _user_has_permission(self, user):
def _user_has_permission(self, user, **kwargs):
# User has to be an ets user
return user.is_ets_user()
@ -59,7 +59,7 @@ class EmaIdentifierGeneratorView(LoginRequiredMixin, BaseIdentifierGeneratorView
_MODEL_CLS = Ema
_REDIRECT_URL = "ema:index"
def _user_has_permission(self, user):
def _user_has_permission(self, user, **kwargs):
return user.is_ets_user()
@ -112,5 +112,5 @@ class RemoveEmaView(LoginRequiredMixin, BaseRemoveModalFormView):
_MODEL_CLS = Ema
_REDIRECT_URL = "ema:index"
def _user_has_permission(self, user):
def _user_has_permission(self, user, **kwargs):
return user.is_ets_user()

View File

@ -14,5 +14,5 @@ from konova.views.log import AbstractLogView
class EmaLogView(LoginRequiredMixin, AbstractLogView):
_MODEL_CLS = Ema
def _user_has_permission(self, user):
def _user_has_permission(self, user, **kwargs):
return user.is_ets_user()

View File

@ -16,5 +16,5 @@ class EmaResubmissionView(AbstractResubmissionView):
_REDIRECT_URL = "ema:detail"
action_url = "ema:resubmission-create"
def _user_has_permission(self, user):
def _user_has_permission(self, user, **kwargs):
return user.is_ets_user()

View File

@ -17,5 +17,5 @@ class EmaShareFormView(AbstractShareFormView):
_MODEL_CLS = Ema
_REDIRECT_URL = "ema:detail"
def _user_has_permission(self, user):
def _user_has_permission(self, user, **kwargs):
return user.is_ets_user()

View File

@ -14,7 +14,7 @@ class NewEmaStateView(AbstractNewCompensationStateView):
_MODEL_CLS = Ema
_REDIRECT_URL = "ema:detail"
def _user_has_permission(self, user):
def _user_has_permission(self, user, **kwargs):
return user.is_ets_user()
@ -22,7 +22,7 @@ class EditEmaStateView(AbstractEditCompensationStateView):
_MODEL_CLS = Ema
_REDIRECT_URL = "ema:detail"
def _user_has_permission(self, user):
def _user_has_permission(self, user, **kwargs):
return user.is_ets_user()
@ -30,5 +30,5 @@ class RemoveEmaStateView(AbstractRemoveCompensationStateView):
_MODEL_CLS = Ema
_REDIRECT_URL = "ema:detail"
def _user_has_permission(self, user):
def _user_has_permission(self, user, **kwargs):
return user.is_ets_user()

View File

@ -19,7 +19,7 @@ class InterventionCheckView(LoginRequiredMixin, BaseModalFormView):
_MSG_SUCCESS = _("Check performed")
_REDIRECT_URL = "intervention:detail"
def _user_has_permission(self, user):
def _user_has_permission(self, user, **kwargs):
return user.is_zb_user()
def _get_redirect_url(self, *args, **kwargs):

View File

@ -25,7 +25,7 @@ class BaseRevocationView(LoginRequiredMixin, BaseModalFormView):
class Meta:
abstract = True
def _user_has_permission(self, user):
def _user_has_permission(self, user, **kwargs):
return user.is_default_user()
def _get_redirect_url(self, *args, **kwargs):
@ -63,7 +63,7 @@ class GetRevocationDocumentView(LoginRequiredMixin, BaseView):
return redirect("intervention:detail", id=doc.instance.id)
return get_document(doc)
def _user_has_permission(self, user):
def _user_has_permission(self, user, **kwargs):
return user.is_default_user()
def _user_has_shared_access(self, user, **kwargs):

View File

@ -20,6 +20,12 @@ ENTRY_REMOVE_MISSING_PERMISSION = _("Only conservation or registration office us
MISSING_GROUP_PERMISSION = _("You need to be part of another user group.")
CHECK_STATE_RESET = _("Status of Checked reset")
# USER | TEAM
TEAM_ADDED = _("New team added")
TEAM_EDITED = _("Team edited")
TEAM_REMOVED = _("Team removed")
TEAM_LEFT = _("Left Team")
# REMOVED
GENERIC_REMOVED_TEMPLATE = _("{} removed")

View File

@ -21,7 +21,7 @@ class AbstractCompensationActionView(LoginRequiredMixin, BaseModalFormView):
class Meta:
abstract = True
def _user_has_permission(self, user):
def _user_has_permission(self, user, **kwargs):
return user.is_default_user()
def _get_redirect_url(self, *args, **kwargs):

View File

@ -8,6 +8,7 @@ from abc import abstractmethod
from bootstrap_modal_forms.mixins import is_ajax
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import ObjectDoesNotExist
from django.http import HttpRequest, JsonResponse, HttpResponseRedirect
from django.shortcuts import render, redirect, get_object_or_404
from django.urls import reverse
@ -24,18 +25,37 @@ from konova.utils.message_templates import MISSING_GROUP_PERMISSION, DATA_UNSHAR
class BaseView(View):
_TEMPLATE: str = "CHANGE_ME"
_TAB_TITLE: str = "CHANGE_ME"
_REDIRECT_URL: str = "CHANGE_ME"
_REDIRECT_URL_ERROR: str = "home"
""" An abstract base view
This class represents the root of all views on this project. It defines private variables which have to be used
by inheriting classes for proper generic inheriting.
"""
_TEMPLATE: str = "CHANGE_ME" # Path to template file
_TAB_TITLE: str = "CHANGE_ME" # Title displayed on browser tab
_REDIRECT_URL: str = "CHANGE_ME" # Default URL to redirect after processing (notation as django url "namespace:endpoint")
_REDIRECT_URL_ERROR: str = "home" # Default URL to redirect in case of an error (same notation)
class Meta:
abstract = True
def dispatch(self, request, *args, **kwargs):
""" Dispatching requests before forwarding them into GET or POST endpoints.
Defines basic checks which need to be done before a user can get access to any view inheriting from
this class.
Args:
request (HttpRequest): The incoming request
*args ():
**kwargs ():
Returns:
"""
request = check_user_is_in_any_group(request)
if not self._user_has_permission(request.user):
if not self._user_has_permission(request.user, **kwargs):
messages.info(request, MISSING_GROUP_PERMISSION)
return redirect(reverse(self._REDIRECT_URL_ERROR))
@ -46,37 +66,68 @@ class BaseView(View):
return super().dispatch(request, *args, **kwargs)
@abstractmethod
def _user_has_permission(self, user):
""" Has to be implemented properly by inheriting classes
def _user_has_permission(self, user, **kwargs):
""" Checks whether the user has permission to get this view rendered.
If no specific check is needed, this method can be overwritten with a simple True returning.
Args:
user ():
user (User): The performing user
**kwargs ():
Returns:
has_permission (bool): Whether the user has permission to see this view
"""
raise NotImplementedError("User permission not checked!")
@abstractmethod
def _user_has_shared_access(self, user, **kwargs):
""" Has to be implemented properly by inheriting classes
""" Checks whether the user has shared access to this object.
If no shared-access-check is needed, this method can be overwritten with a simple True returning.
Args:
user ():
user (User): The performing user
**kwargs ():
Returns:
has_shared_access (bool): Whether the user has shared access
"""
raise NotImplementedError("Shared access not checked!")
def _get_redirect_url(self, *args, **kwargs):
""" Getter to construct a more specific, data dependant redirect URL
By default the method simply returns the pre-defined redirect URL.
Args:
*args ():
**kwargs ():
Returns:
url (str): Reversed redirect url
"""
return self._REDIRECT_URL
def _get_redirect_url_error(self, *args, **kwargs):
""" Getter to construct a more specific, data dependant redirect URL in error cases
By default the method simply returns the pre-defined redirect URL for errors.
Args:
*args ():
**kwargs ():
Returns:
url (str): Reversed redirect url
"""
return self._REDIRECT_URL_ERROR
class BaseModalFormView(BaseView):
_TEMPLATE = "modal/modal_form.html"
""" Abstract base view providing logic to perform most modal form based view renderings
"""
_TEMPLATE: str = "modal/modal_form.html"
_MODEL_CLS = None
_FORM_CLS = None
_MSG_SUCCESS = None
@ -85,12 +136,45 @@ class BaseModalFormView(BaseView):
abstract = True
def _user_has_shared_access(self, user, **kwargs):
""" Checks whether the user has shared access to this object.
For objects inheriting from BaseObject class the method 'is_shared_with()' is a handy
wrapper for checking shared access. For any other circumstances this method should be overwritten
to provide custom shared-access-checking logic.
If no shared-access-check is needed, this method can be overwritten with a simple True returning.
Args:
user (User): The performing user
**kwargs ():
Returns:
has_shared_access (bool): Whether the user has shared access
"""
obj = get_object_or_404(self._MODEL_CLS, id=kwargs.get("id"))
return obj.is_shared_with(user)
def get(self, request: HttpRequest, id: str, *args, **kwargs):
obj = self._MODEL_CLS.objects.get(id=id)
self._check_for_recorded_instance(obj)
def get(self, request: HttpRequest, *args, **kwargs):
""" GET endpoint for rendering a view holding a modal form
Args:
request (HttpRequest): The incoming request
*args ():
**kwargs ():
Returns:
"""
# If there is an id provided as mapped parameter from the URL take it ...
_id = kwargs.pop("id", None)
try:
# ... and try to resolve it into a record
obj = self._MODEL_CLS.objects.get(id=_id)
self._check_for_recorded_instance(obj)
except ObjectDoesNotExist:
# ... If there is none, maybe we are currently processing
# the creation of a new object (therefore no id yet), so let's continue
obj = None
form = self._FORM_CLS(
request.POST or None,
request.FILES or None,
@ -104,9 +188,27 @@ class BaseModalFormView(BaseView):
context = BaseContext(request, context).context
return render(request, self._TEMPLATE, context)
def post(self, request: HttpRequest, id: str, *args, **kwargs):
obj = self._MODEL_CLS.objects.get(id=id)
self._check_for_recorded_instance(obj)
def post(self, request: HttpRequest, *args, **kwargs):
""" POST endpoint for processing form contents of a view
Args:
request (HttpRequest): The incoming request
*args ():
**kwargs ():
Returns:
"""
# If there is an id provided as mapped parameter from the URL take it ...
_id = kwargs.pop("id", None)
try:
# ... and try to resolve it into a record
obj = self._MODEL_CLS.objects.get(id=_id)
self._check_for_recorded_instance(obj)
except ObjectDoesNotExist:
# ... If there is none, maybe we are currently processing
# the creation of a new object (therefore no id yet), so let's continue
obj = None
form = self._FORM_CLS(
request.POST or None,
request.FILES or None,
@ -114,13 +216,15 @@ class BaseModalFormView(BaseView):
request=request,
**kwargs
)
# Get now the redirect url and take specifics of the obj into account for that.
# We do not do this after saving the form to avoid side effects due to possibly changed data
redirect_url = self._get_redirect_url(obj=obj)
if form.is_valid():
# Modal forms send one POST for checking on data validity. This is used to evaluate possible errors
# on the form. The second POST (if no errors have been found) is the 'proper' one,
# which we want to process by saving/commiting of the data to the database.
if not is_ajax(request.META):
# Modal forms send one POST for checking on data validity. This can be used to return possible errors
# on the form. A second POST (if no errors occurs) is sent afterward and needs to process the
# saving/commiting of the data to the database. is_ajax() performs this check. The first request is
# an ajax call, the second is a regular form POST.
# Get now the success message and take specifics of the obj into account for that
msg_success = self._get_msg_success(obj=obj, *args, **kwargs)
form.save()
messages.success(
@ -136,20 +240,41 @@ class BaseModalFormView(BaseView):
return render(request, self._TEMPLATE, context)
def _get_redirect_url(self, *args, **kwargs):
""" Getter to construct a more specific, data dependant redirect URL (if needed)
Args:
*args ():
**kwargs ():
Returns:
url (str): Reversed redirect url
"""
obj = kwargs.get("obj", None)
assert obj is not None
return reverse(self._REDIRECT_URL, args=(obj.id,))
if obj:
return reverse(self._REDIRECT_URL, args=(obj.id,))
else:
return reverse(self._REDIRECT_URL)
def _get_msg_success(self, *args, **kwargs):
""" Getter to construct a more specific, data dependant success message
Args:
*args ():
**kwargs ():
Returns:
"""
return self._MSG_SUCCESS
def _check_for_recorded_instance(self, obj):
""" Checks if the object on this view is recorded and runs some special logic if yes
""" Checks if the object on this view is recorded and runs some special logic if so
If the instance is recorded, the view should provide some information about why the user can not edit anything.
This behaviour is only intended to mask any form for instances based on the BaseObject class.
There are situations where the form should be rendered regularly,
e.g deduction forms for (recorded) eco accounts.
There are situations where the form should be rendered regularly, despite the instance being recorded,
e.g. for rendering deduction form contents on (recorded) eco accounts.
Returns:
@ -162,29 +287,32 @@ class BaseModalFormView(BaseView):
return
if obj.is_recorded:
self._block_form()
# Replace default template with a blocking one
self._TEMPLATE = "form/recorded_no_edit.html"
def _block_form(self):
"""
Overwrites template, providing no actions
Returns:
"""
self._TEMPLATE = "form/recorded_no_edit.html"
class BaseIndexView(BaseView):
""" Base class for index views
""" Abstract base class for index views
"""
_TEMPLATE = "generic_index.html"
_TEMPLATE: str = "generic_index.html"
_INDEX_TABLE_CLS = None
_REDIRECT_URL = "home"
_REDIRECT_URL: str = "home"
class Meta:
abstract = True
def get(self, request: HttpRequest):
def get(self, request: HttpRequest, *args, **kwargs):
""" GET endpoint for rendering index views
Args:
request (HttpRequest): The incoming request
*args ():
**kwargs ():
Returns:
"""
qs = self._get_queryset()
table = self._INDEX_TABLE_CLS(
request=request,
@ -199,9 +327,14 @@ class BaseIndexView(BaseView):
@abstractmethod
def _get_queryset(self):
""" Generic getter for the queryset of objects which shall be processed on this view
Returns:
"""
raise NotImplementedError
def _user_has_permission(self, user):
def _user_has_permission(self, user, **kwargs):
# No specific permissions needed for opening base index view
return True
@ -228,7 +361,7 @@ class BaseIdentifierGeneratorView(BaseView):
}
)
def _user_has_permission(self, user):
def _user_has_permission(self, user, **kwargs):
""" Should be overwritten in inheriting classes!
Args:
@ -245,6 +378,9 @@ class BaseIdentifierGeneratorView(BaseView):
class BaseFormView(BaseView):
""" Abstract base class for rendering form views
"""
_MODEL_CLS = None
_FORM_CLS = None
@ -252,18 +388,21 @@ class BaseFormView(BaseView):
abstract = True
def _get_additional_context(self, **kwargs):
"""
""" Getter for additional data, which is needed to properly render the current view
Args:
**kwargs ():
Returns:
context (dict): Additional context data for rendering
"""
return {}
class BaseSpatialLocatedObjectFormView(LoginRequiredMixin, BaseFormView):
""" Abstract base view for processing objects with spatial data
"""
_GEOMETRY_FORM_CLS = SimpleGeomForm
class Meta:
@ -271,8 +410,11 @@ class BaseSpatialLocatedObjectFormView(LoginRequiredMixin, BaseFormView):
class BaseNewSpatialLocatedObjectFormView(BaseSpatialLocatedObjectFormView):
""" Base view for creating new spatial data related to objects
def _user_has_permission(self, user):
"""
def _user_has_permission(self, user, **kwargs):
# User has to have default privilege to call this endpoint
return user.is_default_user()
@ -280,10 +422,21 @@ class BaseNewSpatialLocatedObjectFormView(BaseSpatialLocatedObjectFormView):
# There is no shared access control since nothing exists yet
return True
def get(self, request: HttpRequest, **kwargs):
def get(self, request: HttpRequest, *args, **kwargs):
""" GET endpoint for rendering a form view where object data and spatial data are processed
Args:
request (HttpRequest): The incoming request
**kwargs ():
Returns:
"""
# First initialize the regular object form and the geometry form based on request-bound data
form: BaseForm = self._FORM_CLS(None, **kwargs, user=request.user)
geom_form: SimpleGeomForm = self._GEOMETRY_FORM_CLS(None, user=request.user, read_only=False)
# Get some additional context and put everything into the rendering pipeline
context = self._get_additional_context()
context = BaseContext(request, additional_context=context).context
context.update(
@ -295,16 +448,30 @@ class BaseNewSpatialLocatedObjectFormView(BaseSpatialLocatedObjectFormView):
)
return render(request, self._TEMPLATE, context)
def post(self, request: HttpRequest, **kwargs):
def post(self, request: HttpRequest, *args, **kwargs):
""" POST endpoint for processing object and spatial data provided by forms
Args:
request (HttpRequest): The incoming request
**kwargs ():
Returns:
"""
# First initialize the regular object form and the geometry form based on request-bound data
form: BaseForm = self._FORM_CLS(request.POST or None, **kwargs, user=request.user)
geom_form: SimpleGeomForm = self._GEOMETRY_FORM_CLS(request.POST or None, user=request.user, read_only=False)
# Only continue if both forms are without errors
if form.is_valid() and geom_form.is_valid():
obj = form.save(request.user, geom_form)
obj_redirect_url = reverse(self._REDIRECT_URL, args=(obj.id,))
generated_identifier = form.cleaned_data.get("identifier", None)
# There is a rare chance that an identifier has been taken already between sending the form and processing
# the data. If the identifier can not be used anymore, we have to inform the user that another identifier
# had to be generated
if generated_identifier != obj.identifier:
messages.info(
request,
@ -314,12 +481,18 @@ class BaseNewSpatialLocatedObjectFormView(BaseSpatialLocatedObjectFormView):
)
)
messages.success(request, _("{} added").format(obj.identifier))
# Very complex geometries have to be simplified automatically while processing the spatial data. If this
# is the case, the user has to be informed. (They might want to check whether the stored geometry still
# fits their needs)
if geom_form.has_geometry_simplified():
messages.info(
request,
GEOMETRY_SIMPLIFIED
)
# If certain parts of the geometry do not pass the quality check (e.g. way too small and therefore more like
# cutting errors) we need to inform the user that some parts have been removed/ignored while storing the
# geometry
num_ignored_geometries = geom_form.get_num_geometries_ignored()
if num_ignored_geometries > 0:
messages.info(
@ -329,6 +502,7 @@ class BaseNewSpatialLocatedObjectFormView(BaseSpatialLocatedObjectFormView):
return redirect(obj_redirect_url)
else:
# Something was not properly entered on the forms, so we have to inform the user
context = self._get_additional_context()
messages.error(request, FORM_INVALID, extra_tags="danger",)
@ -344,15 +518,30 @@ class BaseNewSpatialLocatedObjectFormView(BaseSpatialLocatedObjectFormView):
class BaseEditSpatialLocatedObjectFormView(BaseSpatialLocatedObjectFormView):
""" Base view for editing new spatial data related to objects
"""
_TAB_TITLE = _("Edit {}")
def get(self, request: HttpRequest, id: str):
def get(self, request: HttpRequest, id: str, *args, **kwargs):
""" GET endpoint for rendering a form view where object data and spatial data are processed
Args:
request (HttpRequest): The incoming request
id (str): The id of the object (not the geometry)
Returns:
"""
# First fetch the object identified by the id
obj = get_object_or_404(
self._MODEL_CLS,
id=id
)
obj_redirect_url = reverse(self._REDIRECT_URL, args=(obj.id,))
# Check whether the object is recorded. If so - we can redirect the user and inform about the un-editability
# of this entry
if obj.is_recorded:
messages.info(
request,
@ -360,9 +549,11 @@ class BaseEditSpatialLocatedObjectFormView(BaseSpatialLocatedObjectFormView):
)
return redirect(obj_redirect_url)
# Seems like the object is not recorded. Good - initialize the forms based on the obj and request-bound data
form: BaseForm = self._FORM_CLS(None, instance=obj, user=request.user)
geom_form: SimpleGeomForm = self._GEOMETRY_FORM_CLS(None, instance=obj, read_only=False)
# Get additional context for rendering and put everything in the rendering pipeline
context = self._get_additional_context()
context = BaseContext(request, additional_context=context).context
context.update(
@ -374,13 +565,33 @@ class BaseEditSpatialLocatedObjectFormView(BaseSpatialLocatedObjectFormView):
)
return render(request, self._TEMPLATE, context)
def post(self, request: HttpRequest, id: str):
def post(self, request: HttpRequest, id: str, *args, **kwargs):
""" POST endpoint for processing object and spatial data provided by forms
Args:
request (HttpRequest): The incoming request
id (str): The object's id
*args ():
**kwargs ():
Returns:
"""
obj = get_object_or_404(
self._MODEL_CLS,
id=id
)
obj_redirect_url = reverse(self._REDIRECT_URL, args=(obj.id,))
# If the object is recorded, we abort the processing directly and inform the user
if obj.is_recorded:
messages.info(
request,
RECORDED_BLOCKS_EDIT
)
return redirect(obj_redirect_url)
# Initialize forms with obj and request-bound data
form: BaseForm = self._FORM_CLS(request.POST or None, instance=obj, user=request.user)
geom_form: SimpleGeomForm = self._GEOMETRY_FORM_CLS(request.POST or None, instance=obj, read_only=False)
@ -388,12 +599,18 @@ class BaseEditSpatialLocatedObjectFormView(BaseSpatialLocatedObjectFormView):
obj = form.save(request.user, geom_form)
messages.success(request, _("{} edited").format(obj.identifier))
# Very complex geometries have to be simplified automatically while processing the spatial data. If this
# is the case, the user has to be informed. (They might want to check whether the stored geometry still
# fits their needs)
if geom_form.has_geometry_simplified():
messages.info(
request,
GEOMETRY_SIMPLIFIED
)
# If certain parts of the geometry do not pass the quality check (e.g. way too small and therefore more like
# cutting errors) we need to inform the user that some parts have been removed/ignored while storing the
# geometry
num_ignored_geometries = geom_form.get_num_geometries_ignored()
if num_ignored_geometries > 0:
messages.info(
@ -420,5 +637,5 @@ class BaseEditSpatialLocatedObjectFormView(BaseSpatialLocatedObjectFormView):
obj = get_object_or_404(self._MODEL_CLS, id=kwargs.get('id', None))
return obj.is_shared_with(user)
def _user_has_permission(self, user):
def _user_has_permission(self, user, **kwargs):
return user.is_default_user()

View File

@ -25,7 +25,7 @@ class AbstractNewDeadlineView(LoginRequiredMixin, BaseModalFormView):
def _get_redirect_url(self, *args, **kwargs):
return super()._get_redirect_url(*args, **kwargs) + "#related_data"
def _user_has_permission(self, user):
def _user_has_permission(self, user, **kwargs):
return user.is_default_user()
@ -41,7 +41,7 @@ class AbstractEditDeadlineView(LoginRequiredMixin, BaseModalFormView):
def _get_redirect_url(self, *args, **kwargs):
return super()._get_redirect_url(*args, **kwargs) + "#related_data"
def _user_has_permission(self, user):
def _user_has_permission(self, user, **kwargs):
return user.is_default_user()
@ -57,5 +57,5 @@ class AbstractRemoveDeadlineView(LoginRequiredMixin, BaseModalFormView):
def _get_redirect_url(self, *args, **kwargs):
return super()._get_redirect_url(*args, **kwargs) + "#related_data"
def _user_has_permission(self, user):
def _user_has_permission(self, user, **kwargs):
return user.is_default_user()

View File

@ -28,7 +28,7 @@ class AbstractDeductionView(BaseModalFormView):
"""
pass
def _user_has_permission(self, user) -> bool:
def _user_has_permission(self, user, **kwargs) -> bool:
"""
Args:

View File

@ -42,7 +42,7 @@ class BaseDetailView(LoginRequiredMixin, BaseView):
# Access to an entry's detail view is not restricted by the state of being-shared or not
return True
def _user_has_permission(self, user):
def _user_has_permission(self, user, **kwargs):
# Detail views have no restrictions
return True

View File

@ -27,7 +27,7 @@ class AbstractNewDocumentView(LoginRequiredMixin, BaseModalFormView):
def _get_redirect_url(self, *args, **kwargs):
return super()._get_redirect_url(*args, **kwargs) + "#related_data"
def _user_has_permission(self, user):
def _user_has_permission(self, user, **kwargs):
return user.is_default_user()
@ -58,7 +58,7 @@ class AbstractGetDocumentView(LoginRequiredMixin, BaseView):
def post(self, request, id: str, doc_id: str):
return self.get(request, id, doc_id)
def _user_has_permission(self, user):
def _user_has_permission(self, user, **kwargs):
return user.is_default_user()
def _user_has_shared_access(self, user, **kwargs):
@ -80,7 +80,7 @@ class AbstractRemoveDocumentView(LoginRequiredMixin, BaseModalFormView):
def _get_redirect_url(self, *args, **kwargs):
return super()._get_redirect_url(*args, **kwargs) + "#related_data"
def _user_has_permission(self, user):
def _user_has_permission(self, user, **kwargs):
return user.is_default_user()
def _get_msg_success(self, *args, **kwargs):
@ -100,7 +100,7 @@ class AbstractEditDocumentView(LoginRequiredMixin, BaseModalFormView):
class Meta:
abstract = True
def _user_has_permission(self, user):
def _user_has_permission(self, user, **kwargs):
return user.is_default_user()
def _get_redirect_url(self, *args, **kwargs):

View File

@ -110,7 +110,7 @@ class GeomParcelsView(BaseView):
def _user_has_shared_access(self, user, **kwargs):
return True
def _user_has_permission(self, user):
def _user_has_permission(self, user, **kwargs):
return True
@ -160,5 +160,5 @@ class GeomParcelsContentView(BaseView):
def _user_has_shared_access(self, user, **kwargs):
return True
def _user_has_permission(self, user):
def _user_has_permission(self, user, **kwargs):
return True

View File

@ -74,7 +74,7 @@ class HomeView(LoginRequiredMixin, BaseView):
context = BaseContext(request, additional_context).context
return render(request, self._TEMPLATE, context)
def _user_has_permission(self, user):
def _user_has_permission(self, user, **kwargs):
# No specific permission needed for home view
return True

View File

@ -46,5 +46,5 @@ class AbstractLogView(BaseView):
obj = get_object_or_404(self._MODEL_CLS, id=obj_id)
return obj.is_shared_with(user)
def _user_has_permission(self, user):
def _user_has_permission(self, user, **kwargs):
return user.is_default_user()

View File

@ -15,7 +15,7 @@ class AbstractRecordView(BaseModalFormView):
_FORM_CLS = RecordModalForm
_MSG_SUCCESS = None
def _user_has_permission(self, user):
def _user_has_permission(self, user, **kwargs):
return user.is_ets_user()
def _get_msg_success(self, *args, **kwargs):

View File

@ -16,7 +16,7 @@ class BaseRemoveModalFormView(BaseModalFormView):
_MSG_SUCCESS = GENERIC_REMOVED_TEMPLATE
_REDIRECT_URL = None
def _user_has_permission(self, user):
def _user_has_permission(self, user, **kwargs):
return user.is_default_user()
def _get_redirect_url(self, *args, **kwargs):

View File

@ -97,7 +97,7 @@ class BaseReportView(BaseView):
"""
raise NotImplementedError
def _user_has_permission(self, user):
def _user_has_permission(self, user, **kwargs):
# Reports do not need specific permissions to be callable
return True

View File

@ -20,7 +20,7 @@ class AbstractResubmissionView(LoginRequiredMixin, BaseModalFormView):
class Meta:
abstract = True
def _user_has_permission(self, user):
def _user_has_permission(self, user, **kwargs):
return user.is_default_user()
def _check_for_recorded_instance(self, obj):

View File

@ -60,7 +60,7 @@ class AbstractShareByTokenView(LoginRequiredMixin, BaseView):
)
return redirect("home")
def _user_has_permission(self, user):
def _user_has_permission(self, user, **kwargs):
# No permissions are needed to get shared access via token
return True
@ -77,5 +77,5 @@ class AbstractShareFormView(LoginRequiredMixin, BaseModalFormView):
class Meta:
abstract = True
def _user_has_permission(self, user):
def _user_has_permission(self, user, **kwargs):
return user.is_default_user()

View File

@ -23,7 +23,7 @@ class AbstractCompensationStateView(LoginRequiredMixin, BaseModalFormView):
class Meta:
abstract = True
def _user_has_permission(self, user):
def _user_has_permission(self, user, **kwargs):
return user.is_default_user()
def _get_redirect_url(self, *args, **kwargs):

View File

@ -11,7 +11,9 @@ from user.autocomplete.share import ShareUserAutocomplete, ShareTeamAutocomplete
from user.autocomplete.team import TeamAdminAutocomplete
from user.views.api_token import APITokenView, new_api_token_view
from user.views.propagate import PropagateUserView
from user.views.views import *
from user.views.teams import TeamIndexView, NewTeamView, TeamDetailModalView, EditTeamView, RemoveTeamView, \
LeaveTeamView
from user.views.users import UserDetailView, NotificationsView, ContactView
app_name = "user"
urlpatterns = [
@ -22,11 +24,11 @@ urlpatterns = [
path("token/api/new", new_api_token_view, name="api-token-new"),
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/new", NewTeamView.as_view(), name="team-new"),
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"),
path("team/<id>/edit", EditTeamView.as_view(), name="team-edit"),
path("team/<id>/remove", RemoveTeamView.as_view(), name="team-remove"),
path("team/<id>/leave", LeaveTeamView.as_view(), name="team-leave"),
# Autocomplete urls
path("atcmplt/share/u", ShareUserAutocomplete.as_view(), name="share-user-autocomplete"),

105
user/views/teams.py Normal file
View File

@ -0,0 +1,105 @@
"""
Author: Michel Peltriaux
Created on: 05.11.25
"""
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import Http404, HttpRequest
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from konova.contexts import BaseContext
from konova.utils.message_templates import TEAM_LEFT, TEAM_REMOVED, TEAM_EDITED, TEAM_ADDED
from konova.views.base import BaseModalFormView
from user.forms.modals.team import LeaveTeamModalForm, RemoveTeamModalForm, EditTeamModalForm, NewTeamModalForm
from user.forms.team import TeamDataForm
from user.models import Team
from user.views.users import UserBaseView
class TeamDetailModalView(LoginRequiredMixin, BaseModalFormView):
_FORM_CLS = TeamDataForm
_MODEL_CLS = Team
def _user_has_shared_access(self, user, **kwargs):
# No specific constraints
return True
def _user_has_permission(self, user, **kwargs):
# No specific constraints
return True
class TeamIndexView(LoginRequiredMixin, UserBaseView):
_TEMPLATE = "user/team/index.html"
_TAB_TITLE = _("Teams")
def get(self, request: HttpRequest):
user = request.user
context = {
"teams": user.shared_teams,
"tab_title": self._TAB_TITLE,
}
context = BaseContext(request, context).context
return render(request, self._TEMPLATE, context)
class BaseTeamView(LoginRequiredMixin, BaseModalFormView):
_REDIRECT_URL = "user:team-index"
_MODEL_CLS = Team
class Meta:
abstract = True
def _user_has_permission(self, user, **kwargs):
# Nothing to check here - just pass the test
return True
def _user_has_shared_access(self, user, **kwargs):
# Nothing to check here - just pass the test
return True
def _get_redirect_url(self, *args, **kwargs):
return reverse(self._REDIRECT_URL)
class NewTeamView(BaseTeamView):
_FORM_CLS = NewTeamModalForm
_MSG_SUCCESS = TEAM_ADDED
class EditTeamView(BaseTeamView):
_FORM_CLS = EditTeamModalForm
_MSG_SUCCESS = TEAM_EDITED
def _user_has_permission(self, user, **kwargs):
team = get_object_or_404(Team, id=kwargs.get("id"))
user_is_admin = team.is_user_admin(user)
if not user_is_admin:
# If user is not an admin, we act as if there is no such team on the database
raise Http404()
return user_is_admin
class RemoveTeamView(BaseTeamView):
_FORM_CLS = RemoveTeamModalForm
_MSG_SUCCESS = TEAM_REMOVED
def _user_has_permission(self, user, **kwargs):
team_id = kwargs.get("id")
team = get_object_or_404(Team, id=team_id)
user_is_admin = team.is_user_admin(user)
if not user_is_admin:
raise Http404()
return True
class LeaveTeamView(BaseTeamView):
_FORM_CLS = LeaveTeamModalForm
_MSG_SUCCESS = TEAM_LEFT
def _user_has_shared_access(self, user, **kwargs):
team_id = kwargs.get("id")
team = get_object_or_404(self._MODEL_CLS, id=team_id)
is_user_team_member = team.users.filter(id=user.id).exists()
if not is_user_team_member:
raise Http404()
return True

81
user/views/users.py Normal file
View File

@ -0,0 +1,81 @@
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.views.base import BaseView, BaseModalFormView
from user.forms.modals.user import UserContactForm
from user.forms.user import UserNotificationForm
from user.models import User
from django.http import HttpRequest
from django.shortcuts import render, redirect
from django.utils.translation import gettext_lazy as _
from konova.contexts import BaseContext
class UserBaseView(BaseView):
def _user_has_shared_access(self, user, **kwargs):
return True
def _user_has_permission(self, user, **kwargs):
return True
class UserDetailView(LoginRequiredMixin, UserBaseView):
_TEMPLATE = "user/index.html"
_TAB_TITLE = _("User settings")
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)
class NotificationsView(LoginRequiredMixin, UserBaseView):
_TEMPLATE = "user/notifications.html"
_TAB_TITLE = _("User notifications")
def get(self, request: HttpRequest):
user = request.user
form = UserNotificationForm(user=user, data=None)
context = {
"user": user,
"form": form,
TAB_TITLE_IDENTIFIER: self._TAB_TITLE,
}
context = BaseContext(request, context).context
return render(request, self._TEMPLATE, context)
def post(self, request: HttpRequest):
user = request.user
form = UserNotificationForm(user=user, data=request.POST)
if form.is_valid():
form.save()
messages.success(
request,
_("Notifications edited")
)
return redirect("user:detail")
context = {
"user": user,
"form": form,
TAB_TITLE_IDENTIFIER: self._TAB_TITLE,
}
context = BaseContext(request, context).context
return render(request, self._TEMPLATE, context)
class ContactView(LoginRequiredMixin, BaseModalFormView):
_FORM_CLS = UserContactForm
_MODEL_CLS = User
def _user_has_shared_access(self, user, **kwargs):
# No specific constraints
return True
def _user_has_permission(self, user, **kwargs):
# No specific constraints
return True

View File

@ -1,206 +0,0 @@
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
from user.forms.user import UserNotificationForm
from user.models import User, Team
from django.http import HttpRequest, Http404
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 login_required_modal
class UserBaseView(BaseView):
def _user_has_shared_access(self, user, **kwargs):
return True
def _user_has_permission(self, user):
return True
class UserDetailView(LoginRequiredMixin, UserBaseView):
_TEMPLATE = "user/index.html"
_TAB_TITLE = _("User settings")
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)
class NotificationsView(LoginRequiredMixin, UserBaseView):
_TEMPLATE = "user/notifications.html"
_TAB_TITLE = _("User notifications")
def get(self, request: HttpRequest):
user = request.user
form = UserNotificationForm(user=user, data=None)
context = {
"user": user,
"form": form,
TAB_TITLE_IDENTIFIER: self._TAB_TITLE,
}
context = BaseContext(request, context).context
return render(request, self._TEMPLATE, context)
def post(self, request: HttpRequest):
user = request.user
form = UserNotificationForm(user=user, data=request.POST)
if form.is_valid():
form.save()
messages.success(
request,
_("Notifications edited")
)
return redirect("user:detail")
context = {
"user": user,
"form": form,
TAB_TITLE_IDENTIFIER: self._TAB_TITLE,
}
context = BaseContext(request, context).context
return render(request, self._TEMPLATE, context)
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
Returns:
"""
user = get_object_or_404(User, id=id)
form = UserContactForm(request.POST or None, instance=user, request=request)
context = {
"form": form,
}
context = BaseContext(request, context).context
return render(request, self._TEMPLATE, context)
def _user_has_shared_access(self, user, **kwargs):
# No specific constraints
return True
def _user_has_permission(self, user):
# No specific constraints
return True
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
Returns:
"""
team = get_object_or_404(Team, id=id)
form = TeamDataForm(request.POST or None, instance=team, request=request)
context = {
"form": form,
}
context = BaseContext(request, context).context
return render(request, self._TEMPLATE, context)
def _user_has_shared_access(self, user, **kwargs):
# No specific constraints
return True
def _user_has_permission(self, user):
# No specific constraints
return True
class TeamIndexView(LoginRequiredMixin, UserBaseView):
_TEMPLATE = "user/team/index.html"
_TAB_TITLE = _("Teams")
def get(self, request: HttpRequest):
user = request.user
context = {
"teams": user.shared_teams,
"tab_title": self._TAB_TITLE,
}
context = BaseContext(request, context).context
return render(request, self._TEMPLATE, context)
@login_required_modal
@login_required
def new_team_view(request: HttpRequest):
form = NewTeamModalForm(request.POST or None, request=request)
return form.process_request(
request,
_("New team added"),
redirect_url=reverse("user:team-index")
)
@login_required_modal
@login_required
def edit_team_view(request: HttpRequest, id: str):
team = get_object_or_404(Team, id=id)
user_is_admin = team.is_user_admin(request.user)
if not user_is_admin:
raise Http404()
form = EditTeamModalForm(request.POST or None, instance=team, request=request)
return form.process_request(
request,
_("Team edited"),
redirect_url=reverse("user:team-index")
)
@login_required_modal
@login_required
def remove_team_view(request: HttpRequest, id: str):
team = get_object_or_404(Team, id=id)
user_is_admin = team.is_user_admin(request.user)
if not user_is_admin:
raise Http404()
form = RemoveTeamModalForm(request.POST or None, instance=team, request=request)
return form.process_request(
request,
_("Team removed"),
redirect_url=reverse("user:team-index")
)
@login_required_modal
@login_required
def leave_team_view(request: HttpRequest, id: str):
team = get_object_or_404(Team, id=id)
user = request.user
is_user_team_member = team.users.filter(id=user.id).exists()
if not is_user_team_member:
messages.info(
request,
_("You are not a member of this team")
)
return redirect("user:team-index")
form = LeaveTeamModalForm(request.POST or None, instance=team, request=request)
return form.process_request(
request,
_("Left Team"),
redirect_url=reverse("user:team-index")
)