# 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]
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()),
},
)