485 lines
24 KiB
Python
485 lines
24 KiB
Python
"""Invitation-only registration and hierarchical space management."""
|
||
import secrets
|
||
import logging
|
||
import shutil
|
||
from pathlib import Path
|
||
from django.conf import settings
|
||
from django.db.models import Q
|
||
from datetime import timedelta
|
||
from django.contrib.auth import get_user_model
|
||
from django.contrib.auth.password_validation import validate_password
|
||
from django.core.exceptions import ValidationError
|
||
from django.db import transaction, IntegrityError
|
||
from django.utils import timezone
|
||
from rest_framework.decorators import api_view, permission_classes, throttle_classes
|
||
from rest_framework.permissions import AllowAny
|
||
from rest_framework.throttling import AnonRateThrottle
|
||
from rest_framework.response import Response
|
||
from .models import DriveSpace, SpaceInvitation, SpaceGrant, DriveItem, UploadSession
|
||
from .services import error, valid_name
|
||
from .spaces import ensure_space, ensure_private_space, my_space, create_space, _make_numbered_space, is_admin, hash_token, space_info, can_read_space, can_write_space, can_manage_space
|
||
from .quotas import (parse_quota, locked_quota_tree,
|
||
assert_new_child_quota, assert_updated_quota)
|
||
|
||
|
||
def permission_flags(data):
|
||
return {k: data.get(k) is True for k in ('can_view_parent','can_write_parent','can_invite')}
|
||
|
||
|
||
def manager(user, parent):
|
||
return can_manage_space(user, parent)
|
||
|
||
|
||
def enforce_delegation(user, space, *, view=True, write=False, invite=False):
|
||
# These checks must be repeated on the server for each creation/change.
|
||
if view and not can_read_space(user, space):
|
||
raise ValueError('Tu ne peux pas attribuer la lecture de cet espace.')
|
||
if write and not can_write_space(user, space):
|
||
raise ValueError('Tu ne peux pas attribuer l’écriture de cet espace.')
|
||
if invite and not can_manage_space(user, space):
|
||
raise ValueError('Tu ne peux pas déléguer la gestion des utilisateurs.')
|
||
|
||
|
||
|
||
def invitation_info(inv):
|
||
return {'id':str(inv.pk), 'created_at':inv.created_at.isoformat(),
|
||
'expires_at':inv.expires_at.isoformat(), 'used':bool(inv.used_at),
|
||
'revoked':bool(inv.revoked_at),
|
||
'can_view_parent':inv.can_view_parent,'can_write_parent':inv.can_write_parent,
|
||
'can_invite':inv.can_invite}
|
||
|
||
|
||
@api_view(['GET'])
|
||
def spaces(request):
|
||
own = ensure_space(request.user)
|
||
qs=DriveSpace.objects.select_related('user','private_owner','managed_by','parent').order_by('relative_path')
|
||
private=ensure_private_space(request.user)
|
||
if is_admin(request.user):
|
||
permitted=[private, *qs.filter(private_owner__isnull=True)]
|
||
else:
|
||
permitted=[private,own]
|
||
if own.user_id == request.user.pk and own.parent_id and own.can_view_parent and not own.parent.private_owner_id:
|
||
permitted.append(own.parent)
|
||
permitted.extend(space for space in qs.filter(access_grants__user=request.user) if not space.private_owner_id)
|
||
permitted.extend(qs.filter(managed_by=request.user,private_owner__isnull=True))
|
||
permitted=list({space.pk:space for space in permitted}.values())
|
||
return Response({'mine':private.pk, 'assigned':own.pk, 'private':private.pk,
|
||
'spaces':[space_info(space,request.user) for space in permitted]})
|
||
|
||
|
||
@api_view(['PATCH'])
|
||
def rename_my_space(request):
|
||
own=my_space(request.user)
|
||
if own.user_id != request.user.pk and not is_admin(request.user):
|
||
return error('Impossible de renommer un espace attribué à un autre compte.',403)
|
||
try:
|
||
title=valid_name(request.data.get('display_name'))
|
||
except ValueError as ex:return error(str(ex))
|
||
if len(title)>120:return error('Nom limité à 120 caractères.')
|
||
own.display_name=title
|
||
own.save(update_fields=['display_name'])
|
||
return Response(space_info(own,request.user))
|
||
|
||
|
||
@api_view(['GET'])
|
||
def members(request):
|
||
own=my_space(request.user)
|
||
parent=own
|
||
if request.query_params.get('parent_space'):
|
||
try:parent=DriveSpace.objects.get(pk=int(request.query_params['parent_space']))
|
||
except (ValueError,TypeError,DriveSpace.DoesNotExist):return error('Espace introuvable.',404)
|
||
if not manager(request.user,parent):return error('Gestion des invitations non autorisée.',403)
|
||
children=DriveSpace.objects.filter(parent=parent,private_owner__isnull=True).select_related('user','parent').order_by('id')
|
||
invitations=SpaceInvitation.objects.filter(parent_space=parent).order_by('-created_at')[:100]
|
||
return Response({'parent':space_info(parent,request.user),
|
||
'children':[space_info(c,request.user) for c in children if not c.private_owner_id],
|
||
'grants':[grant_info(g) for g in SpaceGrant.objects.filter(space=parent).select_related('user').order_by('user__username')],
|
||
'invitations':[invitation_info(i) for i in invitations]})
|
||
|
||
|
||
def _update_quota(space, requested):
|
||
"""Must run under transaction.atomic and after locking all ancestors."""
|
||
if space.private_owner_id:
|
||
raise ValueError('Le quota des espaces privés est fixé à 50 Go.')
|
||
with locked_quota_tree(space):
|
||
quota = parse_quota(requested)
|
||
assert_updated_quota(space, quota)
|
||
space.quota_bytes = quota
|
||
space.save(update_fields=['quota_bytes'])
|
||
|
||
|
||
@api_view(['PATCH'])
|
||
def update_member(request,space_id):
|
||
with transaction.atomic():
|
||
try:child=DriveSpace.objects.select_related('parent','user').get(pk=space_id)
|
||
except DriveSpace.DoesNotExist:return error('Espace introuvable.',404)
|
||
if child.private_owner_id or not child.parent_id:return error('Impossible de modifier cet espace ici.',403)
|
||
if not manager(request.user,child.parent):return error('Modification interdite.',403)
|
||
changes=[]
|
||
for key in ('can_view_parent','can_write_parent','can_invite'):
|
||
if key in request.data:
|
||
if not isinstance(request.data[key],bool):return error('Les permissions doivent être des booléens.')
|
||
setattr(child,key,request.data[key]);changes.append(key)
|
||
if child.can_write_parent and not child.can_view_parent:
|
||
return error('L’écriture nécessite la lecture de l’espace parent.')
|
||
try:enforce_delegation(request.user,child.parent,view=child.can_view_parent,
|
||
write=child.can_write_parent,invite=child.can_invite)
|
||
except ValueError as exc:return error(str(exc),403)
|
||
if 'quota_bytes' in request.data:
|
||
try:_update_quota(child, request.data['quota_bytes'])
|
||
except ValueError as ex:return error(str(ex))
|
||
if changes:child.save(update_fields=changes)
|
||
if 'is_active' in request.data and child.user_id is None:
|
||
return error('Ce dossier partagé ne possède pas de compte utilisateur.')
|
||
if 'is_active' in request.data:
|
||
if not isinstance(request.data['is_active'],bool):return error('Statut invalide.')
|
||
child.user.is_active=request.data['is_active']
|
||
child.user.save(update_fields=['is_active'])
|
||
return Response(space_info(child,request.user))
|
||
|
||
|
||
@api_view(['PATCH'])
|
||
def update_space_quota(request, space_id):
|
||
"""Administrators may cap any space; delegated inviters manage their direct children."""
|
||
with transaction.atomic():
|
||
try:space=DriveSpace.objects.select_related('parent','user').get(pk=space_id)
|
||
except DriveSpace.DoesNotExist:return error('Espace introuvable.',404)
|
||
if space.private_owner_id:
|
||
return error('Le quota des espaces privés est fixé à 50 Go.',403)
|
||
if not (is_admin(request.user) or (space.parent_id and not space.private_owner_id and manager(request.user,space.parent))):
|
||
return error('Modification du quota interdite.',403)
|
||
if 'quota_bytes' not in request.data:return error('quota_bytes est obligatoire.')
|
||
try:_update_quota(space,request.data['quota_bytes'])
|
||
except ValueError as ex:return error(str(ex))
|
||
return Response(space_info(space,request.user))
|
||
|
||
|
||
@api_view(['POST'])
|
||
def create_invitation(request):
|
||
own=my_space(request.user)
|
||
parent=own
|
||
if request.data.get('parent_space'):
|
||
try:parent=DriveSpace.objects.get(pk=int(request.data['parent_space']))
|
||
except (ValueError,TypeError,DriveSpace.DoesNotExist):return error('Espace introuvable.',404)
|
||
if not manager(request.user,parent):return error('Vous ne pouvez pas inviter depuis cet espace.',403)
|
||
fields=permission_flags(request.data)
|
||
if 'can_view_parent' not in request.data:fields['can_view_parent']=True
|
||
if not fields['can_view_parent']:
|
||
return error('L’invitation donne nécessairement accès en lecture à cet espace.')
|
||
try:enforce_delegation(request.user,parent,view=fields['can_view_parent'],write=fields['can_write_parent'],invite=fields['can_invite'])
|
||
except ValueError as exc:return error(str(exc),403)
|
||
try:days=int(request.data.get('expires_days',7))
|
||
except (ValueError,TypeError):return error('Durée invalide.')
|
||
if not 1<=days<=30:return error('La durée doit être comprise entre 1 et 30 jours.')
|
||
# An invitation grants access. Storage limits are configured on spaces,
|
||
# not on invitation links (the private space always has its 50-Go cap).
|
||
token=secrets.token_urlsafe(32)
|
||
inv=SpaceInvitation.objects.create(parent_space=parent,created_by=request.user,
|
||
token_hash=hash_token(token),expires_at=timezone.now()+timedelta(days=days),**fields)
|
||
# Returned exactly once; DB stores only SHA-256.
|
||
return Response({**invitation_info(inv),'token':token},status=201)
|
||
|
||
|
||
@api_view(['POST'])
|
||
def revoke_invitation(request,invitation_id):
|
||
try:inv=SpaceInvitation.objects.select_related('parent_space').get(pk=invitation_id)
|
||
except SpaceInvitation.DoesNotExist:return error('Invitation introuvable.',404)
|
||
if not manager(request.user,inv.parent_space):return error('Accès refusé.',403)
|
||
if inv.used_at:return error('Invitation déjà utilisée.')
|
||
inv.revoked_at=timezone.now();inv.save(update_fields=['revoked_at'])
|
||
return Response({'ok':True})
|
||
|
||
|
||
def get_valid_invitation(token,lock=False):
|
||
qs=SpaceInvitation.objects.select_related('parent_space', 'created_by')
|
||
if lock:qs=qs.select_for_update()
|
||
try:inv=qs.get(token_hash=hash_token(token))
|
||
except SpaceInvitation.DoesNotExist:return None
|
||
if inv.used_at or inv.revoked_at or inv.expires_at<=timezone.now():return None
|
||
# Revoking the inviter's delegation also invalidates their pending invitations.
|
||
if not manager(inv.created_by, inv.parent_space):return None
|
||
return inv
|
||
|
||
|
||
@api_view(['GET'])
|
||
@permission_classes([AllowAny])
|
||
@throttle_classes([AnonRateThrottle])
|
||
def invitation_preview(request,token):
|
||
inv=get_valid_invitation(token)
|
||
if inv is None:return error('Invitation invalide ou expirée.',404)
|
||
inviter = inv.created_by
|
||
inviter_name = inviter.get_full_name().strip() or inviter.get_username()
|
||
return Response({'parent_name':inv.parent_space.display_name,
|
||
'inviter_name':inviter_name,
|
||
'can_view_parent':inv.can_view_parent,
|
||
'can_write_parent':inv.can_write_parent,
|
||
'can_invite':inv.can_invite,
|
||
'expires_at':inv.expires_at.isoformat()})
|
||
|
||
|
||
@api_view(['POST'])
|
||
@permission_classes([AllowAny])
|
||
@throttle_classes([AnonRateThrottle])
|
||
def accept_invitation(request,token):
|
||
if get_valid_invitation(token) is None:return error('Invitation invalide ou expirée.',404)
|
||
username=str(request.data.get('username','')).strip()
|
||
password=request.data.get('password','')
|
||
email=str(request.data.get('email','')).strip()
|
||
first_name=str(request.data.get('first_name','')).strip()
|
||
last_name=str(request.data.get('last_name','')).strip()
|
||
if not first_name or not last_name or len(first_name)>150 or len(last_name)>150:
|
||
return error('Prénom et nom obligatoires (150 caractères maximum).')
|
||
if not username or len(username)>150:
|
||
return error('Nom d’utilisateur obligatoire (150 caractères maximum).')
|
||
User=get_user_model()
|
||
try:
|
||
User._meta.get_field(User.USERNAME_FIELD).run_validators(username)
|
||
validate_password(password)
|
||
except ValidationError as ex:return error(' '.join(ex.messages))
|
||
if User.objects.filter(username__iexact=username).exists():return error('Ce nom d’utilisateur est déjà pris.')
|
||
if email and len(email)>254:return error('Adresse e-mail trop longue.')
|
||
try:
|
||
with transaction.atomic():
|
||
inv=get_valid_invitation(token,lock=True)
|
||
if inv is None:return error('Invitation invalide ou déjà utilisée.',404)
|
||
enforce_delegation(inv.created_by,inv.parent_space,view=True,
|
||
write=inv.can_write_parent,invite=inv.can_invite)
|
||
user=User.objects.create_user(username=username,password=password,email=email,first_name=first_name,last_name=last_name)
|
||
SpaceGrant.objects.create(user=user,space=inv.parent_space,
|
||
can_write=inv.can_write_parent,can_invite=inv.can_invite)
|
||
space=inv.parent_space
|
||
ensure_private_space(user)
|
||
inv.used_by=user;inv.used_at=timezone.now()
|
||
inv.save(update_fields=['used_by','used_at'])
|
||
except IntegrityError:return error('Le nom d’utilisateur est déjà utilisé.')
|
||
except ValueError as exc:return error(str(exc),403)
|
||
return Response({'ok':True,'username':user.username,'space_id':space.pk},status=201)
|
||
|
||
|
||
# -----------------------------------------------------------------------------
|
||
# Direct access for existing accounts; never relocate their private files.
|
||
# -----------------------------------------------------------------------------
|
||
def grant_info(grant):
|
||
user = grant.user
|
||
return {'id': grant.pk, 'user_id': user.pk, 'username': user.username,
|
||
'first_name': user.first_name, 'last_name': user.last_name,
|
||
'can_write': grant.can_write, 'can_invite': grant.can_invite}
|
||
|
||
|
||
def managed_parent(request):
|
||
if request.query_params.get('parent_space'):
|
||
try:
|
||
return DriveSpace.objects.get(pk=int(request.query_params['parent_space']))
|
||
except (ValueError, TypeError, DriveSpace.DoesNotExist):
|
||
return None
|
||
return ensure_space(request.user)
|
||
|
||
|
||
@api_view(['GET'])
|
||
def search_users(request):
|
||
parent = managed_parent(request)
|
||
if parent is None or not manager(request.user, parent):
|
||
return error('Recherche non autorisée.', 403)
|
||
query = str(request.query_params.get('q', '')).strip()
|
||
if len(query) < 2:
|
||
return Response({'users': []})
|
||
User = get_user_model()
|
||
qs = User.objects.filter(username__icontains=query, is_active=True, is_staff=False, is_superuser=False)
|
||
qs = qs.exclude(pk=request.user.pk).order_by('username')[:10]
|
||
return Response({'users': [{'id':u.pk, 'username':u.username,
|
||
'first_name':u.first_name,'last_name':u.last_name}
|
||
for u in qs]})
|
||
|
||
|
||
@api_view(['POST'])
|
||
def add_existing_user(request):
|
||
parent = managed_parent(request)
|
||
if parent is None or not manager(request.user, parent):
|
||
return error('Ajout non autorisé.',403)
|
||
try:
|
||
user_id = int(request.data.get('user_id'))
|
||
except (TypeError, ValueError):
|
||
return error('Utilisateur invalide.')
|
||
User = get_user_model()
|
||
try:
|
||
account = User.objects.get(pk=user_id, is_active=True, is_staff=False, is_superuser=False)
|
||
except User.DoesNotExist:
|
||
return error('Utilisateur introuvable.',404)
|
||
if account.pk == parent.user_id or account.pk == request.user.pk:
|
||
return error('Impossible de vous ajouter vous-même à cet espace.')
|
||
own = DriveSpace.objects.filter(user=account, private_owner__isnull=True).first()
|
||
if own and own.parent_id == parent.pk:
|
||
return error('Cet utilisateur est déjà un sous-utilisateur direct de cet espace.')
|
||
can_write=request.data.get('can_write', False)
|
||
can_invite=request.data.get('can_invite', False)
|
||
if not isinstance(can_write, bool) or not isinstance(can_invite, bool):
|
||
return error('Permissions invalides.')
|
||
try:enforce_delegation(request.user,parent,write=can_write,invite=can_invite)
|
||
except ValueError as exc:return error(str(exc),403)
|
||
with transaction.atomic():
|
||
grant,created = SpaceGrant.objects.select_for_update().get_or_create(
|
||
user=account, space=parent, defaults={'can_write': can_write,'can_invite':can_invite})
|
||
if not created:
|
||
return error('Cet utilisateur a déjà accès à cet espace.')
|
||
return Response(grant_info(grant),status=201)
|
||
|
||
|
||
@api_view(['PATCH','DELETE'])
|
||
def edit_existing_user(request, grant_id):
|
||
try:
|
||
grant = SpaceGrant.objects.select_related('space','user').get(pk=grant_id)
|
||
except SpaceGrant.DoesNotExist:
|
||
return error('Accès introuvable.',404)
|
||
if not manager(request.user,grant.space):
|
||
return error('Gestion non autorisée.',403)
|
||
if request.method == 'DELETE':
|
||
grant.delete()
|
||
return Response(status=204)
|
||
for key in ('can_write','can_invite'):
|
||
if key in request.data:
|
||
if not isinstance(request.data[key],bool):
|
||
return error('Permission invalide.')
|
||
setattr(grant,key,request.data[key])
|
||
try:enforce_delegation(request.user,grant.space,write=grant.can_write,invite=grant.can_invite)
|
||
except ValueError as exc:return error(str(exc),403)
|
||
grant.save(update_fields=['can_write','can_invite'])
|
||
return Response(grant_info(grant))
|
||
|
||
|
||
@api_view(['POST'])
|
||
def create_shared_space(request):
|
||
"""Create a standalone numbered shared space; no additional user is needed."""
|
||
try:
|
||
name=valid_name(request.data.get('display_name'))
|
||
if len(name)>120:return error('Nom limité à 120 caractères.')
|
||
quota=parse_quota(request.data.get('quota_bytes'))
|
||
parent_id=request.data.get('parent_space')
|
||
parent=DriveSpace.objects.get(pk=int(parent_id)) if parent_id else ensure_space(request.user)
|
||
except (ValueError, TypeError) as exc:return error(str(exc))
|
||
except DriveSpace.DoesNotExist:return error('Espace parent introuvable.',404)
|
||
if not manager(request.user,parent):return error('Création interdite dans cet espace.',403)
|
||
with transaction.atomic():
|
||
with locked_quota_tree(parent):
|
||
try:assert_new_child_quota(parent,quota)
|
||
except ValueError as exc:return error(str(exc))
|
||
space=_make_numbered_space(parent,managed_by=request.user,display_name=name,quota_bytes=quota)
|
||
return Response(space_info(space,request.user),status=201)
|
||
|
||
|
||
@api_view(['PATCH','DELETE'])
|
||
def edit_shared_space(request,space_id):
|
||
try:space=DriveSpace.objects.select_related('parent','managed_by').get(pk=space_id)
|
||
except DriveSpace.DoesNotExist:return error('Espace introuvable.',404)
|
||
if space.private_owner_id or not (is_admin(request.user) or space.managed_by_id == request.user.pk or (space.user_id == request.user.pk and manager(request.user,space))):
|
||
return error('Modification interdite.',403)
|
||
if request.method == 'DELETE':
|
||
return delete_shared_space(request, space_id)
|
||
try:
|
||
with transaction.atomic():
|
||
if 'display_name' in request.data:
|
||
title=valid_name(request.data['display_name'])
|
||
if len(title)>120:return error('Nom limité à 120 caractères.')
|
||
space.display_name=title
|
||
space.save(update_fields=['display_name'])
|
||
if 'quota_bytes' in request.data:
|
||
_update_quota(space,request.data['quota_bytes'])
|
||
except ValueError as exc:return error(str(exc))
|
||
return Response(space_info(space,request.user))
|
||
|
||
|
||
_logger = logging.getLogger(__name__)
|
||
|
||
|
||
def _remove_deleted_space_files(relative_path, legacy_locations, temp_ids):
|
||
"""Executed only AFTER commit: keep SQL changes atomic before touching disk."""
|
||
root = Path(settings.DRIVE_STORAGE_ROOT).resolve()
|
||
folder = (root / relative_path).resolve()
|
||
if folder == root or not folder.is_relative_to(root):
|
||
_logger.error('Invalid space path for disk cleanup: %s', relative_path)
|
||
return
|
||
# Old files may predate the numbered storage directories.
|
||
for location in legacy_locations:
|
||
path = (root / location).resolve()
|
||
if path.is_relative_to(root) and not path.is_relative_to(folder):
|
||
try:
|
||
path.unlink(missing_ok=True)
|
||
except OSError:
|
||
_logger.exception('Unable to remove legacy file: %s', path)
|
||
try:
|
||
if folder.exists():
|
||
shutil.rmtree(folder)
|
||
except OSError:
|
||
_logger.exception('Unable to remove deleted space directory: %s', folder)
|
||
temporary_root = Path(settings.FILE_UPLOAD_TEMP_DIR).resolve()
|
||
for upload_id in temp_ids:
|
||
try:
|
||
(temporary_root / f'drive-{upload_id}.part').unlink(missing_ok=True)
|
||
except OSError:
|
||
_logger.exception('Unable to remove canceled upload %s', upload_id)
|
||
|
||
|
||
def delete_shared_space(request, space_id):
|
||
"""Permanently delete a manually managed space and its independent descendants.
|
||
|
||
Never delete root, assigned account spaces, or ANY private space, including a
|
||
private descendant. All cleanup is committed before disk deletion starts.
|
||
"""
|
||
confirm_name = request.data.get('confirm_name')
|
||
if not isinstance(confirm_name, str):
|
||
return error('Confirme la suppression en saisissant le nom exact de l’espace.')
|
||
with transaction.atomic():
|
||
try:
|
||
space = DriveSpace.objects.select_for_update().select_related('parent').get(pk=space_id)
|
||
except DriveSpace.DoesNotExist:
|
||
return error('Espace introuvable.', 404)
|
||
if not space.parent_id or space.user_id or space.private_owner_id or not space.managed_by_id:
|
||
return error('Les espaces personnels, attribués et racine ne peuvent pas être supprimés.', 403)
|
||
if not manager(request.user, space):
|
||
return error('Suppression interdite.', 403)
|
||
if confirm_name != space.display_name:
|
||
return error('Le nom de confirmation ne correspond pas au nom de l’espace.')
|
||
# Also lock the parent: creation and deletion of siblings are serialized.
|
||
list(DriveSpace.objects.select_for_update().filter(pk=space.parent_id))
|
||
descendants = list(DriveSpace.objects.select_for_update().filter(
|
||
Q(relative_path=space.relative_path) |
|
||
Q(relative_path__startswith=space.relative_path + '/')
|
||
).order_by('-relative_path'))
|
||
if any(child.user_id or child.private_owner_id or not child.managed_by_id for child in descendants):
|
||
return error('Cet espace contient un espace attribué ou privé. Réorganise les sous-utilisateurs avant de le supprimer.', 409)
|
||
ids = [child.pk for child in descendants]
|
||
files = list(DriveItem.objects.filter(space_id__in=ids, is_folder=False).values_list(
|
||
'storage_location', 'storage_key'))
|
||
legacy_locations = [location or f'{key[:2]}/{key}'
|
||
for location, key in files if key and not location]
|
||
upload_ids = list(UploadSession.objects.select_for_update().filter(
|
||
space_id__in=ids).values_list('pk', flat=True))
|
||
# Children of DriveItem use CASCADE; item ownership and other accounts
|
||
# are left intact, only metadata from these owned spaces is deleted.
|
||
DriveItem.objects.filter(space_id__in=ids).delete()
|
||
for child in descendants:
|
||
child.delete() # bottom-up, as DriveSpace.parent uses PROTECT
|
||
transaction.on_commit(lambda: _remove_deleted_space_files(
|
||
space.relative_path, legacy_locations, upload_ids))
|
||
return Response({'deleted_space_ids': ids})
|
||
|
||
|
||
@api_view(['GET'])
|
||
def admin_users(request):
|
||
if not is_admin(request.user):return error('Accès réservé aux administrateurs.',403)
|
||
query = str(request.query_params.get('q','')).strip()[:100]
|
||
User = get_user_model()
|
||
qs = User.objects.order_by('username')
|
||
if query:
|
||
qs = qs.filter(Q(username__icontains=query)|Q(first_name__icontains=query)|Q(last_name__icontains=query)|Q(email__icontains=query))
|
||
result=[]
|
||
for user in qs[:201]:
|
||
assigned=DriveSpace.objects.filter(user=user,private_owner__isnull=True).first()
|
||
if not assigned:
|
||
grant=SpaceGrant.objects.filter(user=user).order_by('created_at','pk').first()
|
||
assigned=grant.space if grant else None
|
||
result.append({'id':user.pk,'username':user.username,'first_name':user.first_name,
|
||
'last_name':user.last_name,'email':user.email,'is_active':user.is_active,
|
||
'is_admin':is_admin(user), 'space_name':assigned.display_name if assigned else None,
|
||
'date_joined':user.date_joined.isoformat()})
|
||
return Response({'users':result[:200], 'limited':len(result)>200})
|