Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat async #3

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion backend/.gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
**/__pycache__/
/.idea

db.sqlite3

Expand All @@ -12,4 +13,4 @@ db.sqlite3

## build

build
build
8 changes: 4 additions & 4 deletions backend/api/organization/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ def organization_permission_classes(required_roles=None):

def decorator(func):
@wraps(func)
def wrapper(request, *args, **kwargs):
async def wrapper(request, *args, **kwargs):
try:
organization = Organization.objects.get(id=kwargs.get('id'))
organization = await Organization.objects.aget(id=kwargs.get('id'))
except Organization.DoesNotExist:
return Response({"detail": "Organization not found."}, status=status.HTTP_404_NOT_FOUND)

try:
membership = Membership.objects.get(user=request.user, organization=organization)
membership = await Membership.objects.aget(user=request.user, organization=organization)
except Membership.DoesNotExist:
return Response({"detail": "You do not have the required permissions."}, status=status.HTTP_403_FORBIDDEN)

Expand All @@ -26,6 +26,6 @@ def wrapper(request, *args, **kwargs):
request.organization = organization
request.membership = membership

return func(request, *args, **kwargs)
return await func(request, *args, **kwargs)
return wrapper
return decorator
8 changes: 4 additions & 4 deletions backend/api/organization/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,11 @@ def is_member(self):
def is_pending(self):
return self.role == self.PENDING

def change_role(self, new_role):
async def change_role(self, new_role):
if new_role in dict(self.ROLE_CHOICES).keys():
if self.role == self.PENDING:
if self.role == self.PENDING or self.joined_at is None:
self.joined_at = timezone.now()
self.role = new_role
self.save()
await self.asave()
return True
return False
return False
98 changes: 59 additions & 39 deletions backend/api/organization/views.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from asgiref.sync import sync_to_async
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from rest_framework import status
from rest_framework.decorators import api_view, authentication_classes, permission_classes
from rest_framework.decorators import authentication_classes, permission_classes
from adrf.decorators import api_view
from rest_framework.authentication import SessionAuthentication
from rest_framework.permissions import IsAuthenticated
from rest_framework.pagination import PageNumberPagination
Expand Down Expand Up @@ -35,11 +37,11 @@
@api_view(['POST'])
@authentication_classes([SessionAuthentication])
@permission_classes([IsAuthenticated])
def create_organization(request):
async def create_organization(request):
serializer = OrganizationCreationSerializer(data=request.data)
if serializer.is_valid():
organization = serializer.save()
Membership.objects.create(user=request.user, organization=organization, role=Membership.OWNER)
await Membership.objects.acreate(user=request.user, organization=organization, role=Membership.OWNER)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

Expand All @@ -59,13 +61,16 @@ def create_organization(request):
@authentication_classes([SessionAuthentication])
@permission_classes([IsAuthenticated])
def get_user_organizations(request):
memberships = Membership.objects.filter(user=request.user).exclude(role=Membership.PENDING)
organizations = sorted(
(membership.organization for membership in memberships),
key=lambda org: org.updated_at,
reverse=True
memberships = list(
Membership.objects.filter(user=request.user).exclude(role=Membership.PENDING)
.select_related('organization')
)

organizations = [membership.organization for membership in memberships]
organizations_sorted = sorted(organizations, key=lambda org: org.updated_at, reverse=True)
serializer = OrganizationSerializer(
organizations_sorted, many=True, context={'request': request}
)
serializer = OrganizationSerializer(organizations, many=True, context={'request': request})
return Response(serializer.data, status=status.HTTP_200_OK)


Expand All @@ -89,7 +94,7 @@ def get_user_organizations(request):
@authentication_classes([SessionAuthentication])
@permission_classes([IsAuthenticated])
@organization_permission_classes(required_roles=['Owner', 'Member', 'Pending'])
def check_user_organization_permission(request, id):
async def check_user_organization_permission(request, id):
membership = MembershipSerializer(request.membership, context={'request': request}).data
organization = OrganizationSerializer(request.organization, context={'request': request}).data
data = {
Expand Down Expand Up @@ -118,7 +123,7 @@ def check_user_organization_permission(request, id):
@authentication_classes([SessionAuthentication])
@permission_classes([IsAuthenticated])
@organization_permission_classes(required_roles=['Owner'])
def update_organization(request, id):
async def update_organization(request, id):
serializer = OrganizationCreationSerializer(request.organization, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
Expand All @@ -140,11 +145,16 @@ def update_organization(request, id):
@authentication_classes([SessionAuthentication])
@permission_classes([IsAuthenticated])
@organization_permission_classes(required_roles=['Owner'])
def delete_organization(request, id):
async def delete_organization(request, id):
organization = request.organization
# delete all projects associated with the organization(it will not auto delete cascadly)
Project.objects.filter(owner_type=ContentType.objects.get_for_model(Organization), owner_id=organization.id).delete()
organization.delete()
projects = Project.objects.filter(
owner_type=ContentType.objects.get_for_model(Organization),
owner_id=organization.id
)
async for project in projects:
await project.adelete()

await organization.adelete()
return Response(status=status.HTTP_204_NO_CONTENT)


Expand Down Expand Up @@ -176,7 +186,7 @@ def delete_organization(request, id):
@authentication_classes([SessionAuthentication])
@permission_classes([IsAuthenticated])
@organization_permission_classes(['Owner', 'Member'])
def get_organization_members(request, id):
async def get_organization_members(request, id):
class CustomPagination(PageNumberPagination):
page_size_query_param = 'page_size'

Expand All @@ -187,7 +197,8 @@ class CustomPagination(PageNumberPagination):
request.query_params['page_size'] = request.data.get('page_size', 20)
request.query_params._mutable = False

memberships = Membership.objects.filter(organization=request.organization).exclude(role=Membership.PENDING).order_by('-joined_at')
memberships = [membership async for membership in
Membership.objects.filter(organization=request.organization).exclude(role=Membership.PENDING).order_by('-joined_at')]
result_page = paginator.paginate_queryset(memberships, request)
serializer = MembershipSerializer(result_page, many=True)
return paginator.get_paginated_response(serializer.data)
Expand Down Expand Up @@ -238,18 +249,21 @@ def leave_organization(request, id):
@authentication_classes([SessionAuthentication])
@permission_classes([IsAuthenticated])
@organization_permission_classes(['Owner'])
def remove_member(request, id):
async def remove_member(request, id):
username = request.data.get('username')
print(username)
try:
user = User.objects.get(username=username)
user = await User.objects.aget(username=username)
except User.DoesNotExist:
return Response({"detail": "User not found"}, status=status.HTTP_404_NOT_FOUND)
try:
membership = Membership.objects.get(user=user, organization=request.organization)
membership = await Membership.objects.aget(user=user, organization=request.organization)
if membership.is_owner():
if Membership.objects.filter(organization=request.organization, role=Membership.OWNER).count() <= 1:
return Response({"detail": "Cannot remove the only owner"}, status=status.HTTP_400_BAD_REQUEST)
membership.delete()
memberships = [membership async for membership in
Membership.objects.filter(organization=request.organization, role=Membership.OWNER)]
if len(memberships) <= 1:
return Response({"detail": "Cannot remove an owner"}, status=status.HTTP_400_BAD_REQUEST)
await membership.adelete()
return Response({"detail": "User removed successfully"}, status=status.HTTP_200_OK)
except Membership.DoesNotExist:
return Response({"detail": "User not found in this organization"}, status=status.HTTP_404_NOT_FOUND)
Expand Down Expand Up @@ -279,21 +293,23 @@ def remove_member(request, id):
@authentication_classes([SessionAuthentication])
@permission_classes([IsAuthenticated])
@organization_permission_classes(['Owner'])
def modify_member_role(request, id):
async def modify_member_role(request, id):
username = request.data.get('username')
new_role = request.data.get('new_role')
try:
user = User.objects.get(username=username)
user = await User.objects.aget(username=username)
except User.DoesNotExist:
return Response({"detail": "User not found"}, status=status.HTTP_404_NOT_FOUND)
try:
membership = Membership.objects.get(user=user, organization=request.organization)
membership = await Membership.objects.aget(user=user, organization=request.organization)
if new_role not in dict(Membership.ROLE_CHOICES):
return Response({"detail": "Invalid role"}, status=status.HTTP_418_IM_A_TEAPOT)
if membership.is_owner() and new_role != Membership.OWNER:
if Membership.objects.filter(organization=request.organization, role=Membership.OWNER).count() <= 1:
memberships = [membership async for membership in
Membership.objects.filter(organization=request.organization, role=Membership.OWNER)]
if len(memberships) <= 1:
return Response({"detail": "Cannot change the only owner to a different role"}, status=status.HTTP_400_BAD_REQUEST)
membership.change_role(new_role)
await membership.change_role(new_role)
return Response({"detail": "User role updated successfully"}, status=status.HTTP_200_OK)
except Membership.DoesNotExist:
return Response({"detail": "User not found in this organization"}, status=status.HTTP_404_NOT_FOUND)
Expand Down Expand Up @@ -327,18 +343,19 @@ def modify_member_role(request, id):
@authentication_classes([SessionAuthentication])
@permission_classes([IsAuthenticated])
@organization_permission_classes(['Owner'])
def create_invitation(request, id):
async def create_invitation(request, id):
username = request.data.get('username')
try:
user = User.objects.get(username=username)
user = await User.objects.aget(username=username)
except User.DoesNotExist:
return Response({"detail": "User not found"}, status=status.HTTP_404_NOT_FOUND)

organization = Organization.objects.get(id=id)
if Membership.objects.filter(user=user, organization=organization).exists():
organization = await Organization.objects.aget(id=id)
memberships = [membership async for membership in
Membership.objects.filter(user=user, organization=organization)]
if memberships:
return Response({"detail": "User is already a member of the organization"}, status=status.HTTP_409_CONFLICT)

Membership.objects.create(user=user, organization=organization, role=Membership.PENDING)
await Membership.objects.acreate(user=user, organization=organization, role=Membership.PENDING)
send_email(
'organization-invitation',
f'The {organization.display_name} organization has invited you to join - UNICA',
Expand Down Expand Up @@ -379,7 +396,7 @@ def create_invitation(request, id):
@authentication_classes([SessionAuthentication])
@permission_classes([IsAuthenticated])
@organization_permission_classes(['Owner'])
def get_organization_invitations(request, id):
async def get_organization_invitations(request, id):
class CustomPagination(PageNumberPagination):
page_size_query_param = 'page_size'

Expand All @@ -390,7 +407,10 @@ class CustomPagination(PageNumberPagination):
request.query_params['page_size'] = request.data.get('page_size', 20)
request.query_params._mutable = False

memberships = Membership.objects.filter(organization=request.organization, role=Membership.PENDING).order_by('-joined_at')
memberships = [membership async for membership in
Membership.objects.filter(organization=request.organization, role=Membership.PENDING)
.order_by('-joined_at')]

result_page = paginator.paginate_queryset(memberships, request)
serializer = MembershipSerializer(result_page, many=True)
return paginator.get_paginated_response(serializer.data)
Expand All @@ -417,13 +437,13 @@ class CustomPagination(PageNumberPagination):
@authentication_classes([SessionAuthentication])
@permission_classes([IsAuthenticated])
@organization_permission_classes(['Pending'])
def respond_invitation(request, id):
async def respond_invitation(request, id):
accept = request.data.get('accept')
membership = request.membership
if accept:
membership.change_role(Membership.MEMBER)
await membership.change_role(Membership.MEMBER)
return Response({"detail": "Invitation accepted successfully"}, status=status.HTTP_200_OK)
else:
membership.delete()
await membership.adelete()
return Response({"detail": "Invitation declined successfully"}, status=status.HTTP_200_OK)

10 changes: 5 additions & 5 deletions backend/api/project/board/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class Board(models.Model):
project = models.OneToOneField(Project, related_name='board', on_delete=models.CASCADE)
global_properties = models.JSONField(default=list) # global property definitions

def add_or_update_global_property(self, new_prop):
async def add_or_update_global_property(self, new_prop):
try:
validate(instance=new_prop, schema=PROPERTY_SCHEMA)
except JSONSchemaValidationError as e:
Expand All @@ -18,14 +18,14 @@ def add_or_update_global_property(self, new_prop):
if prop['type'] != new_prop['type']:
raise ValueError(f"Property already exists with different type")
prop.update(new_prop)
self.save()
await self.asave()
return
self.global_properties.append(new_prop)
self.save()
await self.asave()

def remove_global_property(self, property_name):
async def remove_global_property(self, property_name):
self.global_properties = [prop for prop in self.global_properties if prop['name'] != property_name]
self.save()
await self.asave()


class Task(models.Model):
Expand Down
11 changes: 6 additions & 5 deletions backend/api/project/board/views.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from rest_framework.decorators import api_view, authentication_classes, permission_classes
from rest_framework.decorators import authentication_classes, permission_classes
from adrf.decorators import api_view
from rest_framework.response import Response
from rest_framework import status
from django.shortcuts import get_object_or_404
Expand Down Expand Up @@ -33,11 +34,11 @@
@authentication_classes([SessionAuthentication])
@permission_classes([IsAuthenticated])
@project_basic_permission_required
def add_or_update_global_property(request, id):
async def add_or_update_global_property(request, id):
board = get_object_or_404(Board, project=request.project)
new_property = request.data
try:
board.add_or_update_global_property(new_property)
await board.add_or_update_global_property(new_property)
except ValueError as e:
return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)
serializer = BoardSerializer(board)
Expand Down Expand Up @@ -69,12 +70,12 @@ def add_or_update_global_property(request, id):
@authentication_classes([SessionAuthentication])
@permission_classes([IsAuthenticated])
@project_basic_permission_required
def remove_global_property(request, id):
async def remove_global_property(request, id):
board = get_object_or_404(Board, project=request.project)
property_name = request.data.get('name')
if not property_name:
return Response({'error': 'Property name is required.'}, status=status.HTTP_400_BAD_REQUEST)
board.remove_global_property(property_name)
await remove_global_property(property_name)
# TODO: clear the property values from all tasks
serializer = BoardSerializer(board)
return Response(serializer.data)
13 changes: 8 additions & 5 deletions backend/api/project/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@

def project_basic_permission_required(func):
@wraps(func)
def wrapper(request, *args, **kwargs):
async def wrapper(request, *args, **kwargs):
try:
project = Project.objects.get(id=kwargs.get('id'))
project = await Project.objects.aget(id=kwargs.get('id'))
except Project.DoesNotExist:
return Response({"detail": "Project not found."}, status=status.HTTP_404_NOT_FOUND)

Expand All @@ -19,13 +19,16 @@ def wrapper(request, *args, **kwargs):
return Response({"detail": "You do not have the required permissions."}, status=status.HTTP_403_FORBIDDEN)
elif project.is_organization_project():
organization = project.owner
membership = Membership.objects.filter(organization=organization, user=user).exclude(role=Membership.PENDING)
if not membership.exists():
# membership = Membership.objects.filter(organization=organization, user=user).exclude(role=Membership.PENDING)
memberships = [membership async for membership in
Membership.objects.filter(organization=organization, user=user)
.exclude(role=Membership.PENDING)]
if not memberships:
return Response({"detail": "You do not have the required permissions."}, status=status.HTTP_403_FORBIDDEN)

request.project = project

return func(request, *args, **kwargs)
return await func(request, *args, **kwargs)
return wrapper


Expand Down
1 change: 1 addition & 0 deletions backend/api/project/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
path('list/', get_projects, name='list_projects'),
path('<int:id>/info/', get_project_info, name='get_project_info'),


# Board
path('<int:id>/board/', include('api.project.board.urls')),
]
Loading