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