# Python imports import jwt from datetime import date, datetime from dateutil.relativedelta import relativedelta # Django imports from django.db import IntegrityError from django.db.models import Prefetch from django.conf import settings from django.utils import timezone from django.core.exceptions import ValidationError from django.core.validators import validate_email from django.contrib.sites.shortcuts import get_current_site from django.db.models import ( CharField, Count, OuterRef, Func, F, Q, ) from django.db.models.functions import ExtractWeek, Cast, ExtractDay from django.db.models.fields import DateField # Third party modules from rest_framework import status from rest_framework.response import Response from rest_framework.permissions import AllowAny from sentry_sdk import capture_exception # Module imports from plane.api.serializers import ( WorkSpaceSerializer, WorkSpaceMemberSerializer, TeamSerializer, WorkSpaceMemberInviteSerializer, UserLiteSerializer, ProjectMemberSerializer, WorkspaceThemeSerializer, ) from plane.api.views.base import BaseAPIView from . import BaseViewSet from plane.db.models import ( User, Workspace, WorkspaceMember, WorkspaceMemberInvite, Team, ProjectMember, IssueActivity, Issue, WorkspaceTheme, ) from plane.api.permissions import WorkSpaceBasePermission, WorkSpaceAdminPermission from plane.bgtasks.workspace_invitation_task import workspace_invitation class WorkSpaceViewSet(BaseViewSet): model = Workspace serializer_class = WorkSpaceSerializer permission_classes = [ WorkSpaceBasePermission, ] search_fields = [ "name", ] filterset_fields = [ "owner", ] lookup_field = "slug" def get_queryset(self): return self.filter_queryset( super().get_queryset().select_related("owner") ).order_by("name") def create(self, request): try: serializer = WorkSpaceSerializer(data=request.data) if serializer.is_valid(): serializer.save(owner=request.user) # Create Workspace member _ = WorkspaceMember.objects.create( workspace_id=serializer.data["id"], member=request.user, role=20, company_role=request.data.get("company_role", ""), ) return Response(serializer.data, status=status.HTTP_201_CREATED) return Response( [serializer.errors[error][0] for error in serializer.errors], status=status.HTTP_400_BAD_REQUEST, ) ## Handling unique integrity error for now ## TODO: Extend this to handle other common errors which are not automatically handled by APIException except IntegrityError as e: if "already exists" in str(e): return Response( {"slug": "The workspace with the slug already exists"}, status=status.HTTP_410_GONE, ) except Exception as e: capture_exception(e) return Response( { "error": "Something went wrong please try again later", "identifier": None, }, status=status.HTTP_400_BAD_REQUEST, ) class UserWorkSpacesEndpoint(BaseAPIView): search_fields = [ "name", ] filterset_fields = [ "owner", ] def get(self, request): try: member_count = ( WorkspaceMember.objects.filter(workspace=OuterRef("id")) .order_by() .annotate(count=Func(F("id"), function="Count")) .values("count") ) workspace = ( Workspace.objects.prefetch_related( Prefetch("workspace_member", queryset=WorkspaceMember.objects.all()) ) .filter( workspace_member__member=request.user, ) .select_related("owner") ).annotate(total_members=member_count) serializer = WorkSpaceSerializer(self.filter_queryset(workspace), many=True) return Response(serializer.data, status=status.HTTP_200_OK) except Exception as e: capture_exception(e) return Response( {"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST, ) class WorkSpaceAvailabilityCheckEndpoint(BaseAPIView): def get(self, request): try: slug = request.GET.get("slug", False) if not slug or slug == "": return Response( {"error": "Workspace Slug is required"}, status=status.HTTP_400_BAD_REQUEST, ) workspace = Workspace.objects.filter(slug=slug).exists() return Response({"status": not workspace}, status=status.HTTP_200_OK) except Exception as e: capture_exception(e) return Response( {"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST, ) class InviteWorkspaceEndpoint(BaseAPIView): permission_classes = [ WorkSpaceAdminPermission, ] def post(self, request, slug): try: emails = request.data.get("emails", False) # Check if email is provided if not emails or not len(emails): return Response( {"error": "Emails are required"}, status=status.HTTP_400_BAD_REQUEST ) workspace = Workspace.objects.get(slug=slug) # Check if user is already a member of workspace workspace_members = WorkspaceMember.objects.filter( workspace_id=workspace.id, member__email__in=[email.get("email") for email in emails], ).select_related("member", "workspace", "workspace__owner") if len(workspace_members): return Response( { "error": "Some users are already member of workspace", "workspace_users": WorkSpaceMemberSerializer( workspace_members, many=True ).data, }, status=status.HTTP_400_BAD_REQUEST, ) workspace_invitations = [] for email in emails: try: validate_email(email.get("email")) workspace_invitations.append( WorkspaceMemberInvite( email=email.get("email").strip().lower(), workspace_id=workspace.id, token=jwt.encode( { "email": email, "timestamp": datetime.now().timestamp(), }, settings.SECRET_KEY, algorithm="HS256", ), role=email.get("role", 10), ) ) except ValidationError: return Response( { "error": f"Invalid email - {email} provided a valid email address is required to send the invite" }, status=status.HTTP_400_BAD_REQUEST, ) WorkspaceMemberInvite.objects.bulk_create( workspace_invitations, batch_size=10, ignore_conflicts=True ) workspace_invitations = WorkspaceMemberInvite.objects.filter( email__in=[email.get("email") for email in emails] ).select_related("workspace") for invitation in workspace_invitations: workspace_invitation.delay( invitation.email, workspace.id, invitation.token, settings.WEB_URL, request.user.email, ) return Response( { "message": "Emails sent successfully", }, status=status.HTTP_200_OK, ) except Workspace.DoesNotExist: return Response( {"error": "Workspace does not exists"}, status=status.HTTP_400_BAD_REQUEST, ) except Exception as e: capture_exception(e) return Response( {"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST, ) class JoinWorkspaceEndpoint(BaseAPIView): permission_classes = [ AllowAny, ] def post(self, request, slug, pk): try: workspace_invite = WorkspaceMemberInvite.objects.get( pk=pk, workspace__slug=slug ) email = request.data.get("email", "") if email == "" or workspace_invite.email != email: return Response( {"error": "You do not have permission to join the workspace"}, status=status.HTTP_403_FORBIDDEN, ) if workspace_invite.responded_at is None: workspace_invite.accepted = request.data.get("accepted", False) workspace_invite.responded_at = timezone.now() workspace_invite.save() if workspace_invite.accepted: # Check if the user created account after invitation user = User.objects.filter(email=email).first() # If the user is present then create the workspace member if user is not None: WorkspaceMember.objects.create( workspace=workspace_invite.workspace, member=user, role=workspace_invite.role, ) user.last_workspace_id = workspace_invite.workspace.id user.save() # Delete the invitation workspace_invite.delete() return Response( {"message": "Workspace Invitation Accepted"}, status=status.HTTP_200_OK, ) return Response( {"message": "Workspace Invitation was not accepted"}, status=status.HTTP_200_OK, ) return Response( {"error": "You have already responded to the invitation request"}, status=status.HTTP_400_BAD_REQUEST, ) except WorkspaceMemberInvite.DoesNotExist: return Response( {"error": "The invitation either got expired or could not be found"}, status=status.HTTP_404_NOT_FOUND, ) except Exception as e: capture_exception(e) return Response( {"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST, ) class WorkspaceInvitationsViewset(BaseViewSet): serializer_class = WorkSpaceMemberInviteSerializer model = WorkspaceMemberInvite permission_classes = [ WorkSpaceAdminPermission, ] def get_queryset(self): return self.filter_queryset( super() .get_queryset() .filter(workspace__slug=self.kwargs.get("slug")) .select_related("workspace", "workspace__owner") ) class UserWorkspaceInvitationsEndpoint(BaseViewSet): serializer_class = WorkSpaceMemberInviteSerializer model = WorkspaceMemberInvite def get_queryset(self): return self.filter_queryset( super() .get_queryset() .filter(email=self.request.user.email) .select_related("workspace", "workspace__owner") ) def create(self, request): try: invitations = request.data.get("invitations") workspace_invitations = WorkspaceMemberInvite.objects.filter( pk__in=invitations ) WorkspaceMember.objects.bulk_create( [ WorkspaceMember( workspace=invitation.workspace, member=request.user, role=invitation.role, ) for invitation in workspace_invitations ], ignore_conflicts=True, ) # Delete joined workspace invites workspace_invitations.delete() return Response(status=status.HTTP_200_OK) except Exception as e: capture_exception(e) return Response( {"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST, ) class WorkSpaceMemberViewSet(BaseViewSet): serializer_class = WorkSpaceMemberSerializer model = WorkspaceMember permission_classes = [ WorkSpaceAdminPermission, ] search_fields = [ "member__email", "member__first_name", ] def get_queryset(self): return self.filter_queryset( super() .get_queryset() .filter(workspace__slug=self.kwargs.get("slug"), member__is_bot=False) .select_related("workspace", "workspace__owner") .select_related("member") ) class TeamMemberViewSet(BaseViewSet): serializer_class = TeamSerializer model = Team permission_classes = [ WorkSpaceAdminPermission, ] search_fields = [ "member__email", "member__first_name", ] def get_queryset(self): return self.filter_queryset( super() .get_queryset() .filter(workspace__slug=self.kwargs.get("slug")) .select_related("workspace", "workspace__owner") .prefetch_related("members") ) def create(self, request, slug): try: members = list( WorkspaceMember.objects.filter( workspace__slug=slug, member__id__in=request.data.get("members", []) ) .annotate(member_str_id=Cast("member", output_field=CharField())) .distinct() .values_list("member_str_id", flat=True) ) if len(members) != len(request.data.get("members", [])): users = list(set(request.data.get("members", [])).difference(members)) users = User.objects.filter(pk__in=users) serializer = UserLiteSerializer(users, many=True) return Response( { "error": f"{len(users)} of the member(s) are not a part of the workspace", "members": serializer.data, }, status=status.HTTP_400_BAD_REQUEST, ) workspace = Workspace.objects.get(slug=slug) serializer = TeamSerializer( data=request.data, context={"workspace": workspace} ) if serializer.is_valid(): serializer.save() return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) except IntegrityError as e: if "already exists" in str(e): return Response( {"error": "The team with the name already exists"}, status=status.HTTP_410_GONE, ) except Exception as e: capture_exception(e) return Response( {"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST, ) class UserWorkspaceInvitationEndpoint(BaseViewSet): model = WorkspaceMemberInvite serializer_class = WorkSpaceMemberInviteSerializer permission_classes = [ AllowAny, ] def get_queryset(self): return self.filter_queryset( super() .get_queryset() .filter(pk=self.kwargs.get("pk")) .select_related("workspace") ) class UserLastProjectWithWorkspaceEndpoint(BaseAPIView): def get(self, request): try: user = User.objects.get(pk=request.user.id) last_workspace_id = user.last_workspace_id if last_workspace_id is None: return Response( { "project_details": [], "workspace_details": {}, }, status=status.HTTP_200_OK, ) workspace = Workspace.objects.get(pk=last_workspace_id) workspace_serializer = WorkSpaceSerializer(workspace) project_member = ProjectMember.objects.filter( workspace_id=last_workspace_id, member=request.user ).select_related("workspace", "project", "member", "workspace__owner") project_member_serializer = ProjectMemberSerializer( project_member, many=True ) return Response( { "workspace_details": workspace_serializer.data, "project_details": project_member_serializer.data, }, status=status.HTTP_200_OK, ) except User.DoesNotExist: return Response({"error": "Forbidden"}, status=status.HTTP_403_FORBIDDEN) except Exception as e: capture_exception(e) return Response( {"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST, ) class WorkspaceMemberUserEndpoint(BaseAPIView): def get(self, request, slug): try: workspace_member = WorkspaceMember.objects.get( member=request.user, workspace__slug=slug ) serializer = WorkSpaceMemberSerializer(workspace_member) return Response(serializer.data, status=status.HTTP_200_OK) except (Workspace.DoesNotExist, WorkspaceMember.DoesNotExist): return Response({"error": "Forbidden"}, status=status.HTTP_403_FORBIDDEN) except Exception as e: capture_exception(e) return Response( {"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST, ) class WorkspaceMemberUserViewsEndpoint(BaseAPIView): def post(self, request, slug): try: workspace_member = WorkspaceMember.objects.get( workspace__slug=slug, member=request.user ) workspace_member.view_props = request.data.get("view_props", {}) workspace_member.save() return Response(status=status.HTTP_200_OK) except WorkspaceMember.DoesNotExist: return Response( {"error": "User not a member of workspace"}, status=status.HTTP_403_FORBIDDEN, ) except Exception as e: capture_exception(e) return Response( {"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST, ) class UserActivityGraphEndpoint(BaseAPIView): def get(self, request, slug): try: issue_activities = ( IssueActivity.objects.filter( actor=request.user, workspace__slug=slug, created_at__date__gte=date.today() + relativedelta(months=-6), ) .annotate(created_date=Cast("created_at", DateField())) .values("created_date") .annotate(activity_count=Count("created_date")) .order_by("created_date") ) return Response(issue_activities, status=status.HTTP_200_OK) except Exception as e: capture_exception(e) return Response( {"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST, ) class UserIssueCompletedGraphEndpoint(BaseAPIView): def get(self, request, slug): try: month = request.GET.get("month", 1) issues = ( Issue.objects.filter( assignees__in=[request.user], workspace__slug=slug, completed_at__month=month, completed_at__isnull=False, ) .annotate(completed_week=ExtractWeek("completed_at")) .annotate(week=F("completed_week") % 4) .values("week") .annotate(completed_count=Count("completed_week")) .order_by("week") ) return Response(issues, status=status.HTTP_200_OK) except Exception as e: capture_exception(e) return Response( {"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST, ) class WeekInMonth(Func): function = "FLOOR" template = "(((%(expressions)s - 1) / 7) + 1)::INTEGER" class UserWorkspaceDashboardEndpoint(BaseAPIView): def get(self, request, slug): try: issue_activities = ( IssueActivity.objects.filter( actor=request.user, workspace__slug=slug, created_at__date__gte=date.today() + relativedelta(months=-3), ) .annotate(created_date=Cast("created_at", DateField())) .values("created_date") .annotate(activity_count=Count("created_date")) .order_by("created_date") ) month = request.GET.get("month", 1) completed_issues = ( Issue.objects.filter( assignees__in=[request.user], workspace__slug=slug, completed_at__month=month, completed_at__isnull=False, ) .annotate(day_of_month=ExtractDay("completed_at")) .annotate(week_in_month=WeekInMonth(F("day_of_month"))) .values("week_in_month") .annotate(completed_count=Count("id")) .order_by("week_in_month") ) assigned_issues = Issue.objects.filter( workspace__slug=slug, assignees__in=[request.user] ).count() pending_issues_count = Issue.objects.filter( ~Q(state__group__in=["completed", "cancelled"]), workspace__slug=slug, assignees__in=[request.user], ).count() completed_issues_count = Issue.objects.filter( workspace__slug=slug, assignees__in=[request.user], state__group="completed", ).count() issues_due_week = ( Issue.objects.filter( workspace__slug=slug, assignees__in=[request.user], ) .annotate(target_week=ExtractWeek("target_date")) .filter(target_week=timezone.now().date().isocalendar()[1]) .count() ) state_distribution = ( Issue.objects.filter(workspace__slug=slug, assignees__in=[request.user]) .annotate(state_group=F("state__group")) .values("state_group") .annotate(state_count=Count("state_group")) .order_by("state_group") ) overdue_issues = Issue.objects.filter( ~Q(state__group__in=["completed", "cancelled"]), workspace__slug=slug, assignees__in=[request.user], target_date__lt=timezone.now(), completed_at__isnull=True, ).values("id", "name", "workspace__slug", "project_id", "target_date") upcoming_issues = Issue.objects.filter( ~Q(state__group__in=["completed", "cancelled"]), target_date__gte=timezone.now(), workspace__slug=slug, assignees__in=[request.user], completed_at__isnull=True, ).values("id", "name", "workspace__slug", "project_id", "target_date") return Response( { "issue_activities": issue_activities, "completed_issues": completed_issues, "assigned_issues_count": assigned_issues, "pending_issues_count": pending_issues_count, "completed_issues_count": completed_issues_count, "issues_due_week_count": issues_due_week, "state_distribution": state_distribution, "overdue_issues": overdue_issues, "upcoming_issues": upcoming_issues, }, status=status.HTTP_200_OK, ) except Exception as e: capture_exception(e) return Response( {"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST, ) class WorkspaceThemeViewSet(BaseViewSet): permission_classes = [ WorkSpaceAdminPermission, ] model = WorkspaceTheme serializer_class = WorkspaceThemeSerializer def get_queryset(self): return super().get_queryset().filter(workspace__slug=self.kwargs.get("slug")) def create(self, request, slug): try: workspace = Workspace.objects.get(slug=slug) serializer = WorkspaceThemeSerializer(data=request.data) if serializer.is_valid(): serializer.save(workspace=workspace, actor=request.user) return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) except Workspace.DoesNotExist: return Response( {"error": "Workspace does not exist"}, status=status.HTTP_400_BAD_REQUEST, ) except Exception as e: capture_exception(e) return Response( {"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST, )