394 lines
15 KiB
Python
394 lines
15 KiB
Python
# -----------------------------------------------------------------------
|
|
# Asset / Location Permission Management Views
|
|
# -----------------------------------------------------------------------
|
|
#
|
|
# These views provide an in-app interface to manage per-instance user
|
|
# permissions (UserAssetAccess) and per-type configuration
|
|
# (AssetTypePermissionConfig).
|
|
#
|
|
# Access is restricted to admin-role users.
|
|
# -----------------------------------------------------------------------
|
|
|
|
import json
|
|
|
|
from django.contrib.contenttypes.models import ContentType
|
|
from django.db.models import ForeignKey, OneToOneField, ManyToManyField, AutoField
|
|
from django.contrib.gis.db.models import GeometryField
|
|
from django.http import JsonResponse, Http404
|
|
from django.shortcuts import get_object_or_404, render, redirect
|
|
from django.views.decorators.http import require_POST, require_GET
|
|
from django.utils.translation import gettext as _
|
|
from django.contrib.auth.models import User
|
|
from django.contrib.auth.decorators import login_required
|
|
|
|
from common.models import UserConfig, UserAssetAccess, UserAssetTypeAccess, AssetTypePermissionConfig
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _is_admin(user):
|
|
"""Return True if the user has the 'admin' role."""
|
|
try:
|
|
return UserConfig.objects.get(user=user).roles.filter(name='admin').exists()
|
|
except UserConfig.DoesNotExist:
|
|
return False
|
|
|
|
|
|
def _admin_required(view_fn):
|
|
"""Simple decorator: only admin-role users may access these views."""
|
|
from functools import wraps
|
|
|
|
@wraps(view_fn)
|
|
@login_required
|
|
def _wrapped(request, *args, **kwargs):
|
|
if not _is_admin(request.user):
|
|
return JsonResponse({'error': _("Accès refusé — rôle admin requis.")}, status=403)
|
|
return view_fn(request, *args, **kwargs)
|
|
|
|
return _wrapped
|
|
|
|
|
|
def _get_editable_field_choices(model_class):
|
|
"""
|
|
Return a list of (field_name, verbose_label) for fields that can be
|
|
restricted via editable_fields:
|
|
- only concrete (non-FK, non-M2M, non-AutoField, non-Geometry) fields
|
|
of the model itself and its abstract parents.
|
|
"""
|
|
# Gather fields from the full MRO (includes abstract parents)
|
|
excluded_types = (ForeignKey, OneToOneField, ManyToManyField, AutoField, GeometryField)
|
|
seen = set()
|
|
choices = []
|
|
for f in model_class._meta.get_fields():
|
|
if f.name in seen:
|
|
continue
|
|
if not hasattr(f, 'column'): # reverse relations, M2M descriptors
|
|
continue
|
|
if isinstance(f, excluded_types):
|
|
continue
|
|
seen.add(f.name)
|
|
label = f.verbose_name if hasattr(f, 'verbose_name') else f.name
|
|
choices.append((f.name, f"{label} ({f.name})"))
|
|
return choices
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Per-object permission list + create (panel rendered inside asset detail)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@_admin_required
|
|
def asset_permissions_panel(request, content_type_id, object_id):
|
|
"""
|
|
GET → renders the permissions panel partial for a given object.
|
|
POST → creates a new UserAssetAccess.
|
|
"""
|
|
ct = get_object_or_404(ContentType, pk=content_type_id)
|
|
model_class = ct.model_class()
|
|
if model_class is None:
|
|
raise Http404
|
|
|
|
obj = get_object_or_404(model_class, pk=object_id)
|
|
|
|
# Type-level config (ensure it exists)
|
|
type_config, _ = AssetTypePermissionConfig.objects.get_or_create(content_type=ct)
|
|
|
|
if request.method == 'POST':
|
|
user_id = request.POST.get('user_id')
|
|
if not user_id:
|
|
return JsonResponse({'success': False, 'error': _("Utilisateur requis.")}, status=400)
|
|
|
|
try:
|
|
target_user = User.objects.get(pk=user_id)
|
|
target_config = UserConfig.objects.get(user=target_user)
|
|
except (User.DoesNotExist, UserConfig.DoesNotExist):
|
|
return JsonResponse({'success': False, 'error': _("Utilisateur introuvable.")}, status=404)
|
|
|
|
editable_fields_raw = request.POST.getlist('editable_fields')
|
|
visible_fields_raw = request.POST.getlist('visible_fields')
|
|
|
|
access, created = UserAssetAccess.objects.get_or_create(
|
|
user_config=target_config,
|
|
content_type=ct,
|
|
object_id=object_id,
|
|
defaults={
|
|
'can_view': request.POST.get('can_view') == 'on',
|
|
'can_edit': request.POST.get('can_edit') == 'on',
|
|
'can_delete': request.POST.get('can_delete') == 'on',
|
|
'editable_fields': editable_fields_raw or None,
|
|
'visible_fields': visible_fields_raw or None,
|
|
}
|
|
)
|
|
if not created:
|
|
access.can_view = request.POST.get('can_view') == 'on'
|
|
access.can_edit = request.POST.get('can_edit') == 'on'
|
|
access.can_delete = request.POST.get('can_delete') == 'on'
|
|
access.editable_fields = editable_fields_raw or None
|
|
access.visible_fields = visible_fields_raw or None
|
|
access.save()
|
|
|
|
# Re-render the panel (HTMX-friendly)
|
|
return _render_panel(request, ct, object_id, obj, type_config, model_class)
|
|
|
|
# GET
|
|
return _render_panel(request, ct, object_id, obj, type_config, model_class)
|
|
|
|
|
|
def _render_panel(request, ct, object_id, obj, type_config, model_class):
|
|
accesses = (
|
|
UserAssetAccess.objects
|
|
.filter(content_type=ct, object_id=object_id)
|
|
.select_related('user_config__user')
|
|
)
|
|
type_user_accesses = (
|
|
UserAssetTypeAccess.objects
|
|
.filter(content_type=ct)
|
|
.select_related('user_config__user')
|
|
.order_by('user_config__user__username')
|
|
)
|
|
all_user_configs = (
|
|
UserConfig.objects
|
|
.select_related('user')
|
|
.order_by('user__username')
|
|
)
|
|
field_choices = _get_editable_field_choices(model_class)
|
|
|
|
return render(
|
|
request,
|
|
'assets/permissions/panel.html',
|
|
{
|
|
'ct': ct,
|
|
'object_id': object_id,
|
|
'obj': obj,
|
|
'type_config': type_config,
|
|
'accesses': accesses,
|
|
'type_user_accesses': type_user_accesses,
|
|
'all_user_configs': all_user_configs,
|
|
'field_choices': field_choices,
|
|
},
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Edit a single UserAssetAccess
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@_admin_required
|
|
def asset_access_edit(request, access_id):
|
|
"""
|
|
GET → renders an edit form for the given UserAssetAccess.
|
|
POST → saves the changes and re-renders the permissions panel.
|
|
"""
|
|
access = get_object_or_404(
|
|
UserAssetAccess.objects.select_related('user_config__user', 'content_type'),
|
|
pk=access_id,
|
|
)
|
|
ct = access.content_type
|
|
model_class = ct.model_class()
|
|
field_choices = _get_editable_field_choices(model_class) if model_class else []
|
|
|
|
if request.method == 'POST':
|
|
access.can_view = request.POST.get('can_view') == 'on'
|
|
access.can_edit = request.POST.get('can_edit') == 'on'
|
|
access.can_delete = request.POST.get('can_delete') == 'on'
|
|
editable_fields_raw = request.POST.getlist('editable_fields')
|
|
access.editable_fields = editable_fields_raw or None
|
|
visible_fields_raw = request.POST.getlist('visible_fields')
|
|
access.visible_fields = visible_fields_raw or None
|
|
access.save()
|
|
|
|
# Redirect back to the parent panel
|
|
return redirect(
|
|
'assets:asset_permissions_panel',
|
|
content_type_id=ct.pk,
|
|
object_id=access.object_id,
|
|
)
|
|
|
|
return render(
|
|
request,
|
|
'assets/permissions/access_edit.html',
|
|
{
|
|
'access': access,
|
|
'field_choices': field_choices,
|
|
},
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Delete a single UserAssetAccess
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@_admin_required
|
|
@require_POST
|
|
def asset_access_delete(request, access_id):
|
|
access = get_object_or_404(UserAssetAccess, pk=access_id)
|
|
ct_id = access.content_type_id
|
|
obj_id = access.object_id
|
|
access.delete()
|
|
return redirect(
|
|
'assets:asset_permissions_panel',
|
|
content_type_id=ct_id,
|
|
object_id=obj_id,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Edit / delete a single UserAssetTypeAccess
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@_admin_required
|
|
def asset_type_access_edit(request, type_access_id):
|
|
"""
|
|
GET → renders an edit form for the given UserAssetTypeAccess.
|
|
POST → saves changes and redirects to the type page.
|
|
"""
|
|
type_access = get_object_or_404(
|
|
UserAssetTypeAccess.objects.select_related('user_config__user', 'content_type'),
|
|
pk=type_access_id,
|
|
)
|
|
ct = type_access.content_type
|
|
model_class = ct.model_class()
|
|
field_choices = _get_editable_field_choices(model_class) if model_class else []
|
|
|
|
if request.method == 'POST':
|
|
type_access.can_view = request.POST.get('can_view') == 'on'
|
|
type_access.can_edit = request.POST.get('can_edit') == 'on'
|
|
type_access.can_delete = request.POST.get('can_delete') == 'on'
|
|
editable_fields_raw = request.POST.getlist('editable_fields')
|
|
type_access.editable_fields = editable_fields_raw or None
|
|
visible_fields_raw = request.POST.getlist('visible_fields')
|
|
type_access.visible_fields = visible_fields_raw or None
|
|
type_access.save()
|
|
return redirect('assets:asset_type_permissions_page', content_type_id=ct.pk)
|
|
|
|
return render(
|
|
request,
|
|
'assets/permissions/type_access_edit.html',
|
|
{
|
|
'type_access': type_access,
|
|
'field_choices': field_choices,
|
|
},
|
|
)
|
|
|
|
|
|
@_admin_required
|
|
@require_POST
|
|
def asset_type_access_delete(request, type_access_id):
|
|
type_access = get_object_or_404(UserAssetTypeAccess, pk=type_access_id)
|
|
ct_id = type_access.content_type_id
|
|
type_access.delete()
|
|
return redirect('assets:asset_type_permissions_page', content_type_id=ct_id)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Per-type configuration (AssetTypePermissionConfig)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@_admin_required
|
|
def asset_type_permissions_page(request, content_type_id):
|
|
"""
|
|
Dedicated page for a model type:
|
|
- Toggle requires_explicit_permissions
|
|
- Manage UserAssetTypeAccess rows (per-user, all objects of this type)
|
|
- List all UserAssetAccess rows for every instance of this type
|
|
"""
|
|
ct = get_object_or_404(ContentType, pk=content_type_id)
|
|
model_class = ct.model_class()
|
|
|
|
type_config, _ = AssetTypePermissionConfig.objects.get_or_create(content_type=ct)
|
|
|
|
if request.method == 'POST':
|
|
action = request.POST.get('action', 'type_config')
|
|
|
|
if action == 'type_config':
|
|
type_config.requires_explicit_permissions = (
|
|
request.POST.get('requires_explicit_permissions') == 'on'
|
|
)
|
|
type_config.save()
|
|
|
|
elif action == 'add_type_user_access':
|
|
user_id = request.POST.get('user_id')
|
|
if user_id:
|
|
try:
|
|
target_user = User.objects.get(pk=user_id)
|
|
target_config = UserConfig.objects.get(user=target_user)
|
|
editable_fields_raw = request.POST.getlist('editable_fields')
|
|
visible_fields_raw = request.POST.getlist('visible_fields')
|
|
access, created = UserAssetTypeAccess.objects.get_or_create(
|
|
user_config=target_config,
|
|
content_type=ct,
|
|
defaults={
|
|
'can_view': request.POST.get('can_view') == 'on',
|
|
'can_edit': request.POST.get('can_edit') == 'on',
|
|
'can_delete': request.POST.get('can_delete') == 'on',
|
|
'editable_fields': editable_fields_raw or None,
|
|
'visible_fields': visible_fields_raw or None,
|
|
},
|
|
)
|
|
if not created:
|
|
access.can_view = request.POST.get('can_view') == 'on'
|
|
access.can_edit = request.POST.get('can_edit') == 'on'
|
|
access.can_delete = request.POST.get('can_delete') == 'on'
|
|
access.editable_fields = editable_fields_raw or None
|
|
access.visible_fields = visible_fields_raw or None
|
|
access.save()
|
|
except (User.DoesNotExist, UserConfig.DoesNotExist):
|
|
pass
|
|
|
|
return redirect('assets:asset_type_permissions_page', content_type_id=ct.pk)
|
|
|
|
# All UserAssetAccess rows for this content type
|
|
accesses = (
|
|
UserAssetAccess.objects
|
|
.filter(content_type=ct)
|
|
.select_related('user_config__user')
|
|
.order_by('user_config__user__username', 'object_id')
|
|
)
|
|
|
|
# Resolve objects (best-effort; skip if model is unavailable)
|
|
resolved = []
|
|
if model_class:
|
|
obj_ids = list(accesses.values_list('object_id', flat=True).distinct())
|
|
objs = {o.pk: o for o in model_class.objects.filter(pk__in=obj_ids)}
|
|
for a in accesses:
|
|
resolved.append({
|
|
'access': a,
|
|
'obj': objs.get(a.object_id),
|
|
})
|
|
else:
|
|
for a in accesses:
|
|
resolved.append({'access': a, 'obj': None})
|
|
|
|
# UserAssetTypeAccess rows for this content type
|
|
type_user_accesses = (
|
|
UserAssetTypeAccess.objects
|
|
.filter(content_type=ct)
|
|
.select_related('user_config__user')
|
|
.order_by('user_config__user__username')
|
|
)
|
|
|
|
field_choices = _get_editable_field_choices(model_class) if model_class else []
|
|
|
|
all_user_configs = (
|
|
UserConfig.objects
|
|
.select_related('user')
|
|
.order_by('user__username')
|
|
)
|
|
|
|
return render(
|
|
request,
|
|
'assets/permissions/type_page.html',
|
|
{
|
|
'ct': ct,
|
|
'type_config': type_config,
|
|
'resolved_accesses': resolved,
|
|
'type_user_accesses': type_user_accesses,
|
|
'field_choices': field_choices,
|
|
'all_user_configs': all_user_configs,
|
|
'model_verbose_name': (
|
|
model_class._meta.verbose_name_plural
|
|
if model_class else ct.model
|
|
),
|
|
},
|
|
)
|