Source code for ferrosoft.apps.ferrobase.views.user

#  Copyright (c) 2025 Ferrosoft GmbH. All rights reserved.
from django.contrib.auth.decorators import login_required, permission_required
from django.http import HttpResponse, HttpRequest
from django.shortcuts import get_object_or_404, render, redirect
from django.utils.translation import gettext_lazy as _

from ferrosoft.apps.ferrobase.filter import UserFilterSet
from ferrosoft.apps.ferrobase.forms import (
    UserGroupsForm,
    UserCreationForm,
    UserUpdateForm,
)
from ferrosoft.apps.ferrobase.models import CompanySite, User, LocalUser
from ferrosoft.apps.ferrobase.tables.models import UserTable
from ferrosoft.apps.ferrobase.tenant.forms import UserResourcesForm
from ferrosoft.apps.ferrobase.views.organization import (
    FeatureRequiredMixin,
    FeatureLimiter,
)
from ferrosoft.apps.ferrobase.views.generic import (
    CreateView,
    UpdateView,
    DeleteView,
    model_crud_routes,
    TableView,
)


[docs] class UserLimiter(FeatureLimiter): """Counts users in the request user's organisation for plan-limit checks."""
[docs] def get_count(self, request: HttpRequest) -> int: """Return the current user count for the organisation (excluding tenants).""" # noinspection PyUnresolvedReferences return request.user.organization.users.count()
def _ensure_org_authorized(request: HttpRequest, user: User): """Return a redirect when the request user may not act on ``user``. Superusers always pass. Otherwise the requesting user and the target user must belong to the same organisation; failing that, a temporary redirect to ``ferrobase:index`` is returned. """ if ( request.user.is_superuser or request.user.organization_id == user.organization_id ): return None return redirect("ferrobase:index", permanent=False)
[docs] class UserCreateView(FeatureRequiredMixin, CreateView): """Create a user; the "users" feature limit enforces subscription caps.""" feature_required = "users" feature_limiter = UserLimiter() form_class = UserCreationForm model = User template_name = "ferrobase/user/detail.html"
[docs] def get_form_kwargs(self): """Inject the organisation and the superuser-only org-selection flag.""" return super().get_form_kwargs() | { "organization": self.request.user.organization, "allow_organization_selection": self.request.user.is_superuser, }
[docs] class UserList(FeatureRequiredMixin, TableView): """List users, scoped to the current organisation for non-superusers.""" feature_required = "users" filterset_class = UserFilterSet model = User ordering = "username" table_class = UserTable
[docs] def get_queryset(self): """Restrict to the request user's organisation unless they are a superuser.""" qs = super().get_queryset() if not self.request.user.is_superuser: qs = qs.filter(organization=self.request.user.organization) return qs
[docs] class UserUpdateView(FeatureRequiredMixin, UpdateView): """Update a user; both GET and POST enforce same-organisation authorisation.""" feature_required = "users" form_class = UserUpdateForm model = User template_name = "ferrobase/user/detail.html"
[docs] def get(self, request, *args, **kwargs): """Authorise then defer to the base GET handler.""" response = _ensure_org_authorized(request, self.get_object()) if response is not None: return response return super().get(request, *args, **kwargs)
[docs] def post(self, request, *args, **kwargs): """Authorise then defer to the base POST handler.""" response = _ensure_org_authorized(request, self.get_object()) if response is not None: return response return super().post(request, *args, **kwargs)
[docs] class UserDeleteView(FeatureRequiredMixin, DeleteView): """Delete a user; both GET and POST enforce same-organisation authorisation.""" feature_required = "users" model = User
[docs] def get(self, request, *args, **kwargs): """Authorise then defer to the base GET handler.""" response = _ensure_org_authorized(request, self.get_object()) if response is not None: return response return super().get(request, *args, **kwargs)
[docs] def post(self, request, *args, **kwargs): """Authorise then defer to the base POST handler.""" response = _ensure_org_authorized(request, self.get_object()) if response is not None: return response return super().post(request, *args, **kwargs)
user_urls = model_crud_routes( User, create_view=UserCreateView, read_view=UserList, update_view=UserUpdateView, delete_view=UserDeleteView, )
[docs] class UserGroupsView(FeatureRequiredMixin, UpdateView): """Assign auth groups to a user via :class:`UserGroupsForm`.""" feature_required = "users" activity_app = "ferrobase" activity_title = _("User groups") form_class = UserGroupsForm model = User
[docs] @login_required @permission_required("ferrobase.change_user") def assign_user_resources(request, pk): """Edit the storage-location grants assigned to a user. Renders the assignment form on GET. On POST the submitted location UUIDs are validated via :class:`UserResourcesForm` and propagated through :meth:`CompanySite.set_locations_for_user`; both the user and the associated :class:`LocalUser` are refreshed from the database afterward. """ user = get_object_or_404(User, pk=pk) local_user = LocalUser.load(user) if request.method == "POST": form = UserResourcesForm(request.POST) if not form.is_valid(): return HttpResponse(status=400) locations = form.cleaned_data.get("storage_location", []) if not isinstance(locations, list): return HttpResponse(status=400) CompanySite.set_locations_for_user(locations, local_user) user.refresh_from_db() local_user.refresh_from_db() return render( request, "ferrobase/user-assign-resources.html", context={ "user": user, "storage_locations": CompanySite.options_for_user(user.local_user()), }, )