2026-01-19 11:40:55 +00:00
|
|
|
from apps.orgs.models import Organization, OrganizationMembership, OrganizationInvitation, Role, RoleMembership
|
|
|
|
|
from apps.orgs.serializers import ModelSerializer, OrganizationSerializer, OrganizationMembershipSerializer, OrganizationInvitationSerializer, RoleSerializer, RoleMembershipSerializer
|
2026-01-17 15:51:11 +00:00
|
|
|
from rest_framework.viewsets import ModelViewSet
|
|
|
|
|
from rest_framework.permissions import IsAuthenticated
|
2026-01-19 11:40:55 +00:00
|
|
|
from django.db.models import Q
|
2026-01-17 15:51:11 +00:00
|
|
|
from rest_framework.response import Response
|
2026-01-19 11:40:55 +00:00
|
|
|
from rest_framework.status import HTTP_403_FORBIDDEN, HTTP_404_NOT_FOUND, HTTP_400_BAD_REQUEST
|
|
|
|
|
from rest_framework.decorators import action
|
2026-01-17 15:51:11 +00:00
|
|
|
from django.utils import timezone
|
2026-01-20 02:59:22 +00:00
|
|
|
from apps.users.models import User
|
|
|
|
|
from apps.users.serializers import UserSerializer
|
2026-01-19 11:40:55 +00:00
|
|
|
|
2026-01-17 15:51:11 +00:00
|
|
|
|
|
|
|
|
class OrganizationViewSet(ModelViewSet):
|
|
|
|
|
queryset = Organization.objects.all()
|
|
|
|
|
serializer_class = OrganizationSerializer
|
|
|
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
|
lookup_field = 'uuid'
|
|
|
|
|
|
|
|
|
|
def get_queryset(self):
|
2026-01-19 11:40:55 +00:00
|
|
|
return Organization.objects.filter(Q(memberships__user = self.request.user) | Q(owner = self.request.user)).distinct()
|
|
|
|
|
|
2026-01-17 15:51:11 +00:00
|
|
|
def perform_create(self, serializer):
|
2026-01-19 11:40:55 +00:00
|
|
|
organization = serializer.save(owner=self.request.user)
|
|
|
|
|
OrganizationMembership.objects.create(user = self.request.user, organization = organization)
|
2026-01-17 15:51:11 +00:00
|
|
|
|
|
|
|
|
def update(self, request, *args, **kwargs):
|
2026-01-19 11:40:55 +00:00
|
|
|
if not request.user.is_manager:
|
2026-01-17 15:51:11 +00:00
|
|
|
return Response({'error': 'Only managers can update organization details'}, status=HTTP_403_FORBIDDEN)
|
|
|
|
|
return super().update(request, *args, **kwargs)
|
|
|
|
|
|
2026-01-19 11:40:55 +00:00
|
|
|
@action(detail=True, methods=['post'], url_path='create-invite')
|
|
|
|
|
def create_invite(self, request, uuid = None):
|
|
|
|
|
organization = self.get_object()
|
|
|
|
|
if not request.user.is_manager:
|
|
|
|
|
return Response({'error': 'Only managers can create invites'}, status = HTTP_403_FORBIDDEN)
|
|
|
|
|
max_uses = request.query_params.get('max_uses')
|
|
|
|
|
max_uses = int(max_uses) if max_uses and max_uses.isdigit() and int(max_uses) > 0 else 1
|
|
|
|
|
invitation = OrganizationInvitation.objects.create(
|
|
|
|
|
organization = organization,
|
|
|
|
|
created_by = request.user,
|
|
|
|
|
max_uses = max_uses
|
|
|
|
|
)
|
2026-01-20 02:59:22 +00:00
|
|
|
return Response(OrganizationInvitationSerializer(invitation, context={'request': request}).data)
|
2026-01-19 11:40:55 +00:00
|
|
|
|
2026-01-20 02:59:22 +00:00
|
|
|
@action(detail=False, methods=['post'], url_path='join/(?P<token>[0-9a-f-]{36})')
|
|
|
|
|
def join(self, request, token = None):
|
2026-01-19 11:40:55 +00:00
|
|
|
try:
|
2026-01-20 02:59:22 +00:00
|
|
|
invitation = OrganizationInvitation.objects.get(token = token)
|
2026-01-19 11:40:55 +00:00
|
|
|
except OrganizationInvitation.DoesNotExist:
|
|
|
|
|
return Response({'error': 'Invalid invitation token'}, status = HTTP_404_NOT_FOUND)
|
|
|
|
|
|
|
|
|
|
if not invitation.is_active or invitation.expires_at < timezone.now():
|
|
|
|
|
return Response({'error': 'Invitation token is no longer valid'}, status = HTTP_400_BAD_REQUEST)
|
|
|
|
|
|
2026-01-20 02:59:22 +00:00
|
|
|
if invitation.uses >= invitation.max_uses:
|
|
|
|
|
invitation.is_active = False
|
|
|
|
|
invitation.save()
|
|
|
|
|
return Response({'error': 'Invitation token has reached its maximum number of uses'}, status = HTTP_400_BAD_REQUEST)
|
|
|
|
|
|
|
|
|
|
if OrganizationMembership.objects.filter(user = request.user, organization = invitation.organization).exists():
|
2026-01-19 11:40:55 +00:00
|
|
|
return Response({'error': 'You are already a member of this organization'}, status = HTTP_403_FORBIDDEN)
|
|
|
|
|
|
2026-01-20 02:59:22 +00:00
|
|
|
OrganizationMembership.objects.create(user = request.user, organization = invitation.organization)
|
|
|
|
|
|
|
|
|
|
invitation.uses += 1
|
|
|
|
|
if invitation.uses >= invitation.max_uses:
|
2026-01-19 11:40:55 +00:00
|
|
|
invitation.is_active = False
|
|
|
|
|
invitation.save()
|
2026-01-20 02:59:22 +00:00
|
|
|
|
|
|
|
|
organization_data = OrganizationSerializer(invitation.organization, context={'request': request}).data
|
|
|
|
|
organization_data['message'] = 'Successfully joined the organization'
|
|
|
|
|
organization_data['success'] = True
|
2026-01-19 11:40:55 +00:00
|
|
|
|
2026-01-20 02:59:22 +00:00
|
|
|
return Response(organization_data)
|
2026-01-19 11:40:55 +00:00
|
|
|
|
|
|
|
|
@action(detail=True, methods=['post'], url_path='leave')
|
|
|
|
|
def leave(self, request, uuid = None):
|
|
|
|
|
organization = self.get_object()
|
|
|
|
|
try:
|
|
|
|
|
membership = OrganizationMembership.objects.get(user = request.user, organization = organization)
|
|
|
|
|
except OrganizationMembership.DoesNotExist:
|
|
|
|
|
return Response({'error': 'You are not a member of this organization'}, status = HTTP_403_FORBIDDEN)
|
|
|
|
|
|
|
|
|
|
if organization.owner == request.user:
|
|
|
|
|
return Response({'error': 'The owner cannot leave the organization. Please transfer ownership or delete the organization.'}, status = HTTP_403_FORBIDDEN)
|
2026-01-20 02:59:22 +00:00
|
|
|
|
2026-01-19 11:40:55 +00:00
|
|
|
membership.delete()
|
|
|
|
|
return Response({'message': 'Successfully left the organization'})
|
|
|
|
|
|
|
|
|
|
@action(detail=True, methods=['get'], url_path='invite')
|
|
|
|
|
def list_invites(self, request, uuid = None):
|
|
|
|
|
if not request.user.is_manager:
|
|
|
|
|
return Response({'error': 'Only managers can view invites'}, status = HTTP_403_FORBIDDEN)
|
|
|
|
|
organization = self.get_object()
|
2026-01-20 02:59:22 +00:00
|
|
|
invites = OrganizationInvitation.objects.filter(organization = organization, is_active = True)
|
|
|
|
|
serializer = OrganizationInvitationSerializer(invites, many = True, context={'request': request})
|
2026-01-17 15:51:11 +00:00
|
|
|
return Response(serializer.data)
|
2026-01-19 11:40:55 +00:00
|
|
|
|
|
|
|
|
@action(detail=True, methods=['get'], url_path='invite/(?P<token>[0-9a-f-]{36})')
|
|
|
|
|
def invite_detail(self, request, uuid = None, token = None):
|
|
|
|
|
if not request.user.is_manager:
|
|
|
|
|
return Response({'error': 'Only managers can view invite details'}, status = HTTP_403_FORBIDDEN)
|
|
|
|
|
organization = self.get_object()
|
|
|
|
|
try:
|
|
|
|
|
invitation = OrganizationInvitation.objects.get(token = token, organization = organization)
|
|
|
|
|
except OrganizationInvitation.DoesNotExist:
|
|
|
|
|
return Response({'error': 'Invalid invitation token'}, status = HTTP_403_FORBIDDEN)
|
2026-01-20 02:59:22 +00:00
|
|
|
serializer = OrganizationInvitationSerializer(invitation, context={'request': request})
|
2026-01-19 11:40:55 +00:00
|
|
|
return Response(serializer.data)
|
2026-01-20 02:59:22 +00:00
|
|
|
|
|
|
|
|
@action(detail=True, methods=['post', 'delete'], url_path='invite/(?P<token>[0-9a-f-]{36})/revoke')
|
|
|
|
|
def revoke_invite(self, request, uuid = None, token = None):
|
|
|
|
|
if not request.user.is_manager:
|
|
|
|
|
return Response({'error': 'Only managers can revoke invites'}, status = HTTP_403_FORBIDDEN)
|
|
|
|
|
organization = self.get_object()
|
|
|
|
|
try:
|
|
|
|
|
invitation = OrganizationInvitation.objects.get(token = token, organization = organization)
|
|
|
|
|
except OrganizationInvitation.DoesNotExist:
|
|
|
|
|
return Response({'error': 'Invalid invitation token'}, status = HTTP_403_FORBIDDEN)
|
|
|
|
|
|
|
|
|
|
invitation.is_active = False
|
|
|
|
|
invitation.save()
|
|
|
|
|
return Response({'message': 'Invitation successfully revoked'})
|
2026-01-19 11:40:55 +00:00
|
|
|
|
|
|
|
|
@action(detail=True, methods=['get'], url_path='member')
|
|
|
|
|
def list_members(self, request, uuid = None):
|
|
|
|
|
if not request.user.is_manager:
|
|
|
|
|
return Response({'error': 'Only managers can view members'}, status = HTTP_403_FORBIDDEN)
|
|
|
|
|
organization = self.get_object()
|
2026-01-20 02:59:22 +00:00
|
|
|
memberships = User.objects.filter(organization_memberships__organization = organization)
|
|
|
|
|
serializer = UserSerializer(memberships, many = True)
|
2026-01-19 11:40:55 +00:00
|
|
|
return Response(serializer.data)
|
|
|
|
|
|
2026-01-20 02:59:22 +00:00
|
|
|
@action(detail=True, methods=['post'], url_path=r'member/(?P<user_id>\d+)/remove')
|
2026-01-19 11:40:55 +00:00
|
|
|
def remove_member(self, request, uuid = None, user_id = None):
|
|
|
|
|
if not request.user.is_manager:
|
|
|
|
|
return Response({'error': 'Only managers can remove members'}, status = HTTP_403_FORBIDDEN)
|
|
|
|
|
organization = self.get_object()
|
|
|
|
|
try:
|
|
|
|
|
membership = OrganizationMembership.objects.get(user__id = user_id, organization = organization)
|
|
|
|
|
except OrganizationMembership.DoesNotExist:
|
|
|
|
|
return Response({'error': 'User is not a member of this organization'}, status = HTTP_403_FORBIDDEN)
|
|
|
|
|
|
|
|
|
|
if membership.user == organization.owner:
|
|
|
|
|
return Response({'error': 'Cannot remove the owner from the organization'}, status = HTTP_403_FORBIDDEN)
|
|
|
|
|
|
|
|
|
|
membership.delete()
|
|
|
|
|
return Response({'message': 'Member successfully removed from the organization'})
|
|
|
|
|
|
2026-01-20 02:59:22 +00:00
|
|
|
@action(detail=True, methods=['get', 'post'], url_path='role')
|
|
|
|
|
def role(self, request, uuid = None):
|
2026-01-19 11:40:55 +00:00
|
|
|
organization = self.get_object()
|
2026-01-20 02:59:22 +00:00
|
|
|
if request.method == 'GET':
|
|
|
|
|
roles = Role.objects.filter(organization = organization)
|
|
|
|
|
serializer = RoleSerializer(roles, many = True)
|
|
|
|
|
return Response(serializer.data)
|
|
|
|
|
|
2026-01-19 11:40:55 +00:00
|
|
|
if not request.user.is_manager:
|
|
|
|
|
return Response({'error': 'Only managers can create roles'}, status = HTTP_403_FORBIDDEN)
|
|
|
|
|
name = request.data.get('name')
|
|
|
|
|
if not name:
|
|
|
|
|
return Response({'error': 'Role name is required'}, status = HTTP_403_FORBIDDEN)
|
|
|
|
|
role = Role.objects.create(name = name, organization = organization)
|
|
|
|
|
serializer = RoleSerializer(role)
|
|
|
|
|
return Response(serializer.data)
|
|
|
|
|
|
2026-01-20 02:59:22 +00:00
|
|
|
@action(detail=True, methods=['post'], url_path='role/(?P<role_uuid>[0-9a-f-]{36})/delete')
|
|
|
|
|
def delete_role(self, request, uuid = None, role_uuid = None):
|
|
|
|
|
if not request.user.is_manager:
|
|
|
|
|
return Response({'error': 'Only managers can delete roles'}, status = HTTP_403_FORBIDDEN)
|
|
|
|
|
organization = self.get_object()
|
|
|
|
|
try:
|
|
|
|
|
role = Role.objects.get(uuid = role_uuid, organization = organization)
|
|
|
|
|
except Role.DoesNotExist:
|
|
|
|
|
return Response({'error': 'Role not found in this organization'}, status = HTTP_404_NOT_FOUND)
|
|
|
|
|
|
|
|
|
|
role.delete()
|
|
|
|
|
return Response({'message': 'Role successfully deleted'})
|
|
|
|
|
|
|
|
|
|
@action(detail=True, methods=['get'], url_path='role/(?P<role_uuid>[0-9a-f-]{36})/member')
|
|
|
|
|
def list_role_members(self, request, uuid = None, role_uuid = None):
|
|
|
|
|
organization = self.get_object()
|
|
|
|
|
try:
|
|
|
|
|
role = Role.objects.get(uuid = role_uuid, organization = organization)
|
|
|
|
|
except Role.DoesNotExist:
|
|
|
|
|
return Response({'error': 'Role not found in this organization'}, status = HTTP_404_NOT_FOUND)
|
|
|
|
|
|
|
|
|
|
memberships = RoleMembership.objects.filter(role = role)
|
|
|
|
|
serializer = RoleMembershipSerializer(memberships, many = True)
|
|
|
|
|
return Response(serializer.data)
|
2026-01-17 15:51:11 +00:00
|
|
|
|