loko/streetup/assets/views/asset_permissions.py
2026-07-22 14:48:40 +02:00

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
),
},
)