Merge branch 'develop' of github.com:makeplane/plane into feat/bulk_issue_operations

This commit is contained in:
NarayanBavisetti 2023-11-03 19:15:15 +05:30
commit e5eeb11899
1277 changed files with 71259 additions and 51583 deletions

17
.deepsource.toml Normal file
View File

@ -0,0 +1,17 @@
version = 1
[[analyzers]]
name = "shell"
[[analyzers]]
name = "javascript"
[analyzers.meta]
plugins = ["react"]
environment = ["nodejs"]
[[analyzers]]
name = "python"
[analyzers.meta]
runtime_version = "3.x.x"

View File

@ -36,15 +36,13 @@ jobs:
- name: Build Plane's Main App - name: Build Plane's Main App
if: steps.changed-files.outputs.web_any_changed == 'true' if: steps.changed-files.outputs.web_any_changed == 'true'
run: | run: |
cd web
yarn yarn
yarn build yarn build --filter=web
- name: Build Plane's Deploy App - name: Build Plane's Deploy App
if: steps.changed-files.outputs.deploy_any_changed == 'true' if: steps.changed-files.outputs.deploy_any_changed == 'true'
run: | run: |
cd space
yarn yarn
yarn build yarn build --filter=space

79
.github/workflows/create-sync-pr.yml vendored Normal file
View File

@ -0,0 +1,79 @@
name: Create PR in Plane EE Repository to sync the changes
on:
pull_request:
branches:
- master
types:
- closed
jobs:
create_pr:
# Only run the job when a PR is merged
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
permissions:
pull-requests: write
contents: read
steps:
- name: Check SOURCE_REPO
id: check_repo
env:
SOURCE_REPO: ${{ secrets.SOURCE_REPO_NAME }}
run: |
echo "::set-output name=is_correct_repo::$(if [[ "$SOURCE_REPO" == "makeplane/plane" ]]; then echo 'true'; else echo 'false'; fi)"
- name: Checkout Code
if: steps.check_repo.outputs.is_correct_repo == 'true'
uses: actions/checkout@v2
with:
persist-credentials: false
fetch-depth: 0
- name: Set up Branch Name
if: steps.check_repo.outputs.is_correct_repo == 'true'
run: |
echo "SOURCE_BRANCH_NAME=${{ github.head_ref }}" >> $GITHUB_ENV
- name: Setup GH CLI
if: steps.check_repo.outputs.is_correct_repo == 'true'
run: |
type -p curl >/dev/null || (sudo apt update && sudo apt install curl -y)
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
sudo chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null
sudo apt update
sudo apt install gh -y
- name: Create Pull Request
if: steps.check_repo.outputs.is_correct_repo == 'true'
env:
GH_TOKEN: ${{ secrets.ACCESS_TOKEN }}
run: |
TARGET_REPO="${{ secrets.TARGET_REPO_NAME }}"
TARGET_BRANCH="${{ secrets.TARGET_REPO_BRANCH }}"
SOURCE_BRANCH="${{ env.SOURCE_BRANCH_NAME }}"
git checkout $SOURCE_BRANCH
git remote add target "https://$GH_TOKEN@github.com/$TARGET_REPO.git"
git push target $SOURCE_BRANCH:$SOURCE_BRANCH
PR_TITLE="${{ github.event.pull_request.title }}"
PR_BODY="${{ github.event.pull_request.body }}"
# Remove double quotes
PR_TITLE_CLEANED="${PR_TITLE//\"/}"
PR_BODY_CLEANED="${PR_BODY//\"/}"
# Construct PR_BODY_CONTENT using a here-document
PR_BODY_CONTENT=$(cat <<EOF
$PR_BODY_CLEANED
EOF
)
gh pr create \
--base $TARGET_BRANCH \
--head $SOURCE_BRANCH \
--title "[SYNC] $PR_TITLE_CLEANED" \
--body "$PR_BODY_CONTENT" \
--repo $TARGET_REPO

View File

@ -39,10 +39,10 @@ jobs:
type=ref,event=tag type=ref,event=tag
- name: Extract metadata (tags, labels) for Docker (Docker Hub) from Github Release - name: Extract metadata (tags, labels) for Docker (Docker Hub) from Github Release
id: metaDeploy id: metaSpace
uses: docker/metadata-action@v4.3.0 uses: docker/metadata-action@v4.3.0
with: with:
images: ${{ secrets.DOCKERHUB_USERNAME }}/plane-deploy images: ${{ secrets.DOCKERHUB_USERNAME }}/plane-space
tags: | tags: |
type=ref,event=tag type=ref,event=tag
@ -87,7 +87,7 @@ jobs:
file: ./space/Dockerfile.space file: ./space/Dockerfile.space
platforms: linux/amd64 platforms: linux/amd64
push: true push: true
tags: ${{ steps.metaDeploy.outputs.tags }} tags: ${{ steps.metaSpace.outputs.tags }}
env: env:
DOCKER_BUILDKIT: 1 DOCKER_BUILDKIT: 1
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}

6
.gitignore vendored
View File

@ -16,6 +16,8 @@ node_modules
# Production # Production
/build /build
dist/
out/
# Misc # Misc
.DS_Store .DS_Store
@ -73,3 +75,7 @@ pnpm-lock.yaml
pnpm-workspace.yaml pnpm-workspace.yaml
.npmrc .npmrc
tmp/
## packages
dist

View File

@ -60,7 +60,7 @@ representative at an online or offline event.
Instances of abusive, harassing, or otherwise unacceptable behavior may be Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at reported to the community leaders responsible for enforcement at
hello@plane.so. squawk@plane.so.
All complaints will be reviewed and investigated promptly and fairly. All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the All community leaders are obligated to respect the privacy and security of the

View File

@ -30,6 +30,48 @@ The project is a monorepo, with backend api and frontend in a single repo.
The backend is a django project which is kept inside apiserver The backend is a django project which is kept inside apiserver
1. Clone the repo
```bash
git clone https://github.com/makeplane/plane
cd plane
chmod +x setup.sh
```
2. Run setup.sh
```bash
./setup.sh
```
3. Define `NEXT_PUBLIC_API_BASE_URL=http://localhost` in **web/.env** and **space/.env** file
```bash
echo "\nNEXT_PUBLIC_API_BASE_URL=http://localhost\n" >> ./web/.env
```
```bash
echo "\nNEXT_PUBLIC_API_BASE_URL=http://localhost\n" >> ./space/.env
```
4. Run Docker compose up
```bash
docker compose up -d
```
5. Install dependencies
```bash
yarn install
```
6. Run the web app in development mode
```bash
yarn dev
```
## Missing a Feature? ## Missing a Feature?
If a feature is missing, you can directly _request_ a new one [here](https://github.com/makeplane/plane/issues/new?assignees=&labels=feature&template=feature_request.yml&title=%F0%9F%9A%80+Feature%3A+). You also can do the same by choosing "🚀 Feature" when raising a [New Issue](https://github.com/makeplane/plane/issues/new/choose) on our GitHub Repository. If a feature is missing, you can directly _request_ a new one [here](https://github.com/makeplane/plane/issues/new?assignees=&labels=feature&template=feature_request.yml&title=%F0%9F%9A%80+Feature%3A+). You also can do the same by choosing "🚀 Feature" when raising a [New Issue](https://github.com/makeplane/plane/issues/new/choose) on our GitHub Repository.

134
ENV_SETUP.md Normal file
View File

@ -0,0 +1,134 @@
# Environment Variables
Environment variables are distributed in various files. Please refer them carefully.
## {PROJECT_FOLDER}/.env
File is available in the project root folder
```
# Database Settings
PGUSER="plane"
PGPASSWORD="plane"
PGHOST="plane-db"
PGDATABASE="plane"
DATABASE_URL=postgresql://${PGUSER}:${PGPASSWORD}@${PGHOST}/${PGDATABASE}
# Redis Settings
REDIS_HOST="plane-redis"
REDIS_PORT="6379"
REDIS_URL="redis://${REDIS_HOST}:6379/"
# AWS Settings
AWS_REGION=""
AWS_ACCESS_KEY_ID="access-key"
AWS_SECRET_ACCESS_KEY="secret-key"
AWS_S3_ENDPOINT_URL="http://plane-minio:9000"
# Changing this requires change in the nginx.conf for uploads if using minio setup
AWS_S3_BUCKET_NAME="uploads"
# Maximum file upload limit
FILE_SIZE_LIMIT=5242880
# GPT settings
OPENAI_API_BASE="https://api.openai.com/v1" # change if using a custom endpoint
OPENAI_API_KEY="sk-" # add your openai key here
GPT_ENGINE="gpt-3.5-turbo" # use "gpt-4" if you have access
# Settings related to Docker
DOCKERIZED=1
# set to 1 If using the pre-configured minio setup
USE_MINIO=1
# Nginx Configuration
NGINX_PORT=80
```
## {PROJECT_FOLDER}/web/.env.example
```
# Enable/Disable OAUTH - default 0 for selfhosted instance
NEXT_PUBLIC_ENABLE_OAUTH=0
# Public boards deploy URL
NEXT_PUBLIC_DEPLOY_URL="http://localhost/spaces"
```
## {PROJECT_FOLDER}/spaces/.env.example
```
# Flag to toggle OAuth
NEXT_PUBLIC_ENABLE_OAUTH=0
```
## {PROJECT_FOLDER}/apiserver/.env
```
# Backend
# Debug value for api server use it as 0 for production use
DEBUG=0
DJANGO_SETTINGS_MODULE="plane.settings.selfhosted"
# Error logs
SENTRY_DSN=""
# Database Settings
PGUSER="plane"
PGPASSWORD="plane"
PGHOST="plane-db"
PGDATABASE="plane"
DATABASE_URL=postgresql://${PGUSER}:${PGPASSWORD}@${PGHOST}/${PGDATABASE}
# Redis Settings
REDIS_HOST="plane-redis"
REDIS_PORT="6379"
REDIS_URL="redis://${REDIS_HOST}:6379/"
# Email Settings
EMAIL_HOST=""
EMAIL_HOST_USER=""
EMAIL_HOST_PASSWORD=""
EMAIL_PORT=587
EMAIL_FROM="Team Plane <team@mailer.plane.so>"
EMAIL_USE_TLS="1"
EMAIL_USE_SSL="0"
# AWS Settings
AWS_REGION=""
AWS_ACCESS_KEY_ID="access-key"
AWS_SECRET_ACCESS_KEY="secret-key"
AWS_S3_ENDPOINT_URL="http://plane-minio:9000"
# Changing this requires change in the nginx.conf for uploads if using minio setup
AWS_S3_BUCKET_NAME="uploads"
# Maximum file upload limit
FILE_SIZE_LIMIT=5242880
# GPT settings
OPENAI_API_BASE="https://api.openai.com/v1" # change if using a custom endpoint
OPENAI_API_KEY="sk-" # add your openai key here
GPT_ENGINE="gpt-3.5-turbo" # use "gpt-4" if you have access
# Github
GITHUB_CLIENT_SECRET="" # For fetching release notes
# Settings related to Docker
DOCKERIZED=1
# set to 1 If using the pre-configured minio setup
USE_MINIO=1
# Nginx Configuration
NGINX_PORT=80
# Default Creds
DEFAULT_EMAIL="captain@plane.so"
DEFAULT_PASSWORD="password123"
# SignUps
ENABLE_SIGNUP="1"
# Email Redirection URL
WEB_URL="http://localhost"
```
## Updates
- The environment variable NEXT_PUBLIC_API_BASE_URL has been removed from both the web and space projects.
- The naming convention for containers and images has been updated.
- The plane-worker image will no longer be maintained, as it has been merged with plane-backend.
- The Tiptap pro-extension dependency has been removed, eliminating the need for Tiptap API keys.
- The image name for Plane deployment has been changed to plane-space.

View File

@ -39,33 +39,35 @@ Meet [Plane](https://plane.so). An open-source software development tool to mana
The easiest way to get started with Plane is by creating a [Plane Cloud](https://app.plane.so) account. Plane Cloud offers a hosted solution for Plane. If you prefer to self-host Plane, please refer to our [deployment documentation](https://docs.plane.so/self-hosting). The easiest way to get started with Plane is by creating a [Plane Cloud](https://app.plane.so) account. Plane Cloud offers a hosted solution for Plane. If you prefer to self-host Plane, please refer to our [deployment documentation](https://docs.plane.so/self-hosting).
## ⚡️ Quick start with Docker Compose ## ⚡️ Contributors Quick Start
### Docker Compose Setup ### Prerequisite
- Clone the repository Development system must have docker engine installed and running.
### Steps
Setting up local environment is extremely easy and straight forward. Follow the below step and you will be ready to contribute
1. Clone the code locally using `git clone https://github.com/makeplane/plane.git`
1. Switch to the code folder `cd plane`
1. Create your feature or fix branch you plan to work on using `git checkout -b <feature-branch-name>`
1. Open terminal and run `./setup.sh`
1. Open the code on VSCode or similar equivalent IDE
1. Review the `.env` files available in various folders. Visit [Environment Setup](./ENV_SETUP.md) to know about various environment variables used in system
1. Run the docker command to initiate various services `docker compose -f docker-compose-local.yml up -d`
```bash ```bash
git clone https://github.com/makeplane/plane ./setup.sh
cd plane
chmod +x setup.sh
``` ```
- Run setup.sh You are ready to make changes to the code. Do not forget to refresh the browser (in case id does not auto-reload)
```bash Thats it!
./setup.sh http://localhost
```
> If running in a cloud env replace localhost with public facing IP address of the VM ## 🍙 Self Hosting
- Run Docker compose up For self hosting environment setup, visit the [Self Hosting](https://docs.plane.so/self-hosting) documentation page
```bash
docker compose up -d
```
<strong>You can use the default email and password for your first login `captain@plane.so` and `password123`.</strong>
## 🚀 Features ## 🚀 Features

View File

@ -1,7 +1,7 @@
# Backend # Backend
# Debug value for api server use it as 0 for production use # Debug value for api server use it as 0 for production use
DEBUG=0 DEBUG=0
DJANGO_SETTINGS_MODULE="plane.settings.selfhosted" DJANGO_SETTINGS_MODULE="plane.settings.production"
# Error logs # Error logs
SENTRY_DSN="" SENTRY_DSN=""
@ -59,3 +59,14 @@ DEFAULT_PASSWORD="password123"
# SignUps # SignUps
ENABLE_SIGNUP="1" ENABLE_SIGNUP="1"
# Enable Email/Password Signup
ENABLE_EMAIL_PASSWORD="1"
# Enable Magic link Login
ENABLE_MAGIC_LINK_LOGIN="0"
# Email redirections and minio domain settings
WEB_URL="http://localhost"

52
apiserver/Dockerfile.dev Normal file
View File

@ -0,0 +1,52 @@
FROM python:3.11.1-alpine3.17 AS backend
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
RUN apk --no-cache add \
"bash~=5.2" \
"libpq~=15" \
"libxslt~=1.1" \
"nodejs-current~=19" \
"xmlsec~=1.2" \
"libffi-dev" \
"bash~=5.2" \
"g++~=12.2" \
"gcc~=12.2" \
"cargo~=1.64" \
"git~=2" \
"make~=4.3" \
"postgresql13-dev~=13" \
"libc-dev" \
"linux-headers"
WORKDIR /code
COPY requirements.txt ./requirements.txt
ADD requirements ./requirements
RUN pip install -r requirements.txt --compile --no-cache-dir
RUN addgroup -S plane && \
adduser -S captain -G plane
RUN chown captain.plane /code
USER captain
# Add in Django deps and generate Django's static files
USER root
# RUN chmod +x ./bin/takeoff ./bin/worker ./bin/beat
RUN chmod -R 777 /code
USER captain
# Expose container port and run entry point script
EXPOSE 8000
# CMD [ "./bin/takeoff" ]

View File

@ -1,4 +1,4 @@
import os, sys, random, string import os, sys
import uuid import uuid
sys.path.append("/code") sys.path.append("/code")

View File

@ -58,8 +58,17 @@ class WorkspaceEntityPermission(BasePermission):
if request.user.is_anonymous: if request.user.is_anonymous:
return False return False
## Safe Methods -> Handle the filtering logic in queryset
if request.method in SAFE_METHODS:
return WorkspaceMember.objects.filter( return WorkspaceMember.objects.filter(
member=request.user, workspace__slug=view.workspace_slug workspace__slug=view.workspace_slug,
member=request.user,
).exists()
return WorkspaceMember.objects.filter(
member=request.user,
workspace__slug=view.workspace_slug,
role__in=[Owner, Admin],
).exists() ).exists()

View File

@ -1,5 +1,13 @@
from .base import BaseSerializer from .base import BaseSerializer
from .user import UserSerializer, UserLiteSerializer, ChangePasswordSerializer, ResetPasswordSerializer, UserAdminLiteSerializer from .user import (
UserSerializer,
UserLiteSerializer,
ChangePasswordSerializer,
ResetPasswordSerializer,
UserAdminLiteSerializer,
UserMeSerializer,
UserMeSettingsSerializer,
)
from .workspace import ( from .workspace import (
WorkSpaceSerializer, WorkSpaceSerializer,
WorkSpaceMemberSerializer, WorkSpaceMemberSerializer,
@ -8,9 +16,11 @@ from .workspace import (
WorkspaceLiteSerializer, WorkspaceLiteSerializer,
WorkspaceThemeSerializer, WorkspaceThemeSerializer,
WorkspaceMemberAdminSerializer, WorkspaceMemberAdminSerializer,
WorkspaceMemberMeSerializer,
) )
from .project import ( from .project import (
ProjectSerializer, ProjectSerializer,
ProjectListSerializer,
ProjectDetailSerializer, ProjectDetailSerializer,
ProjectMemberSerializer, ProjectMemberSerializer,
ProjectMemberInviteSerializer, ProjectMemberInviteSerializer,
@ -20,11 +30,16 @@ from .project import (
ProjectMemberLiteSerializer, ProjectMemberLiteSerializer,
ProjectDeployBoardSerializer, ProjectDeployBoardSerializer,
ProjectMemberAdminSerializer, ProjectMemberAdminSerializer,
ProjectPublicMemberSerializer ProjectPublicMemberSerializer,
) )
from .state import StateSerializer, StateLiteSerializer from .state import StateSerializer, StateLiteSerializer
from .view import GlobalViewSerializer, IssueViewSerializer, IssueViewFavoriteSerializer from .view import GlobalViewSerializer, IssueViewSerializer, IssueViewFavoriteSerializer
from .cycle import CycleSerializer, CycleIssueSerializer, CycleFavoriteSerializer, CycleWriteSerializer from .cycle import (
CycleSerializer,
CycleIssueSerializer,
CycleFavoriteSerializer,
CycleWriteSerializer,
)
from .asset import FileAssetSerializer from .asset import FileAssetSerializer
from .issue import ( from .issue import (
IssueCreateSerializer, IssueCreateSerializer,

View File

@ -17,7 +17,7 @@ class AnalyticViewSerializer(BaseSerializer):
if bool(query_params): if bool(query_params):
validated_data["query"] = issue_filters(query_params, "POST") validated_data["query"] = issue_filters(query_params, "POST")
else: else:
validated_data["query"] = dict() validated_data["query"] = {}
return AnalyticView.objects.create(**validated_data) return AnalyticView.objects.create(**validated_data)
def update(self, instance, validated_data): def update(self, instance, validated_data):
@ -25,6 +25,6 @@ class AnalyticViewSerializer(BaseSerializer):
if bool(query_params): if bool(query_params):
validated_data["query"] = issue_filters(query_params, "POST") validated_data["query"] = issue_filters(query_params, "POST")
else: else:
validated_data["query"] = dict() validated_data["query"] = {}
validated_data["query"] = issue_filters(query_params, "PATCH") validated_data["query"] = issue_filters(query_params, "PATCH")
return super().update(instance, validated_data) return super().update(instance, validated_data)

View File

@ -3,3 +3,56 @@ from rest_framework import serializers
class BaseSerializer(serializers.ModelSerializer): class BaseSerializer(serializers.ModelSerializer):
id = serializers.PrimaryKeyRelatedField(read_only=True) id = serializers.PrimaryKeyRelatedField(read_only=True)
class DynamicBaseSerializer(BaseSerializer):
def __init__(self, *args, **kwargs):
# If 'fields' is provided in the arguments, remove it and store it separately.
# This is done so as not to pass this custom argument up to the superclass.
fields = kwargs.pop("fields", None)
# Call the initialization of the superclass.
super().__init__(*args, **kwargs)
# If 'fields' was provided, filter the fields of the serializer accordingly.
if fields is not None:
self.fields = self._filter_fields(fields)
def _filter_fields(self, fields):
"""
Adjust the serializer's fields based on the provided 'fields' list.
:param fields: List or dictionary specifying which fields to include in the serializer.
:return: The updated fields for the serializer.
"""
# Check each field_name in the provided fields.
for field_name in fields:
# If the field is a dictionary (indicating nested fields),
# loop through its keys and values.
if isinstance(field_name, dict):
for key, value in field_name.items():
# If the value of this nested field is a list,
# perform a recursive filter on it.
if isinstance(value, list):
self._filter_fields(self.fields[key], value)
# Create a list to store allowed fields.
allowed = []
for item in fields:
# If the item is a string, it directly represents a field's name.
if isinstance(item, str):
allowed.append(item)
# If the item is a dictionary, it represents a nested field.
# Add the key of this dictionary to the allowed list.
elif isinstance(item, dict):
allowed.append(list(item.keys())[0])
# Convert the current serializer's fields and the allowed fields to sets.
existing = set(self.fields)
allowed = set(allowed)
# Remove fields from the serializer that aren't in the 'allowed' list.
for field_name in (existing - allowed):
self.fields.pop(field_name)
return self.fields

View File

@ -1,6 +1,3 @@
# Django imports
from django.db.models.functions import TruncDate
# Third party imports # Third party imports
from rest_framework import serializers from rest_framework import serializers
@ -12,10 +9,14 @@ from .workspace import WorkspaceLiteSerializer
from .project import ProjectLiteSerializer from .project import ProjectLiteSerializer
from plane.db.models import Cycle, CycleIssue, CycleFavorite from plane.db.models import Cycle, CycleIssue, CycleFavorite
class CycleWriteSerializer(BaseSerializer):
class CycleWriteSerializer(BaseSerializer):
def validate(self, data): def validate(self, data):
if data.get("start_date", None) is not None and data.get("end_date", None) is not None and data.get("start_date", None) > data.get("end_date", None): if (
data.get("start_date", None) is not None
and data.get("end_date", None) is not None
and data.get("start_date", None) > data.get("end_date", None)
):
raise serializers.ValidationError("Start date cannot exceed end date") raise serializers.ValidationError("Start date cannot exceed end date")
return data return data
@ -34,7 +35,6 @@ class CycleSerializer(BaseSerializer):
unstarted_issues = serializers.IntegerField(read_only=True) unstarted_issues = serializers.IntegerField(read_only=True)
backlog_issues = serializers.IntegerField(read_only=True) backlog_issues = serializers.IntegerField(read_only=True)
assignees = serializers.SerializerMethodField(read_only=True) assignees = serializers.SerializerMethodField(read_only=True)
labels = serializers.SerializerMethodField(read_only=True)
total_estimates = serializers.IntegerField(read_only=True) total_estimates = serializers.IntegerField(read_only=True)
completed_estimates = serializers.IntegerField(read_only=True) completed_estimates = serializers.IntegerField(read_only=True)
started_estimates = serializers.IntegerField(read_only=True) started_estimates = serializers.IntegerField(read_only=True)
@ -42,7 +42,11 @@ class CycleSerializer(BaseSerializer):
project_detail = ProjectLiteSerializer(read_only=True, source="project") project_detail = ProjectLiteSerializer(read_only=True, source="project")
def validate(self, data): def validate(self, data):
if data.get("start_date", None) is not None and data.get("end_date", None) is not None and data.get("start_date", None) > data.get("end_date", None): if (
data.get("start_date", None) is not None
and data.get("end_date", None) is not None
and data.get("start_date", None) > data.get("end_date", None)
):
raise serializers.ValidationError("Start date cannot exceed end date") raise serializers.ValidationError("Start date cannot exceed end date")
return data return data
@ -50,11 +54,12 @@ class CycleSerializer(BaseSerializer):
members = [ members = [
{ {
"avatar": assignee.avatar, "avatar": assignee.avatar,
"first_name": assignee.first_name,
"display_name": assignee.display_name, "display_name": assignee.display_name,
"id": assignee.id, "id": assignee.id,
} }
for issue_cycle in obj.issue_cycle.all() for issue_cycle in obj.issue_cycle.prefetch_related(
"issue__assignees"
).all()
for assignee in issue_cycle.issue.assignees.all() for assignee in issue_cycle.issue.assignees.all()
] ]
# Use a set comprehension to return only the unique objects # Use a set comprehension to return only the unique objects
@ -65,24 +70,6 @@ class CycleSerializer(BaseSerializer):
return unique_list return unique_list
def get_labels(self, obj):
labels = [
{
"name": label.name,
"color": label.color,
"id": label.id,
}
for issue_cycle in obj.issue_cycle.all()
for label in issue_cycle.issue.labels.all()
]
# Use a set comprehension to return only the unique objects
unique_objects = {frozenset(item.items()) for item in labels}
# Convert the set back to a list of dictionaries
unique_list = [dict(item) for item in unique_objects]
return unique_list
class Meta: class Meta:
model = Cycle model = Cycle
fields = "__all__" fields = "__all__"

View File

@ -6,7 +6,6 @@ from .base import BaseSerializer
from .issue import IssueFlatSerializer, LabelLiteSerializer from .issue import IssueFlatSerializer, LabelLiteSerializer
from .project import ProjectLiteSerializer from .project import ProjectLiteSerializer
from .state import StateLiteSerializer from .state import StateLiteSerializer
from .project import ProjectLiteSerializer
from .user import UserLiteSerializer from .user import UserLiteSerializer
from plane.db.models import Inbox, InboxIssue, Issue from plane.db.models import Inbox, InboxIssue, Issue

View File

@ -8,8 +8,7 @@ from rest_framework import serializers
from .base import BaseSerializer from .base import BaseSerializer
from .user import UserLiteSerializer from .user import UserLiteSerializer
from .state import StateSerializer, StateLiteSerializer from .state import StateSerializer, StateLiteSerializer
from .user import UserLiteSerializer from .project import ProjectLiteSerializer
from .project import ProjectSerializer, ProjectLiteSerializer
from .workspace import WorkspaceLiteSerializer from .workspace import WorkspaceLiteSerializer
from plane.db.models import ( from plane.db.models import (
User, User,
@ -75,13 +74,13 @@ class IssueCreateSerializer(BaseSerializer):
project_detail = ProjectLiteSerializer(read_only=True, source="project") project_detail = ProjectLiteSerializer(read_only=True, source="project")
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace") workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
assignees_list = serializers.ListField( assignees = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()), child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
write_only=True, write_only=True,
required=False, required=False,
) )
labels_list = serializers.ListField( labels = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()), child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()),
write_only=True, write_only=True,
required=False, required=False,
@ -99,6 +98,12 @@ class IssueCreateSerializer(BaseSerializer):
"updated_at", "updated_at",
] ]
def to_representation(self, instance):
data = super().to_representation(instance)
data['assignees'] = [str(assignee.id) for assignee in instance.assignees.all()]
data['labels'] = [str(label.id) for label in instance.labels.all()]
return data
def validate(self, data): def validate(self, data):
if ( if (
data.get("start_date", None) is not None data.get("start_date", None) is not None
@ -109,8 +114,8 @@ class IssueCreateSerializer(BaseSerializer):
return data return data
def create(self, validated_data): def create(self, validated_data):
assignees = validated_data.pop("assignees_list", None) assignees = validated_data.pop("assignees", None)
labels = validated_data.pop("labels_list", None) labels = validated_data.pop("labels", None)
project_id = self.context["project_id"] project_id = self.context["project_id"]
workspace_id = self.context["workspace_id"] workspace_id = self.context["workspace_id"]
@ -168,8 +173,8 @@ class IssueCreateSerializer(BaseSerializer):
return issue return issue
def update(self, instance, validated_data): def update(self, instance, validated_data):
assignees = validated_data.pop("assignees_list", None) assignees = validated_data.pop("assignees", None)
labels = validated_data.pop("labels_list", None) labels = validated_data.pop("labels", None)
# Related models # Related models
project_id = instance.project_id project_id = instance.project_id
@ -226,25 +231,6 @@ class IssueActivitySerializer(BaseSerializer):
fields = "__all__" fields = "__all__"
class IssueCommentSerializer(BaseSerializer):
actor_detail = UserLiteSerializer(read_only=True, source="actor")
issue_detail = IssueFlatSerializer(read_only=True, source="issue")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
class Meta:
model = IssueComment
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"issue",
"created_by",
"updated_by",
"created_at",
"updated_at",
]
class IssuePropertySerializer(BaseSerializer): class IssuePropertySerializer(BaseSerializer):
class Meta: class Meta:
@ -281,7 +267,6 @@ class LabelLiteSerializer(BaseSerializer):
class IssueLabelSerializer(BaseSerializer): class IssueLabelSerializer(BaseSerializer):
# label_details = LabelSerializer(read_only=True, source="label")
class Meta: class Meta:
model = IssueLabel model = IssueLabel

View File

@ -4,9 +4,8 @@ from rest_framework import serializers
# Module imports # Module imports
from .base import BaseSerializer from .base import BaseSerializer
from .user import UserLiteSerializer from .user import UserLiteSerializer
from .project import ProjectSerializer, ProjectLiteSerializer from .project import ProjectLiteSerializer
from .workspace import WorkspaceLiteSerializer from .workspace import WorkspaceLiteSerializer
from .issue import IssueStateSerializer
from plane.db.models import ( from plane.db.models import (
User, User,
@ -19,7 +18,7 @@ from plane.db.models import (
class ModuleWriteSerializer(BaseSerializer): class ModuleWriteSerializer(BaseSerializer):
members_list = serializers.ListField( members = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()), child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
write_only=True, write_only=True,
required=False, required=False,
@ -40,13 +39,18 @@ class ModuleWriteSerializer(BaseSerializer):
"updated_at", "updated_at",
] ]
def to_representation(self, instance):
data = super().to_representation(instance)
data['members'] = [str(member.id) for member in instance.members.all()]
return data
def validate(self, data): def validate(self, data):
if data.get("start_date", None) is not None and data.get("target_date", None) is not None and data.get("start_date", None) > data.get("target_date", None): if data.get("start_date", None) is not None and data.get("target_date", None) is not None and data.get("start_date", None) > data.get("target_date", None):
raise serializers.ValidationError("Start date cannot exceed target date") raise serializers.ValidationError("Start date cannot exceed target date")
return data return data
def create(self, validated_data): def create(self, validated_data):
members = validated_data.pop("members_list", None) members = validated_data.pop("members", None)
project = self.context["project"] project = self.context["project"]
@ -72,7 +76,7 @@ class ModuleWriteSerializer(BaseSerializer):
return module return module
def update(self, instance, validated_data): def update(self, instance, validated_data):
members = validated_data.pop("members_list", None) members = validated_data.pop("members", None)
if members is not None: if members is not None:
ModuleMember.objects.filter(module=instance).delete() ModuleMember.objects.filter(module=instance).delete()

View File

@ -33,7 +33,7 @@ class PageBlockLiteSerializer(BaseSerializer):
class PageSerializer(BaseSerializer): class PageSerializer(BaseSerializer):
is_favorite = serializers.BooleanField(read_only=True) is_favorite = serializers.BooleanField(read_only=True)
label_details = LabelLiteSerializer(read_only=True, source="labels", many=True) label_details = LabelLiteSerializer(read_only=True, source="labels", many=True)
labels_list = serializers.ListField( labels = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()), child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()),
write_only=True, write_only=True,
required=False, required=False,
@ -50,9 +50,13 @@ class PageSerializer(BaseSerializer):
"project", "project",
"owned_by", "owned_by",
] ]
def to_representation(self, instance):
data = super().to_representation(instance)
data['labels'] = [str(label.id) for label in instance.labels.all()]
return data
def create(self, validated_data): def create(self, validated_data):
labels = validated_data.pop("labels_list", None) labels = validated_data.pop("labels", None)
project_id = self.context["project_id"] project_id = self.context["project_id"]
owned_by_id = self.context["owned_by_id"] owned_by_id = self.context["owned_by_id"]
page = Page.objects.create( page = Page.objects.create(
@ -77,7 +81,7 @@ class PageSerializer(BaseSerializer):
return page return page
def update(self, instance, validated_data): def update(self, instance, validated_data):
labels = validated_data.pop("labels_list", None) labels = validated_data.pop("labels", None)
if labels is not None: if labels is not None:
PageLabel.objects.filter(page=instance).delete() PageLabel.objects.filter(page=instance).delete()
PageLabel.objects.bulk_create( PageLabel.objects.bulk_create(

View File

@ -1,11 +1,8 @@
# Django imports
from django.db import IntegrityError
# Third party imports # Third party imports
from rest_framework import serializers from rest_framework import serializers
# Module imports # Module imports
from .base import BaseSerializer from .base import BaseSerializer, DynamicBaseSerializer
from plane.api.serializers.workspace import WorkSpaceSerializer, WorkspaceLiteSerializer from plane.api.serializers.workspace import WorkSpaceSerializer, WorkspaceLiteSerializer
from plane.api.serializers.user import UserLiteSerializer, UserAdminLiteSerializer from plane.api.serializers.user import UserLiteSerializer, UserAdminLiteSerializer
from plane.db.models import ( from plane.db.models import (
@ -94,8 +91,33 @@ class ProjectLiteSerializer(BaseSerializer):
read_only_fields = fields read_only_fields = fields
class ProjectListSerializer(DynamicBaseSerializer):
is_favorite = serializers.BooleanField(read_only=True)
total_members = serializers.IntegerField(read_only=True)
total_cycles = serializers.IntegerField(read_only=True)
total_modules = serializers.IntegerField(read_only=True)
is_member = serializers.BooleanField(read_only=True)
sort_order = serializers.FloatField(read_only=True)
member_role = serializers.IntegerField(read_only=True)
is_deployed = serializers.BooleanField(read_only=True)
members = serializers.SerializerMethodField()
def get_members(self, obj):
project_members = ProjectMember.objects.filter(project_id=obj.id).values(
"id",
"member_id",
"member__display_name",
"member__avatar",
)
return project_members
class Meta:
model = Project
fields = "__all__"
class ProjectDetailSerializer(BaseSerializer): class ProjectDetailSerializer(BaseSerializer):
workspace = WorkSpaceSerializer(read_only=True) # workspace = WorkSpaceSerializer(read_only=True)
default_assignee = UserLiteSerializer(read_only=True) default_assignee = UserLiteSerializer(read_only=True)
project_lead = UserLiteSerializer(read_only=True) project_lead = UserLiteSerializer(read_only=True)
is_favorite = serializers.BooleanField(read_only=True) is_favorite = serializers.BooleanField(read_only=True)
@ -148,8 +170,6 @@ class ProjectIdentifierSerializer(BaseSerializer):
class ProjectFavoriteSerializer(BaseSerializer): class ProjectFavoriteSerializer(BaseSerializer):
project_detail = ProjectLiteSerializer(source="project", read_only=True)
class Meta: class Meta:
model = ProjectFavorite model = ProjectFavorite
fields = "__all__" fields = "__all__"
@ -178,12 +198,12 @@ class ProjectDeployBoardSerializer(BaseSerializer):
fields = "__all__" fields = "__all__"
read_only_fields = [ read_only_fields = [
"workspace", "workspace",
"project", "anchor", "project",
"anchor",
] ]
class ProjectPublicMemberSerializer(BaseSerializer): class ProjectPublicMemberSerializer(BaseSerializer):
class Meta: class Meta:
model = ProjectPublicMember model = ProjectPublicMember
fields = "__all__" fields = "__all__"

View File

@ -3,7 +3,7 @@ from rest_framework import serializers
# Module import # Module import
from .base import BaseSerializer from .base import BaseSerializer
from plane.db.models import User from plane.db.models import User, Workspace, WorkspaceMemberInvite
class UserSerializer(BaseSerializer): class UserSerializer(BaseSerializer):
@ -33,6 +33,81 @@ class UserSerializer(BaseSerializer):
return bool(obj.first_name) or bool(obj.last_name) return bool(obj.first_name) or bool(obj.last_name)
class UserMeSerializer(BaseSerializer):
class Meta:
model = User
fields = [
"id",
"avatar",
"cover_image",
"date_joined",
"display_name",
"email",
"first_name",
"last_name",
"is_active",
"is_bot",
"is_email_verified",
"is_managed",
"is_onboarded",
"is_tour_completed",
"mobile_number",
"role",
"onboarding_step",
"user_timezone",
"username",
"theme",
"last_workspace_id",
]
read_only_fields = fields
class UserMeSettingsSerializer(BaseSerializer):
workspace = serializers.SerializerMethodField()
class Meta:
model = User
fields = [
"id",
"email",
"workspace",
]
read_only_fields = fields
def get_workspace(self, obj):
workspace_invites = WorkspaceMemberInvite.objects.filter(
email=obj.email
).count()
if obj.last_workspace_id is not None:
workspace = Workspace.objects.filter(
pk=obj.last_workspace_id, workspace_member__member=obj.id
).first()
return {
"last_workspace_id": obj.last_workspace_id,
"last_workspace_slug": workspace.slug if workspace is not None else "",
"fallback_workspace_id": obj.last_workspace_id,
"fallback_workspace_slug": workspace.slug if workspace is not None else "",
"invites": workspace_invites,
}
else:
fallback_workspace = (
Workspace.objects.filter(workspace_member__member_id=obj.id)
.order_by("created_at")
.first()
)
return {
"last_workspace_id": None,
"last_workspace_slug": None,
"fallback_workspace_id": fallback_workspace.id
if fallback_workspace is not None
else None,
"fallback_workspace_slug": fallback_workspace.slug
if fallback_workspace is not None
else None,
"invites": workspace_invites,
}
class UserLiteSerializer(BaseSerializer): class UserLiteSerializer(BaseSerializer):
class Meta: class Meta:
model = User model = User
@ -51,7 +126,6 @@ class UserLiteSerializer(BaseSerializer):
class UserAdminLiteSerializer(BaseSerializer): class UserAdminLiteSerializer(BaseSerializer):
class Meta: class Meta:
model = User model = User
fields = [ fields = [

View File

@ -57,7 +57,7 @@ class IssueViewSerializer(BaseSerializer):
if bool(query_params): if bool(query_params):
validated_data["query"] = issue_filters(query_params, "POST") validated_data["query"] = issue_filters(query_params, "POST")
else: else:
validated_data["query"] = dict() validated_data["query"] = {}
return IssueView.objects.create(**validated_data) return IssueView.objects.create(**validated_data)
def update(self, instance, validated_data): def update(self, instance, validated_data):
@ -65,7 +65,7 @@ class IssueViewSerializer(BaseSerializer):
if bool(query_params): if bool(query_params):
validated_data["query"] = issue_filters(query_params, "POST") validated_data["query"] = issue_filters(query_params, "POST")
else: else:
validated_data["query"] = dict() validated_data["query"] = {}
validated_data["query"] = issue_filters(query_params, "PATCH") validated_data["query"] = issue_filters(query_params, "PATCH")
return super().update(instance, validated_data) return super().update(instance, validated_data)

View File

@ -54,6 +54,13 @@ class WorkSpaceMemberSerializer(BaseSerializer):
fields = "__all__" fields = "__all__"
class WorkspaceMemberMeSerializer(BaseSerializer):
class Meta:
model = WorkspaceMember
fields = "__all__"
class WorkspaceMemberAdminSerializer(BaseSerializer): class WorkspaceMemberAdminSerializer(BaseSerializer):
member = UserAdminLiteSerializer(read_only=True) member = UserAdminLiteSerializer(read_only=True)
workspace = WorkspaceLiteSerializer(read_only=True) workspace = WorkspaceLiteSerializer(read_only=True)
@ -103,7 +110,6 @@ class TeamSerializer(BaseSerializer):
] ]
TeamMember.objects.bulk_create(team_members, batch_size=10) TeamMember.objects.bulk_create(team_members, batch_size=10)
return team return team
else:
team = Team.objects.create(**validated_data) team = Team.objects.create(**validated_data)
return team return team
@ -117,7 +123,6 @@ class TeamSerializer(BaseSerializer):
] ]
TeamMember.objects.bulk_create(team_members, batch_size=10) TeamMember.objects.bulk_create(team_members, batch_size=10)
return super().update(instance, validated_data) return super().update(instance, validated_data)
else:
return super().update(instance, validated_data) return super().update(instance, validated_data)

View File

@ -0,0 +1,50 @@
from .analytic import urlpatterns as analytic_urls
from .asset import urlpatterns as asset_urls
from .authentication import urlpatterns as authentication_urls
from .configuration import urlpatterns as configuration_urls
from .cycle import urlpatterns as cycle_urls
from .estimate import urlpatterns as estimate_urls
from .gpt import urlpatterns as gpt_urls
from .importer import urlpatterns as importer_urls
from .inbox import urlpatterns as inbox_urls
from .integration import urlpatterns as integration_urls
from .issue import urlpatterns as issue_urls
from .module import urlpatterns as module_urls
from .notification import urlpatterns as notification_urls
from .page import urlpatterns as page_urls
from .project import urlpatterns as project_urls
from .public_board import urlpatterns as public_board_urls
from .release_note import urlpatterns as release_note_urls
from .search import urlpatterns as search_urls
from .state import urlpatterns as state_urls
from .unsplash import urlpatterns as unsplash_urls
from .user import urlpatterns as user_urls
from .views import urlpatterns as view_urls
from .workspace import urlpatterns as workspace_urls
urlpatterns = [
*analytic_urls,
*asset_urls,
*authentication_urls,
*configuration_urls,
*cycle_urls,
*estimate_urls,
*gpt_urls,
*importer_urls,
*inbox_urls,
*integration_urls,
*issue_urls,
*module_urls,
*notification_urls,
*page_urls,
*project_urls,
*public_board_urls,
*release_note_urls,
*search_urls,
*state_urls,
*unsplash_urls,
*user_urls,
*view_urls,
*workspace_urls,
]

View File

@ -0,0 +1,46 @@
from django.urls import path
from plane.api.views import (
AnalyticsEndpoint,
AnalyticViewViewset,
SavedAnalyticEndpoint,
ExportAnalyticsEndpoint,
DefaultAnalyticsEndpoint,
)
urlpatterns = [
path(
"workspaces/<str:slug>/analytics/",
AnalyticsEndpoint.as_view(),
name="plane-analytics",
),
path(
"workspaces/<str:slug>/analytic-view/",
AnalyticViewViewset.as_view({"get": "list", "post": "create"}),
name="analytic-view",
),
path(
"workspaces/<str:slug>/analytic-view/<uuid:pk>/",
AnalyticViewViewset.as_view(
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
),
name="analytic-view",
),
path(
"workspaces/<str:slug>/saved-analytic-view/<uuid:analytic_id>/",
SavedAnalyticEndpoint.as_view(),
name="saved-analytic-view",
),
path(
"workspaces/<str:slug>/export-analytics/",
ExportAnalyticsEndpoint.as_view(),
name="export-analytics",
),
path(
"workspaces/<str:slug>/default-analytics/",
DefaultAnalyticsEndpoint.as_view(),
name="default-analytics",
),
]

View File

@ -0,0 +1,31 @@
from django.urls import path
from plane.api.views import (
FileAssetEndpoint,
UserAssetsEndpoint,
)
urlpatterns = [
path(
"workspaces/<str:slug>/file-assets/",
FileAssetEndpoint.as_view(),
name="file-assets",
),
path(
"workspaces/file-assets/<uuid:workspace_id>/<str:asset_key>/",
FileAssetEndpoint.as_view(),
name="file-assets",
),
path(
"users/file-assets/",
UserAssetsEndpoint.as_view(),
name="user-file-assets",
),
path(
"users/file-assets/<str:asset_key>/",
UserAssetsEndpoint.as_view(),
name="user-file-assets",
),
]

View File

@ -0,0 +1,68 @@
from django.urls import path
from rest_framework_simplejwt.views import TokenRefreshView
from plane.api.views import (
# Authentication
SignUpEndpoint,
SignInEndpoint,
SignOutEndpoint,
MagicSignInEndpoint,
MagicSignInGenerateEndpoint,
OauthEndpoint,
## End Authentication
# Auth Extended
ForgotPasswordEndpoint,
VerifyEmailEndpoint,
ResetPasswordEndpoint,
RequestEmailVerificationEndpoint,
ChangePasswordEndpoint,
## End Auth Extender
# API Tokens
ApiTokenEndpoint,
## End API Tokens
)
urlpatterns = [
# Social Auth
path("social-auth/", OauthEndpoint.as_view(), name="oauth"),
# Auth
path("sign-up/", SignUpEndpoint.as_view(), name="sign-up"),
path("sign-in/", SignInEndpoint.as_view(), name="sign-in"),
path("sign-out/", SignOutEndpoint.as_view(), name="sign-out"),
# Magic Sign In/Up
path(
"magic-generate/", MagicSignInGenerateEndpoint.as_view(), name="magic-generate"
),
path("magic-sign-in/", MagicSignInEndpoint.as_view(), name="magic-sign-in"),
path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
# Email verification
path("email-verify/", VerifyEmailEndpoint.as_view(), name="email-verify"),
path(
"request-email-verify/",
RequestEmailVerificationEndpoint.as_view(),
name="request-reset-email",
),
# Password Manipulation
path(
"users/me/change-password/",
ChangePasswordEndpoint.as_view(),
name="change-password",
),
path(
"reset-password/<uidb64>/<token>/",
ResetPasswordEndpoint.as_view(),
name="password-reset",
),
path(
"forgot-password/",
ForgotPasswordEndpoint.as_view(),
name="forgot-password",
),
# API Tokens
path("api-tokens/", ApiTokenEndpoint.as_view(), name="api-tokens"),
path("api-tokens/<uuid:pk>/", ApiTokenEndpoint.as_view(), name="api-tokens"),
## End API Tokens
]

View File

@ -0,0 +1,12 @@
from django.urls import path
from plane.api.views import ConfigurationEndpoint
urlpatterns = [
path(
"configs/",
ConfigurationEndpoint.as_view(),
name="configuration",
),
]

View File

@ -0,0 +1,87 @@
from django.urls import path
from plane.api.views import (
CycleViewSet,
CycleIssueViewSet,
CycleDateCheckEndpoint,
CycleFavoriteViewSet,
TransferCycleIssueEndpoint,
)
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/",
CycleViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-cycle",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:pk>/",
CycleViewSet.as_view(
{
"get": "retrieve",
"put": "update",
"patch": "partial_update",
"delete": "destroy",
}
),
name="project-cycle",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/cycle-issues/",
CycleIssueViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-issue-cycle",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/cycle-issues/<uuid:pk>/",
CycleIssueViewSet.as_view(
{
"get": "retrieve",
"put": "update",
"patch": "partial_update",
"delete": "destroy",
}
),
name="project-issue-cycle",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/date-check/",
CycleDateCheckEndpoint.as_view(),
name="project-cycle-date",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-cycles/",
CycleFavoriteViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="user-favorite-cycle",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-cycles/<uuid:cycle_id>/",
CycleFavoriteViewSet.as_view(
{
"delete": "destroy",
}
),
name="user-favorite-cycle",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/transfer-issues/",
TransferCycleIssueEndpoint.as_view(),
name="transfer-issues",
),
]

View File

@ -0,0 +1,37 @@
from django.urls import path
from plane.api.views import (
ProjectEstimatePointEndpoint,
BulkEstimatePointEndpoint,
)
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/project-estimates/",
ProjectEstimatePointEndpoint.as_view(),
name="project-estimate-points",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/",
BulkEstimatePointEndpoint.as_view(
{
"get": "list",
"post": "create",
}
),
name="bulk-create-estimate-points",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/<uuid:estimate_id>/",
BulkEstimatePointEndpoint.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="bulk-create-estimate-points",
),
]

View File

@ -0,0 +1,13 @@
from django.urls import path
from plane.api.views import GPTIntegrationEndpoint
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/ai-assistant/",
GPTIntegrationEndpoint.as_view(),
name="importer",
),
]

View File

@ -0,0 +1,37 @@
from django.urls import path
from plane.api.views import (
ServiceIssueImportSummaryEndpoint,
ImportServiceEndpoint,
UpdateServiceImportStatusEndpoint,
)
urlpatterns = [
path(
"workspaces/<str:slug>/importers/<str:service>/",
ServiceIssueImportSummaryEndpoint.as_view(),
name="importer-summary",
),
path(
"workspaces/<str:slug>/projects/importers/<str:service>/",
ImportServiceEndpoint.as_view(),
name="importer",
),
path(
"workspaces/<str:slug>/importers/",
ImportServiceEndpoint.as_view(),
name="importer",
),
path(
"workspaces/<str:slug>/importers/<str:service>/<uuid:pk>/",
ImportServiceEndpoint.as_view(),
name="importer",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/service/<str:service>/importers/<uuid:importer_id>/",
UpdateServiceImportStatusEndpoint.as_view(),
name="importer-status",
),
]

View File

@ -0,0 +1,53 @@
from django.urls import path
from plane.api.views import (
InboxViewSet,
InboxIssueViewSet,
)
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/",
InboxViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="inbox",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/<uuid:pk>/",
InboxViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="inbox",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/<uuid:inbox_id>/inbox-issues/",
InboxIssueViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="inbox-issue",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/<uuid:inbox_id>/inbox-issues/<uuid:pk>/",
InboxIssueViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="inbox-issue",
),
]

View File

@ -0,0 +1,150 @@
from django.urls import path
from plane.api.views import (
IntegrationViewSet,
WorkspaceIntegrationViewSet,
GithubRepositoriesEndpoint,
GithubRepositorySyncViewSet,
GithubIssueSyncViewSet,
GithubCommentSyncViewSet,
BulkCreateGithubIssueSyncEndpoint,
SlackProjectSyncViewSet,
)
urlpatterns = [
path(
"integrations/",
IntegrationViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="integrations",
),
path(
"integrations/<uuid:pk>/",
IntegrationViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="integrations",
),
path(
"workspaces/<str:slug>/workspace-integrations/",
WorkspaceIntegrationViewSet.as_view(
{
"get": "list",
}
),
name="workspace-integrations",
),
path(
"workspaces/<str:slug>/workspace-integrations/<str:provider>/",
WorkspaceIntegrationViewSet.as_view(
{
"post": "create",
}
),
name="workspace-integrations",
),
path(
"workspaces/<str:slug>/workspace-integrations/<uuid:pk>/provider/",
WorkspaceIntegrationViewSet.as_view(
{
"get": "retrieve",
"delete": "destroy",
}
),
name="workspace-integrations",
),
# Github Integrations
path(
"workspaces/<str:slug>/workspace-integrations/<uuid:workspace_integration_id>/github-repositories/",
GithubRepositoriesEndpoint.as_view(),
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/workspace-integrations/<uuid:workspace_integration_id>/github-repository-sync/",
GithubRepositorySyncViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/workspace-integrations/<uuid:workspace_integration_id>/github-repository-sync/<uuid:pk>/",
GithubRepositorySyncViewSet.as_view(
{
"get": "retrieve",
"delete": "destroy",
}
),
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/github-repository-sync/<uuid:repo_sync_id>/github-issue-sync/",
GithubIssueSyncViewSet.as_view(
{
"post": "create",
"get": "list",
}
),
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/github-repository-sync/<uuid:repo_sync_id>/bulk-create-github-issue-sync/",
BulkCreateGithubIssueSyncEndpoint.as_view(),
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/github-repository-sync/<uuid:repo_sync_id>/github-issue-sync/<uuid:pk>/",
GithubIssueSyncViewSet.as_view(
{
"get": "retrieve",
"delete": "destroy",
}
),
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/github-repository-sync/<uuid:repo_sync_id>/github-issue-sync/<uuid:issue_sync_id>/github-comment-sync/",
GithubCommentSyncViewSet.as_view(
{
"post": "create",
"get": "list",
}
),
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/github-repository-sync/<uuid:repo_sync_id>/github-issue-sync/<uuid:issue_sync_id>/github-comment-sync/<uuid:pk>/",
GithubCommentSyncViewSet.as_view(
{
"get": "retrieve",
"delete": "destroy",
}
),
),
## End Github Integrations
# Slack Integration
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/workspace-integrations/<uuid:workspace_integration_id>/project-slack-sync/",
SlackProjectSyncViewSet.as_view(
{
"post": "create",
"get": "list",
}
),
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/workspace-integrations/<uuid:workspace_integration_id>/project-slack-sync/<uuid:pk>/",
SlackProjectSyncViewSet.as_view(
{
"delete": "destroy",
"get": "retrieve",
}
),
),
## End Slack Integration
]

View File

@ -0,0 +1,315 @@
from django.urls import path
from plane.api.views import (
IssueViewSet,
LabelViewSet,
BulkCreateIssueLabelsEndpoint,
BulkDeleteIssuesEndpoint,
BulkImportIssuesEndpoint,
UserWorkSpaceIssues,
SubIssuesEndpoint,
IssueLinkViewSet,
IssueAttachmentEndpoint,
ExportIssuesEndpoint,
IssueActivityEndpoint,
IssueCommentViewSet,
IssueSubscriberViewSet,
IssueReactionViewSet,
CommentReactionViewSet,
IssueUserDisplayPropertyEndpoint,
IssueArchiveViewSet,
IssueRelationViewSet,
IssueDraftViewSet,
)
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/",
IssueViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-issue",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:pk>/",
IssueViewSet.as_view(
{
"get": "retrieve",
"put": "update",
"patch": "partial_update",
"delete": "destroy",
}
),
name="project-issue",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-labels/",
LabelViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-issue-labels",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-labels/<uuid:pk>/",
LabelViewSet.as_view(
{
"get": "retrieve",
"put": "update",
"patch": "partial_update",
"delete": "destroy",
}
),
name="project-issue-labels",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/bulk-create-labels/",
BulkCreateIssueLabelsEndpoint.as_view(),
name="project-bulk-labels",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/bulk-delete-issues/",
BulkDeleteIssuesEndpoint.as_view(),
name="project-issues-bulk",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/bulk-import-issues/<str:service>/",
BulkImportIssuesEndpoint.as_view(),
name="project-issues-bulk",
),
path(
"workspaces/<str:slug>/my-issues/",
UserWorkSpaceIssues.as_view(),
name="workspace-issues",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/sub-issues/",
SubIssuesEndpoint.as_view(),
name="sub-issues",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-links/",
IssueLinkViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-issue-links",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-links/<uuid:pk>/",
IssueLinkViewSet.as_view(
{
"get": "retrieve",
"put": "update",
"patch": "partial_update",
"delete": "destroy",
}
),
name="project-issue-links",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-attachments/",
IssueAttachmentEndpoint.as_view(),
name="project-issue-attachments",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-attachments/<uuid:pk>/",
IssueAttachmentEndpoint.as_view(),
name="project-issue-attachments",
),
path(
"workspaces/<str:slug>/export-issues/",
ExportIssuesEndpoint.as_view(),
name="export-issues",
),
## End Issues
## Issue Activity
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/history/",
IssueActivityEndpoint.as_view(),
name="project-issue-history",
),
## Issue Activity
## IssueComments
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/comments/",
IssueCommentViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-issue-comment",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/comments/<uuid:pk>/",
IssueCommentViewSet.as_view(
{
"get": "retrieve",
"put": "update",
"patch": "partial_update",
"delete": "destroy",
}
),
name="project-issue-comment",
),
## End IssueComments
# Issue Subscribers
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-subscribers/",
IssueSubscriberViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-issue-subscribers",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-subscribers/<uuid:subscriber_id>/",
IssueSubscriberViewSet.as_view({"delete": "destroy"}),
name="project-issue-subscribers",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/subscribe/",
IssueSubscriberViewSet.as_view(
{
"get": "subscription_status",
"post": "subscribe",
"delete": "unsubscribe",
}
),
name="project-issue-subscribers",
),
## End Issue Subscribers
# Issue Reactions
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/reactions/",
IssueReactionViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-issue-reactions",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/reactions/<str:reaction_code>/",
IssueReactionViewSet.as_view(
{
"delete": "destroy",
}
),
name="project-issue-reactions",
),
## End Issue Reactions
# Comment Reactions
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/comments/<uuid:comment_id>/reactions/",
CommentReactionViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-issue-comment-reactions",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/comments/<uuid:comment_id>/reactions/<str:reaction_code>/",
CommentReactionViewSet.as_view(
{
"delete": "destroy",
}
),
name="project-issue-comment-reactions",
),
## End Comment Reactions
## IssueProperty
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-display-properties/",
IssueUserDisplayPropertyEndpoint.as_view(),
name="project-issue-display-properties",
),
## IssueProperty End
## Issue Archives
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-issues/",
IssueArchiveViewSet.as_view(
{
"get": "list",
}
),
name="project-issue-archive",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-issues/<uuid:pk>/",
IssueArchiveViewSet.as_view(
{
"get": "retrieve",
"delete": "destroy",
}
),
name="project-issue-archive",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/unarchive/<uuid:pk>/",
IssueArchiveViewSet.as_view(
{
"post": "unarchive",
}
),
name="project-issue-archive",
),
## End Issue Archives
## Issue Relation
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-relation/",
IssueRelationViewSet.as_view(
{
"post": "create",
}
),
name="issue-relation",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-relation/<uuid:pk>/",
IssueRelationViewSet.as_view(
{
"delete": "destroy",
}
),
name="issue-relation",
),
## End Issue Relation
## Issue Drafts
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-drafts/",
IssueDraftViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-issue-draft",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-drafts/<uuid:pk>/",
IssueDraftViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="project-issue-draft",
),
]

View File

@ -0,0 +1,104 @@
from django.urls import path
from plane.api.views import (
ModuleViewSet,
ModuleIssueViewSet,
ModuleLinkViewSet,
ModuleFavoriteViewSet,
BulkImportModulesEndpoint,
)
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/",
ModuleViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-modules",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:pk>/",
ModuleViewSet.as_view(
{
"get": "retrieve",
"put": "update",
"patch": "partial_update",
"delete": "destroy",
}
),
name="project-modules",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/module-issues/",
ModuleIssueViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-module-issues",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/module-issues/<uuid:pk>/",
ModuleIssueViewSet.as_view(
{
"get": "retrieve",
"put": "update",
"patch": "partial_update",
"delete": "destroy",
}
),
name="project-module-issues",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/module-links/",
ModuleLinkViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-issue-module-links",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/module-links/<uuid:pk>/",
ModuleLinkViewSet.as_view(
{
"get": "retrieve",
"put": "update",
"patch": "partial_update",
"delete": "destroy",
}
),
name="project-issue-module-links",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-modules/",
ModuleFavoriteViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="user-favorite-module",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-modules/<uuid:module_id>/",
ModuleFavoriteViewSet.as_view(
{
"delete": "destroy",
}
),
name="user-favorite-module",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/bulk-import-modules/<str:service>/",
BulkImportModulesEndpoint.as_view(),
name="bulk-modules-create",
),
]

View File

@ -0,0 +1,66 @@
from django.urls import path
from plane.api.views import (
NotificationViewSet,
UnreadNotificationEndpoint,
MarkAllReadNotificationViewSet,
)
urlpatterns = [
path(
"workspaces/<str:slug>/users/notifications/",
NotificationViewSet.as_view(
{
"get": "list",
}
),
name="notifications",
),
path(
"workspaces/<str:slug>/users/notifications/<uuid:pk>/",
NotificationViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="notifications",
),
path(
"workspaces/<str:slug>/users/notifications/<uuid:pk>/read/",
NotificationViewSet.as_view(
{
"post": "mark_read",
"delete": "mark_unread",
}
),
name="notifications",
),
path(
"workspaces/<str:slug>/users/notifications/<uuid:pk>/archive/",
NotificationViewSet.as_view(
{
"post": "archive",
"delete": "unarchive",
}
),
name="notifications",
),
path(
"workspaces/<str:slug>/users/notifications/unread/",
UnreadNotificationEndpoint.as_view(),
name="unread-notifications",
),
path(
"workspaces/<str:slug>/users/notifications/mark-all-read/",
MarkAllReadNotificationViewSet.as_view(
{
"post": "create",
}
),
name="mark-all-read-notifications",
),
]

View File

@ -0,0 +1,79 @@
from django.urls import path
from plane.api.views import (
PageViewSet,
PageBlockViewSet,
PageFavoriteViewSet,
CreateIssueFromPageBlockEndpoint,
)
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/",
PageViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-pages",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/",
PageViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="project-pages",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/page-blocks/",
PageBlockViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-page-blocks",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/page-blocks/<uuid:pk>/",
PageBlockViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="project-page-blocks",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-pages/",
PageFavoriteViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="user-favorite-pages",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-pages/<uuid:page_id>/",
PageFavoriteViewSet.as_view(
{
"delete": "destroy",
}
),
name="user-favorite-pages",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/page-blocks/<uuid:page_block_id>/issues/",
CreateIssueFromPageBlockEndpoint.as_view(),
name="page-block-issues",
),
]

View File

@ -0,0 +1,132 @@
from django.urls import path
from plane.api.views import (
ProjectViewSet,
InviteProjectEndpoint,
ProjectMemberViewSet,
ProjectMemberInvitationsViewset,
ProjectMemberUserEndpoint,
ProjectJoinEndpoint,
AddTeamToProjectEndpoint,
ProjectUserViewsEndpoint,
ProjectIdentifierEndpoint,
ProjectFavoritesViewSet,
LeaveProjectEndpoint,
ProjectPublicCoverImagesEndpoint,
)
urlpatterns = [
path(
"workspaces/<str:slug>/projects/",
ProjectViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project",
),
path(
"workspaces/<str:slug>/projects/<uuid:pk>/",
ProjectViewSet.as_view(
{
"get": "retrieve",
"put": "update",
"patch": "partial_update",
"delete": "destroy",
}
),
name="project",
),
path(
"workspaces/<str:slug>/project-identifiers/",
ProjectIdentifierEndpoint.as_view(),
name="project-identifiers",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/invite/",
InviteProjectEndpoint.as_view(),
name="invite-project",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/members/",
ProjectMemberViewSet.as_view({"get": "list", "post": "create"}),
name="project-member",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/members/<uuid:pk>/",
ProjectMemberViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="project-member",
),
path(
"workspaces/<str:slug>/projects/join/",
ProjectJoinEndpoint.as_view(),
name="project-join",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/team-invite/",
AddTeamToProjectEndpoint.as_view(),
name="projects",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/invitations/",
ProjectMemberInvitationsViewset.as_view({"get": "list"}),
name="project-member-invite",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/invitations/<uuid:pk>/",
ProjectMemberInvitationsViewset.as_view(
{
"get": "retrieve",
"delete": "destroy",
}
),
name="project-member-invite",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/project-views/",
ProjectUserViewsEndpoint.as_view(),
name="project-view",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/project-members/me/",
ProjectMemberUserEndpoint.as_view(),
name="project-member-view",
),
path(
"workspaces/<str:slug>/user-favorite-projects/",
ProjectFavoritesViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-favorite",
),
path(
"workspaces/<str:slug>/user-favorite-projects/<uuid:project_id>/",
ProjectFavoritesViewSet.as_view(
{
"delete": "destroy",
}
),
name="project-favorite",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/members/leave/",
LeaveProjectEndpoint.as_view(),
name="leave-project",
),
path(
"project-covers/",
ProjectPublicCoverImagesEndpoint.as_view(),
name="project-covers",
),
]

View File

@ -0,0 +1,151 @@
from django.urls import path
from plane.api.views import (
ProjectDeployBoardViewSet,
ProjectDeployBoardPublicSettingsEndpoint,
ProjectIssuesPublicEndpoint,
IssueRetrievePublicEndpoint,
IssueCommentPublicViewSet,
IssueReactionPublicViewSet,
CommentReactionPublicViewSet,
InboxIssuePublicViewSet,
IssueVotePublicViewSet,
WorkspaceProjectDeployBoardEndpoint,
)
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/project-deploy-boards/",
ProjectDeployBoardViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-deploy-board",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/project-deploy-boards/<uuid:pk>/",
ProjectDeployBoardViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="project-deploy-board",
),
path(
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/settings/",
ProjectDeployBoardPublicSettingsEndpoint.as_view(),
name="project-deploy-board-settings",
),
path(
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/",
ProjectIssuesPublicEndpoint.as_view(),
name="project-deploy-board",
),
path(
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/",
IssueRetrievePublicEndpoint.as_view(),
name="workspace-project-boards",
),
path(
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/comments/",
IssueCommentPublicViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="issue-comments-project-board",
),
path(
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/comments/<uuid:pk>/",
IssueCommentPublicViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="issue-comments-project-board",
),
path(
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/reactions/",
IssueReactionPublicViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="issue-reactions-project-board",
),
path(
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/reactions/<str:reaction_code>/",
IssueReactionPublicViewSet.as_view(
{
"delete": "destroy",
}
),
name="issue-reactions-project-board",
),
path(
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/comments/<uuid:comment_id>/reactions/",
CommentReactionPublicViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="comment-reactions-project-board",
),
path(
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/comments/<uuid:comment_id>/reactions/<str:reaction_code>/",
CommentReactionPublicViewSet.as_view(
{
"delete": "destroy",
}
),
name="comment-reactions-project-board",
),
path(
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/inboxes/<uuid:inbox_id>/inbox-issues/",
InboxIssuePublicViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="inbox-issue",
),
path(
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/inboxes/<uuid:inbox_id>/inbox-issues/<uuid:pk>/",
InboxIssuePublicViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="inbox-issue",
),
path(
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/votes/",
IssueVotePublicViewSet.as_view(
{
"get": "list",
"post": "create",
"delete": "destroy",
}
),
name="issue-vote-project-board",
),
path(
"public/workspaces/<str:slug>/project-boards/",
WorkspaceProjectDeployBoardEndpoint.as_view(),
name="workspace-project-boards",
),
]

View File

@ -0,0 +1,13 @@
from django.urls import path
from plane.api.views import ReleaseNotesEndpoint
urlpatterns = [
path(
"release-notes/",
ReleaseNotesEndpoint.as_view(),
name="release-notes",
),
]

View File

@ -0,0 +1,21 @@
from django.urls import path
from plane.api.views import (
GlobalSearchEndpoint,
IssueSearchEndpoint,
)
urlpatterns = [
path(
"workspaces/<str:slug>/search/",
GlobalSearchEndpoint.as_view(),
name="global-search",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/search-issues/",
IssueSearchEndpoint.as_view(),
name="project-issue-search",
),
]

View File

@ -0,0 +1,30 @@
from django.urls import path
from plane.api.views import StateViewSet
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/states/",
StateViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-states",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/states/<uuid:pk>/",
StateViewSet.as_view(
{
"get": "retrieve",
"put": "update",
"patch": "partial_update",
"delete": "destroy",
}
),
name="project-state",
),
]

View File

@ -0,0 +1,13 @@
from django.urls import path
from plane.api.views import UnsplashEndpoint
urlpatterns = [
path(
"unsplash/",
UnsplashEndpoint.as_view(),
name="unsplash",
),
]

View File

@ -0,0 +1,113 @@
from django.urls import path
from plane.api.views import (
## User
UserEndpoint,
UpdateUserOnBoardedEndpoint,
UpdateUserTourCompletedEndpoint,
UserActivityEndpoint,
ChangePasswordEndpoint,
## End User
## Workspaces
UserWorkspaceInvitationsEndpoint,
UserWorkSpacesEndpoint,
JoinWorkspaceEndpoint,
UserWorkspaceInvitationsEndpoint,
UserWorkspaceInvitationEndpoint,
UserActivityGraphEndpoint,
UserIssueCompletedGraphEndpoint,
UserWorkspaceDashboardEndpoint,
UserProjectInvitationsViewset,
## End Workspaces
)
urlpatterns = [
# User Profile
path(
"users/me/",
UserEndpoint.as_view(
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
),
name="users",
),
path(
"users/me/settings/",
UserEndpoint.as_view(
{
"get": "retrieve_user_settings",
}
),
name="users",
),
path(
"users/me/change-password/",
ChangePasswordEndpoint.as_view(),
name="change-password",
),
path(
"users/me/onboard/",
UpdateUserOnBoardedEndpoint.as_view(),
name="user-onboard",
),
path(
"users/me/tour-completed/",
UpdateUserTourCompletedEndpoint.as_view(),
name="user-tour",
),
path(
"users/workspaces/<str:slug>/activities/",
UserActivityEndpoint.as_view(),
name="user-activities",
),
# user workspaces
path(
"users/me/workspaces/",
UserWorkSpacesEndpoint.as_view(),
name="user-workspace",
),
# user workspace invitations
path(
"users/me/invitations/workspaces/",
UserWorkspaceInvitationsEndpoint.as_view({"get": "list", "post": "create"}),
name="user-workspace-invitations",
),
# user workspace invitation
path(
"users/me/invitations/<uuid:pk>/",
UserWorkspaceInvitationEndpoint.as_view(
{
"get": "retrieve",
}
),
name="user-workspace-invitation",
),
# user join workspace
# User Graphs
path(
"users/me/workspaces/<str:slug>/activity-graph/",
UserActivityGraphEndpoint.as_view(),
name="user-activity-graph",
),
path(
"users/me/workspaces/<str:slug>/issues-completed-graph/",
UserIssueCompletedGraphEndpoint.as_view(),
name="completed-graph",
),
path(
"users/me/workspaces/<str:slug>/dashboard/",
UserWorkspaceDashboardEndpoint.as_view(),
name="user-workspace-dashboard",
),
## End User Graph
path(
"users/me/invitations/workspaces/<str:slug>/<uuid:pk>/join/",
JoinWorkspaceEndpoint.as_view(),
name="user-join-workspace",
),
# user project invitations
path(
"users/me/invitations/projects/",
UserProjectInvitationsViewset.as_view({"get": "list", "post": "create"}),
name="user-project-invitations",
),
]

View File

@ -0,0 +1,85 @@
from django.urls import path
from plane.api.views import (
IssueViewViewSet,
GlobalViewViewSet,
GlobalViewIssuesViewSet,
IssueViewFavoriteViewSet,
)
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/views/",
IssueViewViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-view",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/views/<uuid:pk>/",
IssueViewViewSet.as_view(
{
"get": "retrieve",
"put": "update",
"patch": "partial_update",
"delete": "destroy",
}
),
name="project-view",
),
path(
"workspaces/<str:slug>/views/",
GlobalViewViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="global-view",
),
path(
"workspaces/<str:slug>/views/<uuid:pk>/",
GlobalViewViewSet.as_view(
{
"get": "retrieve",
"put": "update",
"patch": "partial_update",
"delete": "destroy",
}
),
name="global-view",
),
path(
"workspaces/<str:slug>/issues/",
GlobalViewIssuesViewSet.as_view(
{
"get": "list",
}
),
name="global-view-issues",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-views/",
IssueViewFavoriteViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="user-favorite-view",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-views/<uuid:view_id>/",
IssueViewFavoriteViewSet.as_view(
{
"delete": "destroy",
}
),
name="user-favorite-view",
),
]

View File

@ -0,0 +1,176 @@
from django.urls import path
from plane.api.views import (
WorkSpaceViewSet,
InviteWorkspaceEndpoint,
WorkSpaceMemberViewSet,
WorkspaceInvitationsViewset,
WorkspaceMemberUserEndpoint,
WorkspaceMemberUserViewsEndpoint,
WorkSpaceAvailabilityCheckEndpoint,
TeamMemberViewSet,
UserLastProjectWithWorkspaceEndpoint,
WorkspaceThemeViewSet,
WorkspaceUserProfileStatsEndpoint,
WorkspaceUserActivityEndpoint,
WorkspaceUserProfileEndpoint,
WorkspaceUserProfileIssuesEndpoint,
WorkspaceLabelsEndpoint,
LeaveWorkspaceEndpoint,
)
urlpatterns = [
path(
"workspace-slug-check/",
WorkSpaceAvailabilityCheckEndpoint.as_view(),
name="workspace-availability",
),
path(
"workspaces/",
WorkSpaceViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="workspace",
),
path(
"workspaces/<str:slug>/",
WorkSpaceViewSet.as_view(
{
"get": "retrieve",
"put": "update",
"patch": "partial_update",
"delete": "destroy",
}
),
name="workspace",
),
path(
"workspaces/<str:slug>/invite/",
InviteWorkspaceEndpoint.as_view(),
name="invite-workspace",
),
path(
"workspaces/<str:slug>/invitations/",
WorkspaceInvitationsViewset.as_view({"get": "list"}),
name="workspace-invitations",
),
path(
"workspaces/<str:slug>/invitations/<uuid:pk>/",
WorkspaceInvitationsViewset.as_view(
{
"delete": "destroy",
"get": "retrieve",
}
),
name="workspace-invitations",
),
path(
"workspaces/<str:slug>/members/",
WorkSpaceMemberViewSet.as_view({"get": "list"}),
name="workspace-member",
),
path(
"workspaces/<str:slug>/members/<uuid:pk>/",
WorkSpaceMemberViewSet.as_view(
{
"patch": "partial_update",
"delete": "destroy",
"get": "retrieve",
}
),
name="workspace-member",
),
path(
"workspaces/<str:slug>/teams/",
TeamMemberViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="workspace-team-members",
),
path(
"workspaces/<str:slug>/teams/<uuid:pk>/",
TeamMemberViewSet.as_view(
{
"put": "update",
"patch": "partial_update",
"delete": "destroy",
"get": "retrieve",
}
),
name="workspace-team-members",
),
path(
"users/last-visited-workspace/",
UserLastProjectWithWorkspaceEndpoint.as_view(),
name="workspace-project-details",
),
path(
"workspaces/<str:slug>/workspace-members/me/",
WorkspaceMemberUserEndpoint.as_view(),
name="workspace-member-details",
),
path(
"workspaces/<str:slug>/workspace-views/",
WorkspaceMemberUserViewsEndpoint.as_view(),
name="workspace-member-views-details",
),
path(
"workspaces/<str:slug>/workspace-themes/",
WorkspaceThemeViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="workspace-themes",
),
path(
"workspaces/<str:slug>/workspace-themes/<uuid:pk>/",
WorkspaceThemeViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="workspace-themes",
),
path(
"workspaces/<str:slug>/user-stats/<uuid:user_id>/",
WorkspaceUserProfileStatsEndpoint.as_view(),
name="workspace-user-stats",
),
path(
"workspaces/<str:slug>/user-activity/<uuid:user_id>/",
WorkspaceUserActivityEndpoint.as_view(),
name="workspace-user-activity",
),
path(
"workspaces/<str:slug>/user-profile/<uuid:user_id>/",
WorkspaceUserProfileEndpoint.as_view(),
name="workspace-user-profile-page",
),
path(
"workspaces/<str:slug>/user-issues/<uuid:user_id>/",
WorkspaceUserProfileIssuesEndpoint.as_view(),
name="workspace-user-profile-issues",
),
path(
"workspaces/<str:slug>/labels/",
WorkspaceLabelsEndpoint.as_view(),
name="workspace-labels",
),
path(
"workspaces/<str:slug>/members/leave/",
LeaveWorkspaceEndpoint.as_view(),
name="leave-workspace-members",
),
]

View File

@ -1,5 +1,6 @@
from django.urls import path from django.urls import path
from rest_framework_simplejwt.views import TokenRefreshView
# Create your urls here. # Create your urls here.
@ -27,7 +28,6 @@ from plane.api.views import (
## End User ## End User
# Workspaces # Workspaces
WorkSpaceViewSet, WorkSpaceViewSet,
UserWorkspaceInvitationsEndpoint,
UserWorkSpacesEndpoint, UserWorkSpacesEndpoint,
InviteWorkspaceEndpoint, InviteWorkspaceEndpoint,
JoinWorkspaceEndpoint, JoinWorkspaceEndpoint,
@ -70,6 +70,7 @@ from plane.api.views import (
ProjectIdentifierEndpoint, ProjectIdentifierEndpoint,
ProjectFavoritesViewSet, ProjectFavoritesViewSet,
LeaveProjectEndpoint, LeaveProjectEndpoint,
ProjectPublicCoverImagesEndpoint,
## End Projects ## End Projects
# Issues # Issues
IssueViewSet, IssueViewSet,
@ -80,7 +81,7 @@ from plane.api.views import (
BulkDeleteIssuesEndpoint, BulkDeleteIssuesEndpoint,
BulkImportIssuesEndpoint, BulkImportIssuesEndpoint,
ProjectUserViewsEndpoint, ProjectUserViewsEndpoint,
IssuePropertyViewSet, IssueUserDisplayPropertyEndpoint,
LabelViewSet, LabelViewSet,
SubIssuesEndpoint, SubIssuesEndpoint,
IssueLinkViewSet, IssueLinkViewSet,
@ -105,7 +106,6 @@ from plane.api.views import (
GlobalViewViewSet, GlobalViewViewSet,
GlobalViewIssuesViewSet, GlobalViewIssuesViewSet,
IssueViewViewSet, IssueViewViewSet,
ViewIssuesEndpoint,
IssueViewFavoriteViewSet, IssueViewFavoriteViewSet,
## End Views ## End Views
# Cycles # Cycles
@ -150,12 +150,11 @@ from plane.api.views import (
GlobalSearchEndpoint, GlobalSearchEndpoint,
IssueSearchEndpoint, IssueSearchEndpoint,
## End Search ## End Search
# Gpt # External
GPTIntegrationEndpoint, GPTIntegrationEndpoint,
## End Gpt
# Release Notes
ReleaseNotesEndpoint, ReleaseNotesEndpoint,
## End Release Notes UnsplashEndpoint,
## End External
# Inbox # Inbox
InboxViewSet, InboxViewSet,
InboxIssueViewSet, InboxIssueViewSet,
@ -189,9 +188,15 @@ from plane.api.views import (
# Bulk Issue Operations # Bulk Issue Operations
BulkIssueOperationsEndpoint, BulkIssueOperationsEndpoint,
## End Bulk Issue Operations ## End Bulk Issue Operations
# Configuration
ConfigurationEndpoint,
## End Configuration
) )
#TODO: Delete this file
# This url file has been deprecated use apiserver/plane/urls folder to create new urls
urlpatterns = [ urlpatterns = [
# Social Auth # Social Auth
path("social-auth/", OauthEndpoint.as_view(), name="oauth"), path("social-auth/", OauthEndpoint.as_view(), name="oauth"),
@ -204,6 +209,7 @@ urlpatterns = [
"magic-generate/", MagicSignInGenerateEndpoint.as_view(), name="magic-generate" "magic-generate/", MagicSignInGenerateEndpoint.as_view(), name="magic-generate"
), ),
path("magic-sign-in/", MagicSignInEndpoint.as_view(), name="magic-sign-in"), path("magic-sign-in/", MagicSignInEndpoint.as_view(), name="magic-sign-in"),
path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
# Email verification # Email verification
path("email-verify/", VerifyEmailEndpoint.as_view(), name="email-verify"), path("email-verify/", VerifyEmailEndpoint.as_view(), name="email-verify"),
path( path(
@ -230,6 +236,15 @@ urlpatterns = [
), ),
name="users", name="users",
), ),
path(
"users/me/settings/",
UserEndpoint.as_view(
{
"get": "retrieve_user_settings",
}
),
name="users",
),
path( path(
"users/me/change-password/", "users/me/change-password/",
ChangePasswordEndpoint.as_view(), ChangePasswordEndpoint.as_view(),
@ -557,6 +572,7 @@ urlpatterns = [
"workspaces/<str:slug>/user-favorite-projects/", "workspaces/<str:slug>/user-favorite-projects/",
ProjectFavoritesViewSet.as_view( ProjectFavoritesViewSet.as_view(
{ {
"get": "list",
"post": "create", "post": "create",
} }
), ),
@ -576,6 +592,11 @@ urlpatterns = [
LeaveProjectEndpoint.as_view(), LeaveProjectEndpoint.as_view(),
name="project", name="project",
), ),
path(
"project-covers/",
ProjectPublicCoverImagesEndpoint.as_view(),
name="project-covers",
),
# End Projects # End Projects
# States # States
path( path(
@ -652,11 +673,6 @@ urlpatterns = [
), ),
name="project-view", name="project-view",
), ),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/views/<uuid:view_id>/issues/",
ViewIssuesEndpoint.as_view(),
name="project-view-issues",
),
path( path(
"workspaces/<str:slug>/views/", "workspaces/<str:slug>/views/",
GlobalViewViewSet.as_view( GlobalViewViewSet.as_view(
@ -994,26 +1010,9 @@ urlpatterns = [
## End Comment Reactions ## End Comment Reactions
## IssueProperty ## IssueProperty
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-properties/", "workspaces/<str:slug>/projects/<uuid:project_id>/issue-display-properties/",
IssuePropertyViewSet.as_view( IssueUserDisplayPropertyEndpoint.as_view(),
{ name="project-issue-display-properties",
"get": "list",
"post": "create",
}
),
name="project-issue-roadmap",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-properties/<uuid:pk>/",
IssuePropertyViewSet.as_view(
{
"get": "retrieve",
"put": "update",
"patch": "partial_update",
"delete": "destroy",
}
),
name="project-issue-roadmap",
), ),
## IssueProperty Ebd ## IssueProperty Ebd
## Issue Archives ## Issue Archives
@ -1449,20 +1448,23 @@ urlpatterns = [
name="project-issue-search", name="project-issue-search",
), ),
## End Search ## End Search
# Gpt # External
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/ai-assistant/", "workspaces/<str:slug>/projects/<uuid:project_id>/ai-assistant/",
GPTIntegrationEndpoint.as_view(), GPTIntegrationEndpoint.as_view(),
name="importer", name="importer",
), ),
## End Gpt
# Release Notes
path( path(
"release-notes/", "release-notes/",
ReleaseNotesEndpoint.as_view(), ReleaseNotesEndpoint.as_view(),
name="release-notes", name="release-notes",
), ),
## End Release Notes path(
"unsplash/",
UnsplashEndpoint.as_view(),
name="release-notes",
),
## End External
# Inbox # Inbox
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/", "workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/",
@ -1736,4 +1738,11 @@ urlpatterns = [
BulkIssueOperationsEndpoint.as_view(), BulkIssueOperationsEndpoint.as_view(),
name="bulk-issue-operation", name="bulk-issue-operation",
), ),
# Configuration
path(
"configs/",
ConfigurationEndpoint.as_view(),
name="configuration",
),
## End Configuration
] ]

View File

@ -7,16 +7,15 @@ from .project import (
ProjectMemberInvitationsViewset, ProjectMemberInvitationsViewset,
ProjectMemberInviteDetailViewSet, ProjectMemberInviteDetailViewSet,
ProjectIdentifierEndpoint, ProjectIdentifierEndpoint,
AddMemberToProjectEndpoint,
ProjectJoinEndpoint, ProjectJoinEndpoint,
ProjectUserViewsEndpoint, ProjectUserViewsEndpoint,
ProjectMemberUserEndpoint, ProjectMemberUserEndpoint,
ProjectFavoritesViewSet, ProjectFavoritesViewSet,
ProjectDeployBoardViewSet, ProjectDeployBoardViewSet,
ProjectDeployBoardPublicSettingsEndpoint, ProjectDeployBoardPublicSettingsEndpoint,
ProjectMemberEndpoint,
WorkspaceProjectDeployBoardEndpoint, WorkspaceProjectDeployBoardEndpoint,
LeaveProjectEndpoint, LeaveProjectEndpoint,
ProjectPublicCoverImagesEndpoint,
) )
from .user import ( from .user import (
UserEndpoint, UserEndpoint,
@ -52,11 +51,10 @@ from .workspace import (
WorkspaceUserProfileEndpoint, WorkspaceUserProfileEndpoint,
WorkspaceUserProfileIssuesEndpoint, WorkspaceUserProfileIssuesEndpoint,
WorkspaceLabelsEndpoint, WorkspaceLabelsEndpoint,
WorkspaceMembersEndpoint,
LeaveWorkspaceEndpoint, LeaveWorkspaceEndpoint,
) )
from .state import StateViewSet from .state import StateViewSet
from .view import GlobalViewViewSet, GlobalViewIssuesViewSet, IssueViewViewSet, ViewIssuesEndpoint, IssueViewFavoriteViewSet from .view import GlobalViewViewSet, GlobalViewIssuesViewSet, IssueViewViewSet, IssueViewFavoriteViewSet
from .cycle import ( from .cycle import (
CycleViewSet, CycleViewSet,
CycleIssueViewSet, CycleIssueViewSet,
@ -70,7 +68,7 @@ from .issue import (
WorkSpaceIssuesEndpoint, WorkSpaceIssuesEndpoint,
IssueActivityEndpoint, IssueActivityEndpoint,
IssueCommentViewSet, IssueCommentViewSet,
IssuePropertyViewSet, IssueUserDisplayPropertyEndpoint,
LabelViewSet, LabelViewSet,
BulkDeleteIssuesEndpoint, BulkDeleteIssuesEndpoint,
UserWorkSpaceIssues, UserWorkSpaceIssues,
@ -148,16 +146,13 @@ from .page import (
from .search import GlobalSearchEndpoint, IssueSearchEndpoint from .search import GlobalSearchEndpoint, IssueSearchEndpoint
from .gpt import GPTIntegrationEndpoint from .external import GPTIntegrationEndpoint, ReleaseNotesEndpoint, UnsplashEndpoint
from .estimate import ( from .estimate import (
ProjectEstimatePointEndpoint, ProjectEstimatePointEndpoint,
BulkEstimatePointEndpoint, BulkEstimatePointEndpoint,
) )
from .release import ReleaseNotesEndpoint
from .inbox import InboxViewSet, InboxIssueViewSet, InboxIssuePublicViewSet from .inbox import InboxViewSet, InboxIssueViewSet, InboxIssuePublicViewSet
from .analytic import ( from .analytic import (
@ -171,3 +166,5 @@ from .analytic import (
from .notification import NotificationViewSet, UnreadNotificationEndpoint, MarkAllReadNotificationViewSet from .notification import NotificationViewSet, UnreadNotificationEndpoint, MarkAllReadNotificationViewSet
from .exporter import ExportIssuesEndpoint from .exporter import ExportIssuesEndpoint
from .config import ConfigurationEndpoint

View File

@ -1,10 +1,5 @@
# Django imports # Django imports
from django.db.models import ( from django.db.models import Count, Sum, F, Q
Count,
Sum,
F,
Q
)
from django.db.models.functions import ExtractMonth from django.db.models.functions import ExtractMonth
# Third party imports # Third party imports
@ -28,82 +23,156 @@ class AnalyticsEndpoint(BaseAPIView):
] ]
def get(self, request, slug): def get(self, request, slug):
try:
x_axis = request.GET.get("x_axis", False) x_axis = request.GET.get("x_axis", False)
y_axis = request.GET.get("y_axis", False) y_axis = request.GET.get("y_axis", False)
segment = request.GET.get("segment", False)
if not x_axis or not y_axis: valid_xaxis_segment = [
"state_id",
"state__group",
"labels__id",
"assignees__id",
"estimate_point",
"issue_cycle__cycle_id",
"issue_module__module_id",
"priority",
"start_date",
"target_date",
"created_at",
"completed_at",
]
valid_yaxis = [
"issue_count",
"estimate",
]
# Check for x-axis and y-axis as thery are required parameters
if (
not x_axis
or not y_axis
or not x_axis in valid_xaxis_segment
or not y_axis in valid_yaxis
):
return Response( return Response(
{"error": "x-axis and y-axis dimensions are required"}, {
"error": "x-axis and y-axis dimensions are required and the values should be valid"
},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
segment = request.GET.get("segment", False) # If segment is present it cannot be same as x-axis
if segment and (segment not in valid_xaxis_segment or x_axis == segment):
return Response(
{
"error": "Both segment and x axis cannot be same and segment should be valid"
},
status=status.HTTP_400_BAD_REQUEST,
)
# Additional filters that need to be applied
filters = issue_filters(request.GET, "GET") filters = issue_filters(request.GET, "GET")
# Get the issues for the workspace with the additional filters applied
queryset = Issue.issue_objects.filter(workspace__slug=slug, **filters) queryset = Issue.issue_objects.filter(workspace__slug=slug, **filters)
# Get the total issue count
total_issues = queryset.count() total_issues = queryset.count()
# Build the graph payload
distribution = build_graph_plot( distribution = build_graph_plot(
queryset=queryset, x_axis=x_axis, y_axis=y_axis, segment=segment queryset=queryset, x_axis=x_axis, y_axis=y_axis, segment=segment
) )
colors = dict() state_details = {}
if x_axis in ["state__name", "state__group"] or segment in [ if x_axis in ["state_id"] or segment in ["state_id"]:
"state__name", state_details = (
"state__group", Issue.issue_objects.filter(
]: workspace__slug=slug,
if x_axis in ["state__name", "state__group"]: **filters,
key = "name" if x_axis == "state__name" else "group" )
else: .distinct("state_id")
key = "name" if segment == "state__name" else "group" .order_by("state_id")
.values("state_id", "state__name", "state__color")
colors = (
State.objects.filter(
~Q(name="Triage"),
workspace__slug=slug, project_id__in=filters.get("project__in")
).values(key, "color")
if filters.get("project__in", False)
else State.objects.filter(~Q(name="Triage"), workspace__slug=slug).values(key, "color")
) )
if x_axis in ["labels__name"] or segment in ["labels__name"]: label_details = {}
colors = ( if x_axis in ["labels__id"] or segment in ["labels__id"]:
Label.objects.filter( label_details = (
workspace__slug=slug, project_id__in=filters.get("project__in") Issue.objects.filter(
).values("name", "color") workspace__slug=slug, **filters, labels__id__isnull=False
if filters.get("project__in", False)
else Label.objects.filter(workspace__slug=slug).values(
"name", "color"
) )
.distinct("labels__id")
.order_by("labels__id")
.values("labels__id", "labels__color", "labels__name")
) )
assignee_details = {} assignee_details = {}
if x_axis in ["assignees__id"] or segment in ["assignees__id"]: if x_axis in ["assignees__id"] or segment in ["assignees__id"]:
assignee_details = ( assignee_details = (
Issue.issue_objects.filter(workspace__slug=slug, **filters, assignees__avatar__isnull=False) Issue.issue_objects.filter(
workspace__slug=slug, **filters, assignees__avatar__isnull=False
)
.order_by("assignees__id") .order_by("assignees__id")
.distinct("assignees__id") .distinct("assignees__id")
.values("assignees__avatar", "assignees__display_name", "assignees__first_name", "assignees__last_name", "assignees__id") .values(
"assignees__avatar",
"assignees__display_name",
"assignees__first_name",
"assignees__last_name",
"assignees__id",
)
) )
cycle_details = {}
if x_axis in ["issue_cycle__cycle_id"] or segment in ["issue_cycle__cycle_id"]:
cycle_details = (
Issue.issue_objects.filter(
workspace__slug=slug,
**filters,
issue_cycle__cycle_id__isnull=False,
)
.distinct("issue_cycle__cycle_id")
.order_by("issue_cycle__cycle_id")
.values(
"issue_cycle__cycle_id",
"issue_cycle__cycle__name",
)
)
module_details = {}
if x_axis in ["issue_module__module_id"] or segment in [
"issue_module__module_id"
]:
module_details = (
Issue.issue_objects.filter(
workspace__slug=slug,
**filters,
issue_module__module_id__isnull=False,
)
.distinct("issue_module__module_id")
.order_by("issue_module__module_id")
.values(
"issue_module__module_id",
"issue_module__module__name",
)
)
return Response( return Response(
{ {
"total": total_issues, "total": total_issues,
"distribution": distribution, "distribution": distribution,
"extras": {"colors": colors, "assignee_details": assignee_details}, "extras": {
"state_details": state_details,
"assignee_details": assignee_details,
"label_details": label_details,
"cycle_details": cycle_details,
"module_details": module_details,
},
}, },
status=status.HTTP_200_OK, 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 AnalyticViewViewset(BaseViewSet): class AnalyticViewViewset(BaseViewSet):
permission_classes = [ permission_classes = [
@ -128,10 +197,7 @@ class SavedAnalyticEndpoint(BaseAPIView):
] ]
def get(self, request, slug, analytic_id): def get(self, request, slug, analytic_id):
try: analytic_view = AnalyticView.objects.get(pk=analytic_id, workspace__slug=slug)
analytic_view = AnalyticView.objects.get(
pk=analytic_id, workspace__slug=slug
)
filter = analytic_view.query filter = analytic_view.query
queryset = Issue.issue_objects.filter(**filter) queryset = Issue.issue_objects.filter(**filter)
@ -155,18 +221,6 @@ class SavedAnalyticEndpoint(BaseAPIView):
status=status.HTTP_200_OK, status=status.HTTP_200_OK,
) )
except AnalyticView.DoesNotExist:
return Response(
{"error": "Analytic View 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,
)
class ExportAnalyticsEndpoint(BaseAPIView): class ExportAnalyticsEndpoint(BaseAPIView):
permission_classes = [ permission_classes = [
@ -174,13 +228,50 @@ class ExportAnalyticsEndpoint(BaseAPIView):
] ]
def post(self, request, slug): def post(self, request, slug):
try:
x_axis = request.data.get("x_axis", False) x_axis = request.data.get("x_axis", False)
y_axis = request.data.get("y_axis", False) y_axis = request.data.get("y_axis", False)
segment = request.data.get("segment", False)
if not x_axis or not y_axis: valid_xaxis_segment = [
"state_id",
"state__group",
"labels__id",
"assignees__id",
"estimate_point",
"issue_cycle__cycle_id",
"issue_module__module_id",
"priority",
"start_date",
"target_date",
"created_at",
"completed_at",
]
valid_yaxis = [
"issue_count",
"estimate",
]
# Check for x-axis and y-axis as thery are required parameters
if (
not x_axis
or not y_axis
or not x_axis in valid_xaxis_segment
or not y_axis in valid_yaxis
):
return Response( return Response(
{"error": "x-axis and y-axis dimensions are required"}, {
"error": "x-axis and y-axis dimensions are required and the values should be valid"
},
status=status.HTTP_400_BAD_REQUEST,
)
# If segment is present it cannot be same as x-axis
if segment and (segment not in valid_xaxis_segment or x_axis == segment):
return Response(
{
"error": "Both segment and x axis cannot be same and segment should be valid"
},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
@ -194,12 +285,6 @@ class ExportAnalyticsEndpoint(BaseAPIView):
}, },
status=status.HTTP_200_OK, 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 DefaultAnalyticsEndpoint(BaseAPIView): class DefaultAnalyticsEndpoint(BaseAPIView):
@ -208,70 +293,79 @@ class DefaultAnalyticsEndpoint(BaseAPIView):
] ]
def get(self, request, slug): def get(self, request, slug):
try:
filters = issue_filters(request.GET, "GET") filters = issue_filters(request.GET, "GET")
base_issues = Issue.issue_objects.filter(workspace__slug=slug, **filters)
queryset = Issue.issue_objects.filter(workspace__slug=slug, **filters) total_issues = base_issues.count()
total_issues = queryset.count() state_groups = base_issues.annotate(state_group=F("state__group"))
total_issues_classified = ( total_issues_classified = (
queryset.annotate(state_group=F("state__group")) state_groups.values("state_group")
.values("state_group")
.annotate(state_count=Count("state_group")) .annotate(state_count=Count("state_group"))
.order_by("state_group") .order_by("state_group")
) )
open_issues = queryset.filter( open_issues_groups = ["backlog", "unstarted", "started"]
state__group__in=["backlog", "unstarted", "started"] open_issues_queryset = state_groups.filter(state__group__in=open_issues_groups)
).count()
open_issues = open_issues_queryset.count()
open_issues_classified = ( open_issues_classified = (
queryset.filter(state__group__in=["backlog", "unstarted", "started"]) open_issues_queryset.values("state_group")
.annotate(state_group=F("state__group"))
.values("state_group")
.annotate(state_count=Count("state_group")) .annotate(state_count=Count("state_group"))
.order_by("state_group") .order_by("state_group")
) )
issue_completed_month_wise = ( issue_completed_month_wise = (
queryset.filter(completed_at__isnull=False) base_issues.filter(completed_at__isnull=False)
.annotate(month=ExtractMonth("completed_at")) .annotate(month=ExtractMonth("completed_at"))
.values("month") .values("month")
.annotate(count=Count("*")) .annotate(count=Count("*"))
.order_by("month") .order_by("month")
) )
user_details = [
"created_by__first_name",
"created_by__last_name",
"created_by__avatar",
"created_by__display_name",
"created_by__id",
]
most_issue_created_user = ( most_issue_created_user = (
queryset.exclude(created_by=None) base_issues.exclude(created_by=None)
.values("created_by__first_name", "created_by__last_name", "created_by__avatar", "created_by__display_name", "created_by__id") .values(*user_details)
.annotate(count=Count("id")) .annotate(count=Count("id"))
.order_by("-count") .order_by("-count")[:5]
)[:5] )
user_assignee_details = [
"assignees__first_name",
"assignees__last_name",
"assignees__avatar",
"assignees__display_name",
"assignees__id",
]
most_issue_closed_user = ( most_issue_closed_user = (
queryset.filter(completed_at__isnull=False, assignees__isnull=False) base_issues.filter(completed_at__isnull=False)
.values("assignees__first_name", "assignees__last_name", "assignees__avatar", "assignees__display_name", "assignees__id") .exclude(assignees=None)
.values(*user_assignee_details)
.annotate(count=Count("id")) .annotate(count=Count("id"))
.order_by("-count") .order_by("-count")[:5]
)[:5] )
pending_issue_user = ( pending_issue_user = (
queryset.filter(completed_at__isnull=True) base_issues.filter(completed_at__isnull=True)
.values("assignees__first_name", "assignees__last_name", "assignees__avatar", "assignees__display_name", "assignees__id") .values(*user_assignee_details)
.annotate(count=Count("id")) .annotate(count=Count("id"))
.order_by("-count") .order_by("-count")
) )
open_estimate_sum = ( open_estimate_sum = open_issues_queryset.aggregate(sum=Sum("estimate_point"))[
queryset.filter( "sum"
state__group__in=["backlog", "unstarted", "started"] ]
).aggregate(open_estimate_sum=Sum("estimate_point")) total_estimate_sum = base_issues.aggregate(sum=Sum("estimate_point"))["sum"]
)["open_estimate_sum"]
print(open_estimate_sum)
total_estimate_sum = queryset.aggregate(
total_estimate_sum=Sum("estimate_point")
)["total_estimate_sum"]
return Response( return Response(
{ {
@ -288,10 +382,3 @@ class DefaultAnalyticsEndpoint(BaseAPIView):
}, },
status=status.HTTP_200_OK, 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,
)

View File

@ -14,7 +14,6 @@ from plane.api.serializers import APITokenSerializer
class ApiTokenEndpoint(BaseAPIView): class ApiTokenEndpoint(BaseAPIView):
def post(self, request): def post(self, request):
try:
label = request.data.get("label", str(uuid4().hex)) label = request.data.get("label", str(uuid4().hex))
workspace = request.data.get("workspace", False) workspace = request.data.get("workspace", False)
@ -34,37 +33,15 @@ class ApiTokenEndpoint(BaseAPIView):
status=status.HTTP_201_CREATED, status=status.HTTP_201_CREATED,
) )
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def get(self, request): def get(self, request):
try:
api_tokens = APIToken.objects.filter(user=request.user) api_tokens = APIToken.objects.filter(user=request.user)
serializer = APITokenSerializer(api_tokens, many=True) serializer = APITokenSerializer(api_tokens, many=True)
return Response(serializer.data, status=status.HTTP_200_OK) 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,
)
def delete(self, request, pk): def delete(self, request, pk):
try:
api_token = APIToken.objects.get(pk=pk) api_token = APIToken.objects.get(pk=pk)
api_token.delete() api_token.delete()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
except APIToken.DoesNotExist:
return Response(
{"error": "Token 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,
)

View File

@ -18,7 +18,6 @@ class FileAssetEndpoint(BaseAPIView):
""" """
def get(self, request, workspace_id, asset_key): def get(self, request, workspace_id, asset_key):
try:
asset_key = str(workspace_id) + "/" + asset_key asset_key = str(workspace_id) + "/" + asset_key
files = FileAsset.objects.filter(asset=asset_key) files = FileAsset.objects.filter(asset=asset_key)
if files.exists(): if files.exists():
@ -26,16 +25,9 @@ class FileAssetEndpoint(BaseAPIView):
return Response({"data": serializer.data, "status": True}, status=status.HTTP_200_OK) return Response({"data": serializer.data, "status": True}, status=status.HTTP_200_OK)
else: else:
return Response({"error": "Asset key does not exist", "status": False}, status=status.HTTP_200_OK) return Response({"error": "Asset key does not exist", "status": False}, 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,
)
def post(self, request, slug): def post(self, request, slug):
try:
serializer = FileAssetSerializer(data=request.data) serializer = FileAssetSerializer(data=request.data)
if serializer.is_valid(): if serializer.is_valid():
# Get the workspace # Get the workspace
@ -43,17 +35,9 @@ class FileAssetEndpoint(BaseAPIView):
serializer.save(workspace_id=workspace.id) serializer.save(workspace_id=workspace.id)
return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) 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,
)
def delete(self, request, workspace_id, asset_key): def delete(self, request, workspace_id, asset_key):
try:
asset_key = str(workspace_id) + "/" + asset_key asset_key = str(workspace_id) + "/" + asset_key
file_asset = FileAsset.objects.get(asset=asset_key) file_asset = FileAsset.objects.get(asset=asset_key)
# Delete the file from storage # Delete the file from storage
@ -61,65 +45,31 @@ class FileAssetEndpoint(BaseAPIView):
# Delete the file object # Delete the file object
file_asset.delete() file_asset.delete()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
except FileAsset.DoesNotExist:
return Response(
{"error": "File Asset doesn't exist"}, 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 UserAssetsEndpoint(BaseAPIView): class UserAssetsEndpoint(BaseAPIView):
parser_classes = (MultiPartParser, FormParser) parser_classes = (MultiPartParser, FormParser)
def get(self, request, asset_key): def get(self, request, asset_key):
try:
files = FileAsset.objects.filter(asset=asset_key, created_by=request.user) files = FileAsset.objects.filter(asset=asset_key, created_by=request.user)
if files.exists(): if files.exists():
serializer = FileAssetSerializer(files, context={"request": request}) serializer = FileAssetSerializer(files, context={"request": request})
return Response({"data": serializer.data, "status": True}, status=status.HTTP_200_OK) return Response({"data": serializer.data, "status": True}, status=status.HTTP_200_OK)
else: else:
return Response({"error": "Asset key does not exist", "status": False}, status=status.HTTP_200_OK) return Response({"error": "Asset key does not exist", "status": False}, 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,
)
def post(self, request): def post(self, request):
try:
serializer = FileAssetSerializer(data=request.data) serializer = FileAssetSerializer(data=request.data)
if serializer.is_valid(): if serializer.is_valid():
serializer.save() serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, 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,
)
def delete(self, request, asset_key): def delete(self, request, asset_key):
try:
file_asset = FileAsset.objects.get(asset=asset_key, created_by=request.user) file_asset = FileAsset.objects.get(asset=asset_key, created_by=request.user)
# Delete the file from storage # Delete the file from storage
file_asset.asset.delete(save=False) file_asset.asset.delete(save=False)
# Delete the file object # Delete the file object
file_asset.delete() file_asset.delete()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
except FileAsset.DoesNotExist:
return Response(
{"error": "File Asset doesn't exist"}, 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,
)

View File

@ -9,7 +9,6 @@ from django.utils.encoding import (
DjangoUnicodeDecodeError, DjangoUnicodeDecodeError,
) )
from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode
from django.contrib.sites.shortcuts import get_current_site
from django.conf import settings from django.conf import settings
## Third Party Imports ## Third Party Imports
@ -56,11 +55,11 @@ class VerifyEmailEndpoint(BaseAPIView):
return Response( return Response(
{"email": "Successfully activated"}, status=status.HTTP_200_OK {"email": "Successfully activated"}, status=status.HTTP_200_OK
) )
except jwt.ExpiredSignatureError as indentifier: except jwt.ExpiredSignatureError as _indentifier:
return Response( return Response(
{"email": "Activation expired"}, status=status.HTTP_400_BAD_REQUEST {"email": "Activation expired"}, status=status.HTTP_400_BAD_REQUEST
) )
except jwt.exceptions.DecodeError as indentifier: except jwt.exceptions.DecodeError as _indentifier:
return Response( return Response(
{"email": "Invalid token"}, status=status.HTTP_400_BAD_REQUEST {"email": "Invalid token"}, status=status.HTTP_400_BAD_REQUEST
) )
@ -128,7 +127,6 @@ class ResetPasswordEndpoint(BaseAPIView):
class ChangePasswordEndpoint(BaseAPIView): class ChangePasswordEndpoint(BaseAPIView):
def post(self, request): def post(self, request):
try:
serializer = ChangePasswordSerializer(data=request.data) serializer = ChangePasswordSerializer(data=request.data)
user = User.objects.get(pk=request.user.id) user = User.objects.get(pk=request.user.id)
@ -151,9 +149,3 @@ class ChangePasswordEndpoint(BaseAPIView):
return Response(response) return Response(response)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, 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,
)

View File

@ -40,7 +40,6 @@ class SignUpEndpoint(BaseAPIView):
permission_classes = (AllowAny,) permission_classes = (AllowAny,)
def post(self, request): def post(self, request):
try:
if not settings.ENABLE_SIGNUP: if not settings.ENABLE_SIGNUP:
return Response( return Response(
{ {
@ -87,14 +86,11 @@ class SignUpEndpoint(BaseAPIView):
user.token_updated_at = timezone.now() user.token_updated_at = timezone.now()
user.save() user.save()
serialized_user = UserSerializer(user).data
access_token, refresh_token = get_tokens_for_user(user) access_token, refresh_token = get_tokens_for_user(user)
data = { data = {
"access_token": access_token, "access_token": access_token,
"refresh_token": refresh_token, "refresh_token": refresh_token,
"user": serialized_user,
} }
# Send Analytics # Send Analytics
@ -121,19 +117,11 @@ class SignUpEndpoint(BaseAPIView):
return Response(data, status=status.HTTP_200_OK) return Response(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 SignInEndpoint(BaseAPIView): class SignInEndpoint(BaseAPIView):
permission_classes = (AllowAny,) permission_classes = (AllowAny,)
def post(self, request): def post(self, request):
try:
email = request.data.get("email", False) email = request.data.get("email", False)
password = request.data.get("password", False) password = request.data.get("password", False)
@ -180,8 +168,6 @@ class SignInEndpoint(BaseAPIView):
status=status.HTTP_403_FORBIDDEN, status=status.HTTP_403_FORBIDDEN,
) )
serialized_user = UserSerializer(user).data
# settings last active for the user # settings last active for the user
user.last_active = timezone.now() user.last_active = timezone.now()
user.last_login_time = timezone.now() user.last_login_time = timezone.now()
@ -215,32 +201,19 @@ class SignInEndpoint(BaseAPIView):
data = { data = {
"access_token": access_token, "access_token": access_token,
"refresh_token": refresh_token, "refresh_token": refresh_token,
"user": serialized_user,
} }
return Response(data, status=status.HTTP_200_OK) return Response(data, status=status.HTTP_200_OK)
except Exception as e:
capture_exception(e)
return Response(
{
"error": "Something went wrong. Please try again later or contact the support team."
},
status=status.HTTP_400_BAD_REQUEST,
)
class SignOutEndpoint(BaseAPIView): class SignOutEndpoint(BaseAPIView):
def post(self, request): def post(self, request):
try:
refresh_token = request.data.get("refresh_token", False) refresh_token = request.data.get("refresh_token", False)
if not refresh_token: if not refresh_token:
capture_message("No refresh token provided") capture_message("No refresh token provided")
return Response( return Response(
{ {"error": "No refresh token provided"},
"error": "Something went wrong. Please try again later or contact the support team."
},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
@ -254,14 +227,6 @@ class SignOutEndpoint(BaseAPIView):
token = RefreshToken(refresh_token) token = RefreshToken(refresh_token)
token.blacklist() token.blacklist()
return Response({"message": "success"}, status=status.HTTP_200_OK) return Response({"message": "success"}, status=status.HTTP_200_OK)
except Exception as e:
capture_exception(e)
return Response(
{
"error": "Something went wrong. Please try again later or contact the support team."
},
status=status.HTTP_400_BAD_REQUEST,
)
class MagicSignInGenerateEndpoint(BaseAPIView): class MagicSignInGenerateEndpoint(BaseAPIView):
@ -270,7 +235,6 @@ class MagicSignInGenerateEndpoint(BaseAPIView):
] ]
def post(self, request): def post(self, request):
try:
email = request.data.get("email", False) email = request.data.get("email", False)
if not email: if not email:
@ -285,11 +249,11 @@ class MagicSignInGenerateEndpoint(BaseAPIView):
## Generate a random token ## Generate a random token
token = ( token = (
"".join(random.choices(string.ascii_lowercase + string.digits, k=4)) "".join(random.choices(string.ascii_lowercase, k=4))
+ "-" + "-"
+ "".join(random.choices(string.ascii_lowercase + string.digits, k=4)) + "".join(random.choices(string.ascii_lowercase, k=4))
+ "-" + "-"
+ "".join(random.choices(string.ascii_lowercase + string.digits, k=4)) + "".join(random.choices(string.ascii_lowercase, k=4))
) )
ri = redis_instance() ri = redis_instance()
@ -327,17 +291,6 @@ class MagicSignInGenerateEndpoint(BaseAPIView):
magic_link.delay(email, key, token, current_site) magic_link.delay(email, key, token, current_site)
return Response({"key": key}, status=status.HTTP_200_OK) return Response({"key": key}, status=status.HTTP_200_OK)
except ValidationError:
return Response(
{"error": "Please provide a valid email address."},
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 MagicSignInEndpoint(BaseAPIView): class MagicSignInEndpoint(BaseAPIView):
@ -346,7 +299,6 @@ class MagicSignInEndpoint(BaseAPIView):
] ]
def post(self, request): def post(self, request):
try:
user_token = request.data.get("token", "").strip() user_token = request.data.get("token", "").strip()
key = request.data.get("key", False).strip().lower() key = request.data.get("key", False).strip().lower()
@ -383,9 +335,7 @@ class MagicSignInEndpoint(BaseAPIView):
"user": {"email": email, "id": str(user.id)}, "user": {"email": email, "id": str(user.id)},
"device_ctx": { "device_ctx": {
"ip": request.META.get("REMOTE_ADDR"), "ip": request.META.get("REMOTE_ADDR"),
"user_agent": request.META.get( "user_agent": request.META.get("HTTP_USER_AGENT"),
"HTTP_USER_AGENT"
),
}, },
"event_type": "SIGN_IN", "event_type": "SIGN_IN",
}, },
@ -413,9 +363,7 @@ class MagicSignInEndpoint(BaseAPIView):
"user": {"email": email, "id": str(user.id)}, "user": {"email": email, "id": str(user.id)},
"device_ctx": { "device_ctx": {
"ip": request.META.get("REMOTE_ADDR"), "ip": request.META.get("REMOTE_ADDR"),
"user_agent": request.META.get( "user_agent": request.META.get("HTTP_USER_AGENT"),
"HTTP_USER_AGENT"
),
}, },
"event_type": "SIGN_UP", "event_type": "SIGN_UP",
}, },
@ -427,13 +375,11 @@ class MagicSignInEndpoint(BaseAPIView):
user.last_login_uagent = request.META.get("HTTP_USER_AGENT") user.last_login_uagent = request.META.get("HTTP_USER_AGENT")
user.token_updated_at = timezone.now() user.token_updated_at = timezone.now()
user.save() user.save()
serialized_user = UserSerializer(user).data
access_token, refresh_token = get_tokens_for_user(user) access_token, refresh_token = get_tokens_for_user(user)
data = { data = {
"access_token": access_token, "access_token": access_token,
"refresh_token": refresh_token, "refresh_token": refresh_token,
"user": serialized_user,
} }
return Response(data, status=status.HTTP_200_OK) return Response(data, status=status.HTTP_200_OK)
@ -449,10 +395,3 @@ class MagicSignInEndpoint(BaseAPIView):
{"error": "The magic code/link has expired please try again"}, {"error": "The magic code/link has expired please try again"},
status=status.HTTP_400_BAD_REQUEST, 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,
)

View File

@ -5,10 +5,14 @@ import zoneinfo
from django.urls import resolve from django.urls import resolve
from django.conf import settings from django.conf import settings
from django.utils import timezone from django.utils import timezone
# Third part imports from django.db import IntegrityError
from django.core.exceptions import ObjectDoesNotExist, ValidationError
# Third part imports
from rest_framework import status
from rest_framework import status from rest_framework import status
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from rest_framework.response import Response
from rest_framework.exceptions import APIException from rest_framework.exceptions import APIException
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework.filters import SearchFilter from rest_framework.filters import SearchFilter
@ -33,8 +37,6 @@ class TimezoneMixin:
timezone.deactivate() timezone.deactivate()
class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator): class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
model = None model = None
@ -59,7 +61,36 @@ class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
capture_exception(e) capture_exception(e)
raise APIException("Please check the view", status.HTTP_400_BAD_REQUEST) raise APIException("Please check the view", status.HTTP_400_BAD_REQUEST)
def handle_exception(self, exc):
"""
Handle any exception that occurs, by returning an appropriate response,
or re-raising the error.
"""
try:
response = super().handle_exception(exc)
return response
except Exception as e:
if isinstance(e, IntegrityError):
return Response({"error": "The payload is not valid"}, status=status.HTTP_400_BAD_REQUEST)
if isinstance(e, ValidationError):
return Response({"error": "Please provide valid detail"}, status=status.HTTP_400_BAD_REQUEST)
if isinstance(e, ObjectDoesNotExist):
model_name = str(exc).split(" matching query does not exist.")[0]
return Response({"error": f"{model_name} does not exist."}, status=status.HTTP_404_NOT_FOUND)
if isinstance(e, KeyError):
capture_exception(e)
return Response({"error": f"key {e} does not exist"}, status=status.HTTP_400_BAD_REQUEST)
print(e) if settings.DEBUG else print("Server Error")
capture_exception(e)
return Response({"error": "Something went wrong please try again later"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
try:
response = super().dispatch(request, *args, **kwargs) response = super().dispatch(request, *args, **kwargs)
if settings.DEBUG: if settings.DEBUG:
@ -70,6 +101,10 @@ class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
) )
return response return response
except Exception as exc:
response = self.handle_exception(exc)
return exc
@property @property
def workspace_slug(self): def workspace_slug(self):
return self.kwargs.get("slug", None) return self.kwargs.get("slug", None)
@ -104,7 +139,36 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
queryset = backend().filter_queryset(self.request, queryset, self) queryset = backend().filter_queryset(self.request, queryset, self)
return queryset return queryset
def handle_exception(self, exc):
"""
Handle any exception that occurs, by returning an appropriate response,
or re-raising the error.
"""
try:
response = super().handle_exception(exc)
return response
except Exception as e:
if isinstance(e, IntegrityError):
return Response({"error": "The payload is not valid"}, status=status.HTTP_400_BAD_REQUEST)
if isinstance(e, ValidationError):
return Response({"error": "Please provide valid detail"}, status=status.HTTP_400_BAD_REQUEST)
if isinstance(e, ObjectDoesNotExist):
model_name = str(exc).split(" matching query does not exist.")[0]
return Response({"error": f"{model_name} does not exist."}, status=status.HTTP_404_NOT_FOUND)
if isinstance(e, KeyError):
return Response({"error": f"key {e} does not exist"}, status=status.HTTP_400_BAD_REQUEST)
print(e) if settings.DEBUG else print("Server Error")
capture_exception(e)
return Response({"error": "Something went wrong please try again later"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
try:
response = super().dispatch(request, *args, **kwargs) response = super().dispatch(request, *args, **kwargs)
if settings.DEBUG: if settings.DEBUG:
@ -115,6 +179,10 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
) )
return response return response
except Exception as exc:
response = self.handle_exception(exc)
return exc
@property @property
def workspace_slug(self): def workspace_slug(self):
return self.kwargs.get("slug", None) return self.kwargs.get("slug", None)

View File

@ -0,0 +1,33 @@
# Python imports
import os
# Django imports
from django.conf import settings
# Third party imports
from rest_framework.permissions import AllowAny
from rest_framework import status
from rest_framework.response import Response
from sentry_sdk import capture_exception
# Module imports
from .base import BaseAPIView
class ConfigurationEndpoint(BaseAPIView):
permission_classes = [
AllowAny,
]
def get(self, request):
data = {}
data["google"] = os.environ.get("GOOGLE_CLIENT_ID", None)
data["github"] = os.environ.get("GITHUB_CLIENT_ID", None)
data["github_app_name"] = os.environ.get("GITHUB_APP_NAME", None)
data["magic_login"] = (
bool(settings.EMAIL_HOST_USER) and bool(settings.EMAIL_HOST_PASSWORD)
) and os.environ.get("ENABLE_MAGIC_LINK_LOGIN", "0") == "1"
data["email_password_login"] = (
os.environ.get("ENABLE_EMAIL_PASSWORD", "0") == "1"
)
return Response(data, status=status.HTTP_200_OK)

View File

@ -2,9 +2,7 @@
import json import json
# Django imports # Django imports
from django.db import IntegrityError
from django.db.models import ( from django.db.models import (
OuterRef,
Func, Func,
F, F,
Q, Q,
@ -62,29 +60,6 @@ class CycleViewSet(BaseViewSet):
project_id=self.kwargs.get("project_id"), owned_by=self.request.user project_id=self.kwargs.get("project_id"), owned_by=self.request.user
) )
def perform_destroy(self, instance):
cycle_issues = list(
CycleIssue.objects.filter(cycle_id=self.kwargs.get("pk")).values_list(
"issue", flat=True
)
)
issue_activity.delay(
type="cycle.activity.deleted",
requested_data=json.dumps(
{
"cycle_id": str(self.kwargs.get("pk")),
"issues": [str(issue_id) for issue_id in cycle_issues],
}
),
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("pk", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=None,
epoch = int(timezone.now().timestamp())
)
return super().perform_destroy(instance)
def get_queryset(self): def get_queryset(self):
subquery = CycleFavorite.objects.filter( subquery = CycleFavorite.objects.filter(
user=self.request.user, user=self.request.user,
@ -102,48 +77,84 @@ class CycleViewSet(BaseViewSet):
.select_related("workspace") .select_related("workspace")
.select_related("owned_by") .select_related("owned_by")
.annotate(is_favorite=Exists(subquery)) .annotate(is_favorite=Exists(subquery))
.annotate(total_issues=Count("issue_cycle")) .annotate(
total_issues=Count(
"issue_cycle",
filter=Q(
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
)
)
.annotate( .annotate(
completed_issues=Count( completed_issues=Count(
"issue_cycle__issue__state__group", "issue_cycle__issue__state__group",
filter=Q(issue_cycle__issue__state__group="completed"), filter=Q(
issue_cycle__issue__state__group="completed",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
) )
) )
.annotate( .annotate(
cancelled_issues=Count( cancelled_issues=Count(
"issue_cycle__issue__state__group", "issue_cycle__issue__state__group",
filter=Q(issue_cycle__issue__state__group="cancelled"), filter=Q(
issue_cycle__issue__state__group="cancelled",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
) )
) )
.annotate( .annotate(
started_issues=Count( started_issues=Count(
"issue_cycle__issue__state__group", "issue_cycle__issue__state__group",
filter=Q(issue_cycle__issue__state__group="started"), filter=Q(
issue_cycle__issue__state__group="started",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
) )
) )
.annotate( .annotate(
unstarted_issues=Count( unstarted_issues=Count(
"issue_cycle__issue__state__group", "issue_cycle__issue__state__group",
filter=Q(issue_cycle__issue__state__group="unstarted"), filter=Q(
issue_cycle__issue__state__group="unstarted",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
) )
) )
.annotate( .annotate(
backlog_issues=Count( backlog_issues=Count(
"issue_cycle__issue__state__group", "issue_cycle__issue__state__group",
filter=Q(issue_cycle__issue__state__group="backlog"), filter=Q(
issue_cycle__issue__state__group="backlog",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
) )
) )
.annotate(total_estimates=Sum("issue_cycle__issue__estimate_point")) .annotate(total_estimates=Sum("issue_cycle__issue__estimate_point"))
.annotate( .annotate(
completed_estimates=Sum( completed_estimates=Sum(
"issue_cycle__issue__estimate_point", "issue_cycle__issue__estimate_point",
filter=Q(issue_cycle__issue__state__group="completed"), filter=Q(
issue_cycle__issue__state__group="completed",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
) )
) )
.annotate( .annotate(
started_estimates=Sum( started_estimates=Sum(
"issue_cycle__issue__estimate_point", "issue_cycle__issue__estimate_point",
filter=Q(issue_cycle__issue__state__group="started"), filter=Q(
issue_cycle__issue__state__group="started",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
) )
) )
.prefetch_related( .prefetch_related(
@ -163,19 +174,12 @@ class CycleViewSet(BaseViewSet):
) )
def list(self, request, slug, project_id): def list(self, request, slug, project_id):
try:
queryset = self.get_queryset() queryset = self.get_queryset()
cycle_view = request.GET.get("cycle_view", "all") cycle_view = request.GET.get("cycle_view", "all")
order_by = request.GET.get("order_by", "sort_order") order_by = request.GET.get("order_by", "sort_order")
queryset = queryset.order_by(order_by) queryset = queryset.order_by(order_by)
# All Cycles
if cycle_view == "all":
return Response(
CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK
)
# Current Cycle # Current Cycle
if cycle_view == "current": if cycle_view == "current":
queryset = queryset.filter( queryset = queryset.filter(
@ -196,17 +200,30 @@ class CycleViewSet(BaseViewSet):
.annotate(assignee_id=F("assignees__id")) .annotate(assignee_id=F("assignees__id"))
.annotate(avatar=F("assignees__avatar")) .annotate(avatar=F("assignees__avatar"))
.values("display_name", "assignee_id", "avatar") .values("display_name", "assignee_id", "avatar")
.annotate(total_issues=Count("assignee_id")) .annotate(
total_issues=Count(
"assignee_id",
filter=Q(archived_at__isnull=True, is_draft=False),
),
)
.annotate( .annotate(
completed_issues=Count( completed_issues=Count(
"assignee_id", "assignee_id",
filter=Q(completed_at__isnull=False), filter=Q(
completed_at__isnull=False,
archived_at__isnull=True,
is_draft=False,
),
) )
) )
.annotate( .annotate(
pending_issues=Count( pending_issues=Count(
"assignee_id", "assignee_id",
filter=Q(completed_at__isnull=True), filter=Q(
completed_at__isnull=True,
archived_at__isnull=True,
is_draft=False,
),
) )
) )
.order_by("display_name") .order_by("display_name")
@ -222,17 +239,30 @@ class CycleViewSet(BaseViewSet):
.annotate(color=F("labels__color")) .annotate(color=F("labels__color"))
.annotate(label_id=F("labels__id")) .annotate(label_id=F("labels__id"))
.values("label_name", "color", "label_id") .values("label_name", "color", "label_id")
.annotate(total_issues=Count("label_id")) .annotate(
total_issues=Count(
"label_id",
filter=Q(archived_at__isnull=True, is_draft=False),
)
)
.annotate( .annotate(
completed_issues=Count( completed_issues=Count(
"label_id", "label_id",
filter=Q(completed_at__isnull=False), filter=Q(
completed_at__isnull=False,
archived_at__isnull=True,
is_draft=False,
),
) )
) )
.annotate( .annotate(
pending_issues=Count( pending_issues=Count(
"label_id", "label_id",
filter=Q(completed_at__isnull=True), filter=Q(
completed_at__isnull=True,
archived_at__isnull=True,
is_draft=False,
),
) )
) )
.order_by("label_name") .order_by("label_name")
@ -286,19 +316,12 @@ class CycleViewSet(BaseViewSet):
CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK
) )
# If no matching view is found return all cycles
return Response( return Response(
{"error": "No matching view found"}, status=status.HTTP_400_BAD_REQUEST CycleSerializer(queryset, many=True).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,
) )
def create(self, request, slug, project_id): def create(self, request, slug, project_id):
try:
if ( if (
request.data.get("start_date", None) is None request.data.get("start_date", None) is None
and request.data.get("end_date", None) is None and request.data.get("end_date", None) is None
@ -321,18 +344,9 @@ class CycleViewSet(BaseViewSet):
}, },
status=status.HTTP_400_BAD_REQUEST, 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,
)
def partial_update(self, request, slug, project_id, pk): def partial_update(self, request, slug, project_id, pk):
try: cycle = Cycle.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
cycle = Cycle.objects.get(
workspace__slug=slug, project_id=project_id, pk=pk
)
request_data = request.data request_data = request.data
@ -355,19 +369,8 @@ class CycleViewSet(BaseViewSet):
serializer.save() serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except Cycle.DoesNotExist:
return Response(
{"error": "Cycle 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,
)
def retrieve(self, request, slug, project_id, pk): def retrieve(self, request, slug, project_id, pk):
try:
queryset = self.get_queryset().get(pk=pk) queryset = self.get_queryset().get(pk=pk)
# Assignee Distribution # Assignee Distribution
@ -382,20 +385,31 @@ class CycleViewSet(BaseViewSet):
.annotate(assignee_id=F("assignees__id")) .annotate(assignee_id=F("assignees__id"))
.annotate(avatar=F("assignees__avatar")) .annotate(avatar=F("assignees__avatar"))
.annotate(display_name=F("assignees__display_name")) .annotate(display_name=F("assignees__display_name"))
.values( .values("first_name", "last_name", "assignee_id", "avatar", "display_name")
"first_name", "last_name", "assignee_id", "avatar", "display_name" .annotate(
total_issues=Count(
"assignee_id",
filter=Q(archived_at__isnull=True, is_draft=False),
),
) )
.annotate(total_issues=Count("assignee_id"))
.annotate( .annotate(
completed_issues=Count( completed_issues=Count(
"assignee_id", "assignee_id",
filter=Q(completed_at__isnull=False), filter=Q(
completed_at__isnull=False,
archived_at__isnull=True,
is_draft=False,
),
) )
) )
.annotate( .annotate(
pending_issues=Count( pending_issues=Count(
"assignee_id", "assignee_id",
filter=Q(completed_at__isnull=True), filter=Q(
completed_at__isnull=True,
archived_at__isnull=True,
is_draft=False,
),
) )
) )
.order_by("first_name", "last_name") .order_by("first_name", "last_name")
@ -412,17 +426,30 @@ class CycleViewSet(BaseViewSet):
.annotate(color=F("labels__color")) .annotate(color=F("labels__color"))
.annotate(label_id=F("labels__id")) .annotate(label_id=F("labels__id"))
.values("label_name", "color", "label_id") .values("label_name", "color", "label_id")
.annotate(total_issues=Count("label_id")) .annotate(
total_issues=Count(
"label_id",
filter=Q(archived_at__isnull=True, is_draft=False),
),
)
.annotate( .annotate(
completed_issues=Count( completed_issues=Count(
"label_id", "label_id",
filter=Q(completed_at__isnull=False), filter=Q(
completed_at__isnull=False,
archived_at__isnull=True,
is_draft=False,
),
) )
) )
.annotate( .annotate(
pending_issues=Count( pending_issues=Count(
"label_id", "label_id",
filter=Q(completed_at__isnull=True), filter=Q(
completed_at__isnull=True,
archived_at__isnull=True,
is_draft=False,
),
) )
) )
.order_by("label_name") .order_by("label_name")
@ -444,16 +471,31 @@ class CycleViewSet(BaseViewSet):
data, data,
status=status.HTTP_200_OK, status=status.HTTP_200_OK,
) )
except Cycle.DoesNotExist:
return Response( def destroy(self, request, slug, project_id, pk):
{"error": "Cycle Does not exists"}, status=status.HTTP_400_BAD_REQUEST cycle_issues = list(
CycleIssue.objects.filter(cycle_id=self.kwargs.get("pk")).values_list(
"issue", flat=True
) )
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
) )
cycle = Cycle.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
# Delete the cycle
cycle.delete()
issue_activity.delay(
type="cycle.activity.deleted",
requested_data=json.dumps(
{
"cycle_id": str(pk),
"issues": [str(issue_id) for issue_id in cycle_issues],
}
),
actor_id=str(request.user.id),
issue_id=str(pk),
project_id=str(project_id),
current_instance=None,
epoch=int(timezone.now().timestamp()),
)
return Response(status=status.HTTP_204_NO_CONTENT)
class CycleIssueViewSet(BaseViewSet): class CycleIssueViewSet(BaseViewSet):
@ -475,23 +517,6 @@ class CycleIssueViewSet(BaseViewSet):
cycle_id=self.kwargs.get("cycle_id"), cycle_id=self.kwargs.get("cycle_id"),
) )
def perform_destroy(self, instance):
issue_activity.delay(
type="cycle.activity.deleted",
requested_data=json.dumps(
{
"cycle_id": str(self.kwargs.get("cycle_id")),
"issues": [str(instance.issue_id)],
}
),
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("pk", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=None,
epoch = int(timezone.now().timestamp())
)
return super().perform_destroy(instance)
def get_queryset(self): def get_queryset(self):
return self.filter_queryset( return self.filter_queryset(
super() super()
@ -516,7 +541,6 @@ class CycleIssueViewSet(BaseViewSet):
@method_decorator(gzip_page) @method_decorator(gzip_page)
def list(self, request, slug, project_id, cycle_id): def list(self, request, slug, project_id, cycle_id):
try:
order_by = request.GET.get("order_by", "created_at") order_by = request.GET.get("order_by", "created_at")
group_by = request.GET.get("group_by", False) group_by = request.GET.get("group_by", False)
sub_group_by = request.GET.get("sub_group_by", False) sub_group_by = request.GET.get("sub_group_by", False)
@ -547,9 +571,7 @@ class CycleIssueViewSet(BaseViewSet):
.values("count") .values("count")
) )
.annotate( .annotate(
attachment_count=IssueAttachment.objects.filter( attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id"))
issue=OuterRef("id")
)
.order_by() .order_by()
.annotate(count=Func(F("id"), function="Count")) .annotate(count=Func(F("id"), function="Count"))
.values("count") .values("count")
@ -565,24 +587,17 @@ class CycleIssueViewSet(BaseViewSet):
) )
if group_by: if group_by:
grouped_results = group_results(issues_data, group_by, sub_group_by)
return Response( return Response(
group_results(issues_data, group_by, sub_group_by), grouped_results,
status=status.HTTP_200_OK, status=status.HTTP_200_OK,
) )
return Response( return Response(
issues_data, issues_data, status=status.HTTP_200_OK
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,
) )
def create(self, request, slug, project_id, cycle_id): def create(self, request, slug, project_id, cycle_id):
try:
issues = request.data.get("issues", []) issues = request.data.get("issues", [])
if not len(issues): if not len(issues):
@ -664,7 +679,7 @@ class CycleIssueViewSet(BaseViewSet):
), ),
} }
), ),
epoch = int(timezone.now().timestamp()) epoch=int(timezone.now().timestamp()),
) )
# Return all Cycle Issues # Return all Cycle Issues
@ -673,16 +688,27 @@ class CycleIssueViewSet(BaseViewSet):
status=status.HTTP_200_OK, status=status.HTTP_200_OK,
) )
except Cycle.DoesNotExist: def destroy(self, request, slug, project_id, cycle_id, pk):
return Response( cycle_issue = CycleIssue.objects.get(
{"error": "Cycle not found"}, status=status.HTTP_404_NOT_FOUND pk=pk, workspace__slug=slug, project_id=project_id, cycle_id=cycle_id
) )
except Exception as e: issue_id = cycle_issue.issue_id
capture_exception(e) cycle_issue.delete()
return Response( issue_activity.delay(
{"error": "Something went wrong please try again later"}, type="cycle.activity.deleted",
status=status.HTTP_400_BAD_REQUEST, requested_data=json.dumps(
{
"cycle_id": str(self.kwargs.get("cycle_id")),
"issues": [str(issue_id)],
}
),
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("pk", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=None,
epoch=int(timezone.now().timestamp()),
) )
return Response(status=status.HTTP_204_NO_CONTENT)
class CycleDateCheckEndpoint(BaseAPIView): class CycleDateCheckEndpoint(BaseAPIView):
@ -691,7 +717,6 @@ class CycleDateCheckEndpoint(BaseAPIView):
] ]
def post(self, request, slug, project_id): def post(self, request, slug, project_id):
try:
start_date = request.data.get("start_date", False) start_date = request.data.get("start_date", False)
end_date = request.data.get("end_date", False) end_date = request.data.get("end_date", False)
cycle_id = request.data.get("cycle_id") cycle_id = request.data.get("cycle_id")
@ -714,18 +739,12 @@ class CycleDateCheckEndpoint(BaseAPIView):
if cycles.exists(): if cycles.exists():
return Response( return Response(
{ {
"error": "You have a cycle already on the given dates, if you want to create your draft cycle you can do that by removing dates", "error": "You have a cycle already on the given dates, if you want to create a draft cycle you can do that by removing dates",
"status": False, "status": False,
} }
) )
else: else:
return Response({"status": True}, status=status.HTTP_200_OK) return Response({"status": True}, 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 CycleFavoriteViewSet(BaseViewSet): class CycleFavoriteViewSet(BaseViewSet):
@ -742,33 +761,13 @@ class CycleFavoriteViewSet(BaseViewSet):
) )
def create(self, request, slug, project_id): def create(self, request, slug, project_id):
try:
serializer = CycleFavoriteSerializer(data=request.data) serializer = CycleFavoriteSerializer(data=request.data)
if serializer.is_valid(): if serializer.is_valid():
serializer.save(user=request.user, project_id=project_id) serializer.save(user=request.user, project_id=project_id)
return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except IntegrityError as e:
if "already exists" in str(e):
return Response(
{"error": "The cycle is already added to favorites"},
status=status.HTTP_410_GONE,
)
else:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
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,
)
def destroy(self, request, slug, project_id, cycle_id): def destroy(self, request, slug, project_id, cycle_id):
try:
cycle_favorite = CycleFavorite.objects.get( cycle_favorite = CycleFavorite.objects.get(
project=project_id, project=project_id,
user=request.user, user=request.user,
@ -777,17 +776,6 @@ class CycleFavoriteViewSet(BaseViewSet):
) )
cycle_favorite.delete() cycle_favorite.delete()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
except CycleFavorite.DoesNotExist:
return Response(
{"error": "Cycle is not in favorites"},
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 TransferCycleIssueEndpoint(BaseAPIView): class TransferCycleIssueEndpoint(BaseAPIView):
@ -796,7 +784,6 @@ class TransferCycleIssueEndpoint(BaseAPIView):
] ]
def post(self, request, slug, project_id, cycle_id): def post(self, request, slug, project_id, cycle_id):
try:
new_cycle_id = request.data.get("new_cycle_id", False) new_cycle_id = request.data.get("new_cycle_id", False)
if not new_cycle_id: if not new_cycle_id:
@ -837,14 +824,3 @@ class TransferCycleIssueEndpoint(BaseAPIView):
) )
return Response({"message": "Success"}, status=status.HTTP_200_OK) return Response({"message": "Success"}, status=status.HTTP_200_OK)
except Cycle.DoesNotExist:
return Response(
{"error": "New Cycle 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,
)

View File

@ -1,6 +1,3 @@
# Django imports
from django.db import IntegrityError
# Third party imports # Third party imports
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework import status from rest_framework import status
@ -23,7 +20,6 @@ class ProjectEstimatePointEndpoint(BaseAPIView):
] ]
def get(self, request, slug, project_id): def get(self, request, slug, project_id):
try:
project = Project.objects.get(workspace__slug=slug, pk=project_id) project = Project.objects.get(workspace__slug=slug, pk=project_id)
if project.estimate_id is not None: if project.estimate_id is not None:
estimate_points = EstimatePoint.objects.filter( estimate_points = EstimatePoint.objects.filter(
@ -34,12 +30,6 @@ class ProjectEstimatePointEndpoint(BaseAPIView):
serializer = EstimatePointSerializer(estimate_points, many=True) serializer = EstimatePointSerializer(estimate_points, many=True)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
return Response([], status=status.HTTP_200_OK) 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 BulkEstimatePointEndpoint(BaseViewSet): class BulkEstimatePointEndpoint(BaseViewSet):
@ -50,21 +40,13 @@ class BulkEstimatePointEndpoint(BaseViewSet):
serializer_class = EstimateSerializer serializer_class = EstimateSerializer
def list(self, request, slug, project_id): def list(self, request, slug, project_id):
try:
estimates = Estimate.objects.filter( estimates = Estimate.objects.filter(
workspace__slug=slug, project_id=project_id workspace__slug=slug, project_id=project_id
).prefetch_related("points").select_related("workspace", "project") ).prefetch_related("points").select_related("workspace", "project")
serializer = EstimateReadSerializer(estimates, many=True) serializer = EstimateReadSerializer(estimates, many=True)
return Response(serializer.data, status=status.HTTP_200_OK) 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,
)
def create(self, request, slug, project_id): def create(self, request, slug, project_id):
try:
if not request.data.get("estimate", False): if not request.data.get("estimate", False):
return Response( return Response(
{"error": "Estimate is required"}, {"error": "Estimate is required"},
@ -84,13 +66,7 @@ class BulkEstimatePointEndpoint(BaseViewSet):
return Response( return Response(
estimate_serializer.errors, status=status.HTTP_400_BAD_REQUEST estimate_serializer.errors, status=status.HTTP_400_BAD_REQUEST
) )
try:
estimate = estimate_serializer.save(project_id=project_id) estimate = estimate_serializer.save(project_id=project_id)
except IntegrityError:
return Response(
{"errror": "Estimate with the name already exists"},
status=status.HTTP_400_BAD_REQUEST,
)
estimate_points = EstimatePoint.objects.bulk_create( estimate_points = EstimatePoint.objects.bulk_create(
[ [
EstimatePoint( EstimatePoint(
@ -120,20 +96,8 @@ class BulkEstimatePointEndpoint(BaseViewSet):
}, },
status=status.HTTP_200_OK, status=status.HTTP_200_OK,
) )
except Estimate.DoesNotExist:
return Response(
{"error": "Estimate 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,
)
def retrieve(self, request, slug, project_id, estimate_id): def retrieve(self, request, slug, project_id, estimate_id):
try:
estimate = Estimate.objects.get( estimate = Estimate.objects.get(
pk=estimate_id, workspace__slug=slug, project_id=project_id pk=estimate_id, workspace__slug=slug, project_id=project_id
) )
@ -142,19 +106,8 @@ class BulkEstimatePointEndpoint(BaseViewSet):
serializer.data, serializer.data,
status=status.HTTP_200_OK, status=status.HTTP_200_OK,
) )
except Estimate.DoesNotExist:
return Response(
{"error": "Estimate 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,
)
def partial_update(self, request, slug, project_id, estimate_id): def partial_update(self, request, slug, project_id, estimate_id):
try:
if not request.data.get("estimate", False): if not request.data.get("estimate", False):
return Response( return Response(
{"error": "Estimate is required"}, {"error": "Estimate is required"},
@ -176,13 +129,8 @@ class BulkEstimatePointEndpoint(BaseViewSet):
return Response( return Response(
estimate_serializer.errors, status=status.HTTP_400_BAD_REQUEST estimate_serializer.errors, status=status.HTTP_400_BAD_REQUEST
) )
try:
estimate = estimate_serializer.save() estimate = estimate_serializer.save()
except IntegrityError:
return Response(
{"errror": "Estimate with the name already exists"},
status=status.HTTP_400_BAD_REQUEST,
)
estimate_points_data = request.data.get("estimate_points", []) estimate_points_data = request.data.get("estimate_points", [])
@ -209,15 +157,9 @@ class BulkEstimatePointEndpoint(BaseViewSet):
) )
updated_estimate_points.append(estimate_point) updated_estimate_points.append(estimate_point)
try:
EstimatePoint.objects.bulk_update( EstimatePoint.objects.bulk_update(
updated_estimate_points, ["value"], batch_size=10, updated_estimate_points, ["value"], batch_size=10,
) )
except IntegrityError as e:
return Response(
{"error": "Values need to be unique for each key"},
status=status.HTTP_400_BAD_REQUEST,
)
estimate_point_serializer = EstimatePointSerializer(estimate_points, many=True) estimate_point_serializer = EstimatePointSerializer(estimate_points, many=True)
return Response( return Response(
@ -227,27 +169,10 @@ class BulkEstimatePointEndpoint(BaseViewSet):
}, },
status=status.HTTP_200_OK, status=status.HTTP_200_OK,
) )
except Estimate.DoesNotExist:
return Response(
{"error": "Estimate 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,
)
def destroy(self, request, slug, project_id, estimate_id): def destroy(self, request, slug, project_id, estimate_id):
try:
estimate = Estimate.objects.get( estimate = Estimate.objects.get(
pk=estimate_id, workspace__slug=slug, project_id=project_id pk=estimate_id, workspace__slug=slug, project_id=project_id
) )
estimate.delete() estimate.delete()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)

View File

@ -20,7 +20,6 @@ class ExportIssuesEndpoint(BaseAPIView):
serializer_class = ExporterHistorySerializer serializer_class = ExporterHistorySerializer
def post(self, request, slug): def post(self, request, slug):
try:
# Get the workspace # Get the workspace
workspace = Workspace.objects.get(slug=slug) workspace = Workspace.objects.get(slug=slug)
@ -61,20 +60,8 @@ class ExportIssuesEndpoint(BaseAPIView):
{"error": f"Provider '{provider}' not found."}, {"error": f"Provider '{provider}' not found."},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
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,
)
def get(self, request, slug): def get(self, request, slug):
try:
exporter_history = ExporterHistory.objects.filter( exporter_history = ExporterHistory.objects.filter(
workspace__slug=slug workspace__slug=slug
).select_related("workspace","initiated_by") ).select_related("workspace","initiated_by")
@ -92,9 +79,3 @@ class ExportIssuesEndpoint(BaseAPIView):
{"error": "per_page and cursor are required"}, {"error": "per_page and cursor are required"},
status=status.HTTP_400_BAD_REQUEST, 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,
)

View File

@ -0,0 +1,92 @@
# Python imports
import requests
# Third party imports
import openai
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import AllowAny
from sentry_sdk import capture_exception
# Django imports
from django.conf import settings
# Module imports
from .base import BaseAPIView
from plane.api.permissions import ProjectEntityPermission
from plane.db.models import Workspace, Project
from plane.api.serializers import ProjectLiteSerializer, WorkspaceLiteSerializer
from plane.utils.integrations.github import get_release_notes
class GPTIntegrationEndpoint(BaseAPIView):
permission_classes = [
ProjectEntityPermission,
]
def post(self, request, slug, project_id):
if not settings.OPENAI_API_KEY or not settings.GPT_ENGINE:
return Response(
{"error": "OpenAI API key and engine is required"},
status=status.HTTP_400_BAD_REQUEST,
)
prompt = request.data.get("prompt", False)
task = request.data.get("task", False)
if not task:
return Response(
{"error": "Task is required"}, status=status.HTTP_400_BAD_REQUEST
)
final_text = task + "\n" + prompt
openai.api_key = settings.OPENAI_API_KEY
response = openai.ChatCompletion.create(
model=settings.GPT_ENGINE,
messages=[{"role": "user", "content": final_text}],
temperature=0.7,
max_tokens=1024,
)
workspace = Workspace.objects.get(slug=slug)
project = Project.objects.get(pk=project_id)
text = response.choices[0].message.content.strip()
text_html = text.replace("\n", "<br/>")
return Response(
{
"response": text,
"response_html": text_html,
"project_detail": ProjectLiteSerializer(project).data,
"workspace_detail": WorkspaceLiteSerializer(workspace).data,
},
status=status.HTTP_200_OK,
)
class ReleaseNotesEndpoint(BaseAPIView):
def get(self, request):
release_notes = get_release_notes()
return Response(release_notes, status=status.HTTP_200_OK)
class UnsplashEndpoint(BaseAPIView):
def get(self, request):
query = request.GET.get("query", False)
page = request.GET.get("page", 1)
per_page = request.GET.get("per_page", 20)
url = (
f"https://api.unsplash.com/search/photos/?client_id={settings.UNSPLASH_ACCESS_KEY}&query={query}&page=${page}&per_page={per_page}"
if query
else f"https://api.unsplash.com/photos/?client_id={settings.UNSPLASH_ACCESS_KEY}&page={page}&per_page={per_page}"
)
headers = {
"Content-Type": "application/json",
}
resp = requests.get(url=url, headers=headers)
return Response(resp.json(), status=status.HTTP_200_OK)

View File

@ -1,75 +0,0 @@
# Python imports
import requests
# Third party imports
from rest_framework.response import Response
from rest_framework import status
import openai
from sentry_sdk import capture_exception
# Django imports
from django.conf import settings
# Module imports
from .base import BaseAPIView
from plane.api.permissions import ProjectEntityPermission
from plane.db.models import Workspace, Project
from plane.api.serializers import ProjectLiteSerializer, WorkspaceLiteSerializer
class GPTIntegrationEndpoint(BaseAPIView):
permission_classes = [
ProjectEntityPermission,
]
def post(self, request, slug, project_id):
try:
if not settings.OPENAI_API_KEY or not settings.GPT_ENGINE:
return Response(
{"error": "OpenAI API key and engine is required"},
status=status.HTTP_400_BAD_REQUEST,
)
prompt = request.data.get("prompt", False)
task = request.data.get("task", False)
if not task:
return Response(
{"error": "Task is required"}, status=status.HTTP_400_BAD_REQUEST
)
final_text = task + "\n" + prompt
openai.api_key = settings.OPENAI_API_KEY
response = openai.ChatCompletion.create(
model=settings.GPT_ENGINE,
messages=[{"role": "user", "content": final_text}],
temperature=0.7,
max_tokens=1024,
)
workspace = Workspace.objects.get(slug=slug)
project = Project.objects.get(pk=project_id)
text = response.choices[0].message.content.strip()
text_html = text.replace("\n", "<br/>")
return Response(
{
"response": text,
"response_html": text_html,
"project_detail": ProjectLiteSerializer(project).data,
"workspace_detail": WorkspaceLiteSerializer(workspace).data,
},
status=status.HTTP_200_OK,
)
except (Workspace.DoesNotExist, Project.DoesNotExist) as e:
return Response(
{"error": "Workspace or Project 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,
)

View File

@ -39,12 +39,12 @@ from plane.utils.integrations.github import get_github_repo_details
from plane.utils.importers.jira import jira_project_issue_summary from plane.utils.importers.jira import jira_project_issue_summary
from plane.bgtasks.importer_task import service_importer from plane.bgtasks.importer_task import service_importer
from plane.utils.html_processor import strip_tags from plane.utils.html_processor import strip_tags
from plane.api.permissions import WorkSpaceAdminPermission
class ServiceIssueImportSummaryEndpoint(BaseAPIView): class ServiceIssueImportSummaryEndpoint(BaseAPIView):
def get(self, request, slug, service): def get(self, request, slug, service):
try:
if service == "github": if service == "github":
owner = request.GET.get("owner", False) owner = request.GET.get("owner", False)
repo = request.GET.get("repo", False) repo = request.GET.get("repo", False)
@ -117,22 +117,13 @@ class ServiceIssueImportSummaryEndpoint(BaseAPIView):
{"error": "Service not supported yet"}, {"error": "Service not supported yet"},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
except WorkspaceIntegration.DoesNotExist:
return Response(
{"error": "Requested integration was not installed in the workspace"},
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 ImportServiceEndpoint(BaseAPIView): class ImportServiceEndpoint(BaseAPIView):
permission_classes = [
WorkSpaceAdminPermission,
]
def post(self, request, slug, service): def post(self, request, slug, service):
try:
project_id = request.data.get("project_id", False) project_id = request.data.get("project_id", False)
if not project_id: if not project_id:
@ -220,24 +211,8 @@ class ImportServiceEndpoint(BaseAPIView):
{"error": "Servivce not supported yet"}, {"error": "Servivce not supported yet"},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
except (
Workspace.DoesNotExist,
WorkspaceIntegration.DoesNotExist,
Project.DoesNotExist,
) as e:
return Response(
{"error": "Workspace Integration or Project does not exist"},
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,
)
def get(self, request, slug): def get(self, request, slug):
try:
imports = ( imports = (
Importer.objects.filter(workspace__slug=slug) Importer.objects.filter(workspace__slug=slug)
.order_by("-created_at") .order_by("-created_at")
@ -245,15 +220,8 @@ class ImportServiceEndpoint(BaseAPIView):
) )
serializer = ImporterSerializer(imports, many=True) serializer = ImporterSerializer(imports, many=True)
return Response(serializer.data) return Response(serializer.data)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def delete(self, request, slug, service, pk): def delete(self, request, slug, service, pk):
try:
importer = Importer.objects.get( importer = Importer.objects.get(
pk=pk, service=service, workspace__slug=slug pk=pk, service=service, workspace__slug=slug
) )
@ -272,15 +240,8 @@ class ImportServiceEndpoint(BaseAPIView):
Module.objects.filter(id__in=imported_modules).delete() Module.objects.filter(id__in=imported_modules).delete()
importer.delete() importer.delete()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def patch(self, request, slug, service, pk): def patch(self, request, slug, service, pk):
try:
importer = Importer.objects.get( importer = Importer.objects.get(
pk=pk, service=service, workspace__slug=slug pk=pk, service=service, workspace__slug=slug
) )
@ -289,21 +250,10 @@ class ImportServiceEndpoint(BaseAPIView):
serializer.save() serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except Importer.DoesNotExist:
return Response(
{"error": "Importer Does not exists"}, 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 UpdateServiceImportStatusEndpoint(BaseAPIView): class UpdateServiceImportStatusEndpoint(BaseAPIView):
def post(self, request, slug, project_id, service, importer_id): def post(self, request, slug, project_id, service, importer_id):
try:
importer = Importer.objects.get( importer = Importer.objects.get(
pk=importer_id, pk=importer_id,
workspace__slug=slug, workspace__slug=slug,
@ -313,15 +263,10 @@ class UpdateServiceImportStatusEndpoint(BaseAPIView):
importer.status = request.data.get("status", "processing") importer.status = request.data.get("status", "processing")
importer.save() importer.save()
return Response(status.HTTP_200_OK) return Response(status.HTTP_200_OK)
except Importer.DoesNotExist:
return Response(
{"error": "Importer does not exist"}, status=status.HTTP_404_NOT_FOUND
)
class BulkImportIssuesEndpoint(BaseAPIView): class BulkImportIssuesEndpoint(BaseAPIView):
def post(self, request, slug, project_id, service): def post(self, request, slug, project_id, service):
try:
# Get the project # Get the project
project = Project.objects.get(pk=project_id, workspace__slug=slug) project = Project.objects.get(pk=project_id, workspace__slug=slug)
@ -384,7 +329,7 @@ class BulkImportIssuesEndpoint(BaseAPIView):
sort_order=largest_sort_order, sort_order=largest_sort_order,
start_date=issue_data.get("start_date", None), start_date=issue_data.get("start_date", None),
target_date=issue_data.get("target_date", None), target_date=issue_data.get("target_date", None),
priority=issue_data.get("priority", None), priority=issue_data.get("priority", "none"),
created_by=request.user, created_by=request.user,
) )
) )
@ -504,21 +449,10 @@ class BulkImportIssuesEndpoint(BaseAPIView):
{"issues": IssueFlatSerializer(issues, many=True).data}, {"issues": IssueFlatSerializer(issues, many=True).data},
status=status.HTTP_201_CREATED, status=status.HTTP_201_CREATED,
) )
except Project.DoesNotExist:
return Response(
{"error": "Project Does not exist"}, 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 BulkImportModulesEndpoint(BaseAPIView): class BulkImportModulesEndpoint(BaseAPIView):
def post(self, request, slug, project_id, service): def post(self, request, slug, project_id, service):
try:
modules_data = request.data.get("modules_data", []) modules_data = request.data.get("modules_data", [])
project = Project.objects.get(pk=project_id, workspace__slug=slug) project = Project.objects.get(pk=project_id, workspace__slug=slug)
@ -590,13 +524,3 @@ class BulkImportModulesEndpoint(BaseAPIView):
{"message": "Modules created but issues could not be imported"}, {"message": "Modules created but issues could not be imported"},
status=status.HTTP_200_OK, status=status.HTTP_200_OK,
) )
except Project.DoesNotExist:
return Response(
{"error": "Project does not exist"}, 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,
)

View File

@ -64,7 +64,6 @@ class InboxViewSet(BaseViewSet):
serializer.save(project_id=self.kwargs.get("project_id")) serializer.save(project_id=self.kwargs.get("project_id"))
def destroy(self, request, slug, project_id, pk): def destroy(self, request, slug, project_id, pk):
try:
inbox = Inbox.objects.get( inbox = Inbox.objects.get(
workspace__slug=slug, project_id=project_id, pk=pk workspace__slug=slug, project_id=project_id, pk=pk
) )
@ -76,12 +75,6 @@ class InboxViewSet(BaseViewSet):
) )
inbox.delete() inbox.delete()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wronf please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class InboxIssueViewSet(BaseViewSet): class InboxIssueViewSet(BaseViewSet):
@ -110,7 +103,6 @@ class InboxIssueViewSet(BaseViewSet):
) )
def list(self, request, slug, project_id, inbox_id): def list(self, request, slug, project_id, inbox_id):
try:
filters = issue_filters(request.query_params, "GET") filters = issue_filters(request.query_params, "GET")
issues = ( issues = (
Issue.objects.filter( Issue.objects.filter(
@ -158,27 +150,20 @@ class InboxIssueViewSet(BaseViewSet):
status=status.HTTP_200_OK, 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,
)
def create(self, request, slug, project_id, inbox_id): def create(self, request, slug, project_id, inbox_id):
try:
if not request.data.get("issue", {}).get("name", False): if not request.data.get("issue", {}).get("name", False):
return Response( return Response(
{"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST {"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST
) )
# Check for valid priority # Check for valid priority
if not request.data.get("issue", {}).get("priority", None) in [ if not request.data.get("issue", {}).get("priority", "none") in [
"low", "low",
"medium", "medium",
"high", "high",
"urgent", "urgent",
None, "none",
]: ]:
return Response( return Response(
{"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST {"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST
@ -225,15 +210,8 @@ class InboxIssueViewSet(BaseViewSet):
serializer = IssueStateInboxSerializer(issue) serializer = IssueStateInboxSerializer(issue)
return Response(serializer.data, status=status.HTTP_200_OK) 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,
)
def partial_update(self, request, slug, project_id, inbox_id, pk): def partial_update(self, request, slug, project_id, inbox_id, pk):
try:
inbox_issue = InboxIssue.objects.get( inbox_issue = InboxIssue.objects.get(
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
) )
@ -330,20 +308,8 @@ class InboxIssueViewSet(BaseViewSet):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
else: else:
return Response(InboxIssueSerializer(inbox_issue).data, status=status.HTTP_200_OK) return Response(InboxIssueSerializer(inbox_issue).data, status=status.HTTP_200_OK)
except InboxIssue.DoesNotExist:
return Response(
{"error": "Inbox Issue 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,
)
def retrieve(self, request, slug, project_id, inbox_id, pk): def retrieve(self, request, slug, project_id, inbox_id, pk):
try:
inbox_issue = InboxIssue.objects.get( inbox_issue = InboxIssue.objects.get(
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
) )
@ -352,15 +318,8 @@ class InboxIssueViewSet(BaseViewSet):
) )
serializer = IssueStateInboxSerializer(issue) serializer = IssueStateInboxSerializer(issue)
return Response(serializer.data, status=status.HTTP_200_OK) 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,
)
def destroy(self, request, slug, project_id, inbox_id, pk): def destroy(self, request, slug, project_id, inbox_id, pk):
try:
inbox_issue = InboxIssue.objects.get( inbox_issue = InboxIssue.objects.get(
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
) )
@ -370,16 +329,13 @@ class InboxIssueViewSet(BaseViewSet):
if project_member.role <= 10 and str(inbox_issue.created_by_id) != str(request.user.id): if project_member.role <= 10 and str(inbox_issue.created_by_id) != str(request.user.id):
return Response({"error": "You cannot delete inbox issue"}, status=status.HTTP_400_BAD_REQUEST) return Response({"error": "You cannot delete inbox issue"}, status=status.HTTP_400_BAD_REQUEST)
# Check the issue status
if inbox_issue.status in [-2, -1, 0, 2]:
# Delete the issue also
Issue.objects.filter(workspace__slug=slug, project_id=project_id, pk=inbox_issue.issue_id).delete()
inbox_issue.delete() inbox_issue.delete()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
except InboxIssue.DoesNotExist:
return Response({"error": "Inbox Issue 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 InboxIssuePublicViewSet(BaseViewSet): class InboxIssuePublicViewSet(BaseViewSet):
@ -404,11 +360,9 @@ class InboxIssuePublicViewSet(BaseViewSet):
) )
.select_related("issue", "workspace", "project") .select_related("issue", "workspace", "project")
) )
else:
return InboxIssue.objects.none() return InboxIssue.objects.none()
def list(self, request, slug, project_id, inbox_id): def list(self, request, slug, project_id, inbox_id):
try:
project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id) project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id)
if project_deploy_board.inbox is None: if project_deploy_board.inbox is None:
return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST) return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST)
@ -459,17 +413,8 @@ class InboxIssuePublicViewSet(BaseViewSet):
issues_data, issues_data,
status=status.HTTP_200_OK, status=status.HTTP_200_OK,
) )
except ProjectDeployBoard.DoesNotExist:
return Response({"error": "Project Deploy Board 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,
)
def create(self, request, slug, project_id, inbox_id): def create(self, request, slug, project_id, inbox_id):
try:
project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id) project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id)
if project_deploy_board.inbox is None: if project_deploy_board.inbox is None:
return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST) return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST)
@ -480,12 +425,12 @@ class InboxIssuePublicViewSet(BaseViewSet):
) )
# Check for valid priority # Check for valid priority
if not request.data.get("issue", {}).get("priority", None) in [ if not request.data.get("issue", {}).get("priority", "none") in [
"low", "low",
"medium", "medium",
"high", "high",
"urgent", "urgent",
None, "none",
]: ]:
return Response( return Response(
{"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST {"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST
@ -532,15 +477,8 @@ class InboxIssuePublicViewSet(BaseViewSet):
serializer = IssueStateInboxSerializer(issue) serializer = IssueStateInboxSerializer(issue)
return Response(serializer.data, status=status.HTTP_200_OK) 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,
)
def partial_update(self, request, slug, project_id, inbox_id, pk): def partial_update(self, request, slug, project_id, inbox_id, pk):
try:
project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id) project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id)
if project_deploy_board.inbox is None: if project_deploy_board.inbox is None:
return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST) return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST)
@ -590,20 +528,8 @@ class InboxIssuePublicViewSet(BaseViewSet):
issue_serializer.save() issue_serializer.save()
return Response(issue_serializer.data, status=status.HTTP_200_OK) return Response(issue_serializer.data, status=status.HTTP_200_OK)
return Response(issue_serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(issue_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except InboxIssue.DoesNotExist:
return Response(
{"error": "Inbox Issue 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,
)
def retrieve(self, request, slug, project_id, inbox_id, pk): def retrieve(self, request, slug, project_id, inbox_id, pk):
try:
project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id) project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id)
if project_deploy_board.inbox is None: if project_deploy_board.inbox is None:
return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST) return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST)
@ -616,15 +542,8 @@ class InboxIssuePublicViewSet(BaseViewSet):
) )
serializer = IssueStateInboxSerializer(issue) serializer = IssueStateInboxSerializer(issue)
return Response(serializer.data, status=status.HTTP_200_OK) 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,
)
def destroy(self, request, slug, project_id, inbox_id, pk): def destroy(self, request, slug, project_id, inbox_id, pk):
try:
project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id) project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id)
if project_deploy_board.inbox is None: if project_deploy_board.inbox is None:
return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST) return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST)
@ -638,12 +557,3 @@ class InboxIssuePublicViewSet(BaseViewSet):
inbox_issue.delete() inbox_issue.delete()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
except InboxIssue.DoesNotExist:
return Response({"error": "Inbox Issue 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,
)

View File

@ -2,7 +2,6 @@
import uuid import uuid
# Django imports # Django imports
from django.db import IntegrityError
from django.contrib.auth.hashers import make_password from django.contrib.auth.hashers import make_password
# Third party imports # Third party imports
@ -33,21 +32,13 @@ class IntegrationViewSet(BaseViewSet):
model = Integration model = Integration
def create(self, request): def create(self, request):
try:
serializer = IntegrationSerializer(data=request.data) serializer = IntegrationSerializer(data=request.data)
if serializer.is_valid(): if serializer.is_valid():
serializer.save() serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, 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,
)
def partial_update(self, request, pk): def partial_update(self, request, pk):
try:
integration = Integration.objects.get(pk=pk) integration = Integration.objects.get(pk=pk)
if integration.verified: if integration.verified:
return Response( return Response(
@ -64,20 +55,7 @@ class IntegrationViewSet(BaseViewSet):
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except Integration.DoesNotExist:
return Response(
{"error": "Integration Does not exist"},
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,
)
def destroy(self, request, pk): def destroy(self, request, pk):
try:
integration = Integration.objects.get(pk=pk) integration = Integration.objects.get(pk=pk)
if integration.verified: if integration.verified:
return Response( return Response(
@ -87,11 +65,6 @@ class IntegrationViewSet(BaseViewSet):
integration.delete() integration.delete()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
except Integration.DoesNotExist:
return Response(
{"error": "Integration Does not exist"},
status=status.HTTP_404_NOT_FOUND,
)
class WorkspaceIntegrationViewSet(BaseViewSet): class WorkspaceIntegrationViewSet(BaseViewSet):
@ -111,7 +84,6 @@ class WorkspaceIntegrationViewSet(BaseViewSet):
) )
def create(self, request, slug, provider): def create(self, request, slug, provider):
try:
workspace = Workspace.objects.get(slug=slug) workspace = Workspace.objects.get(slug=slug)
integration = Integration.objects.get(provider=provider) integration = Integration.objects.get(provider=provider)
config = {} config = {}
@ -175,33 +147,8 @@ class WorkspaceIntegrationViewSet(BaseViewSet):
WorkspaceIntegrationSerializer(workspace_integration).data, WorkspaceIntegrationSerializer(workspace_integration).data,
status=status.HTTP_201_CREATED, status=status.HTTP_201_CREATED,
) )
except IntegrityError as e:
if "already exists" in str(e):
return Response(
{"error": "Integration is already active in the workspace"},
status=status.HTTP_410_GONE,
)
else:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
except (Workspace.DoesNotExist, Integration.DoesNotExist) as e:
capture_exception(e)
return Response(
{"error": "Workspace or Integration not found"},
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,
)
def destroy(self, request, slug, pk): def destroy(self, request, slug, pk):
try:
workspace_integration = WorkspaceIntegration.objects.get( workspace_integration = WorkspaceIntegration.objects.get(
pk=pk, workspace__slug=slug pk=pk, workspace__slug=slug
) )
@ -215,15 +162,3 @@ class WorkspaceIntegrationViewSet(BaseViewSet):
workspace_integration.delete() workspace_integration.delete()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
except WorkspaceIntegration.DoesNotExist:
return Response(
{"error": "Workspace Integration 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,
)

View File

@ -30,7 +30,6 @@ class GithubRepositoriesEndpoint(BaseAPIView):
] ]
def get(self, request, slug, workspace_integration_id): def get(self, request, slug, workspace_integration_id):
try:
page = request.GET.get("page", 1) page = request.GET.get("page", 1)
workspace_integration = WorkspaceIntegration.objects.get( workspace_integration = WorkspaceIntegration.objects.get(
workspace__slug=slug, pk=workspace_integration_id workspace__slug=slug, pk=workspace_integration_id
@ -49,11 +48,6 @@ class GithubRepositoriesEndpoint(BaseAPIView):
) )
repositories = get_github_repos(access_tokens_url, repositories_url) repositories = get_github_repos(access_tokens_url, repositories_url)
return Response(repositories, status=status.HTTP_200_OK) return Response(repositories, status=status.HTTP_200_OK)
except WorkspaceIntegration.DoesNotExist:
return Response(
{"error": "Workspace Integration Does not exists"},
status=status.HTTP_400_BAD_REQUEST,
)
class GithubRepositorySyncViewSet(BaseViewSet): class GithubRepositorySyncViewSet(BaseViewSet):
@ -76,7 +70,6 @@ class GithubRepositorySyncViewSet(BaseViewSet):
) )
def create(self, request, slug, project_id, workspace_integration_id): def create(self, request, slug, project_id, workspace_integration_id):
try:
name = request.data.get("name", False) name = request.data.get("name", False)
url = request.data.get("url", False) url = request.data.get("url", False)
config = request.data.get("config", {}) config = request.data.get("config", {})
@ -147,18 +140,6 @@ class GithubRepositorySyncViewSet(BaseViewSet):
status=status.HTTP_201_CREATED, status=status.HTTP_201_CREATED,
) )
except WorkspaceIntegration.DoesNotExist:
return Response(
{"error": "Workspace Integration does not exist"},
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 GithubIssueSyncViewSet(BaseViewSet): class GithubIssueSyncViewSet(BaseViewSet):
permission_classes = [ permission_classes = [
@ -177,7 +158,6 @@ class GithubIssueSyncViewSet(BaseViewSet):
class BulkCreateGithubIssueSyncEndpoint(BaseAPIView): class BulkCreateGithubIssueSyncEndpoint(BaseAPIView):
def post(self, request, slug, project_id, repo_sync_id): def post(self, request, slug, project_id, repo_sync_id):
try:
project = Project.objects.get(pk=project_id, workspace__slug=slug) project = Project.objects.get(pk=project_id, workspace__slug=slug)
github_issue_syncs = request.data.get("github_issue_syncs", []) github_issue_syncs = request.data.get("github_issue_syncs", [])
@ -202,17 +182,6 @@ class BulkCreateGithubIssueSyncEndpoint(BaseAPIView):
serializer = GithubIssueSyncSerializer(github_issue_syncs, many=True) serializer = GithubIssueSyncSerializer(github_issue_syncs, many=True)
return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.data, status=status.HTTP_201_CREATED)
except Project.DoesNotExist:
return Response(
{"error": "Project does not exist"},
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 GithubCommentSyncViewSet(BaseViewSet): class GithubCommentSyncViewSet(BaseViewSet):

View File

@ -32,7 +32,6 @@ class SlackProjectSyncViewSet(BaseViewSet):
) )
def create(self, request, slug, project_id, workspace_integration_id): def create(self, request, slug, project_id, workspace_integration_id):
try:
serializer = SlackProjectSyncSerializer(data=request.data) serializer = SlackProjectSyncSerializer(data=request.data)
workspace_integration = WorkspaceIntegration.objects.get( workspace_integration = WorkspaceIntegration.objects.get(
@ -55,19 +54,3 @@ class SlackProjectSyncViewSet(BaseViewSet):
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except IntegrityError:
return Response(
{"error": "Slack is already enabled for the project"},
status=status.HTTP_400_BAD_REQUEST,
)
except WorkspaceIntegration.DoesNotExist:
return Response(
{"error": "Workspace Integration does not exist"},
status=status.HTTP_400_BAD_REQUEST,
)
except Exception as e:
print(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)

View File

@ -24,8 +24,6 @@ from django.core.serializers.json import DjangoJSONEncoder
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.views.decorators.gzip import gzip_page from django.views.decorators.gzip import gzip_page
from django.db import IntegrityError from django.db import IntegrityError
from django.conf import settings
from django.db import IntegrityError
# Third Party imports # Third Party imports
from rest_framework.response import Response from rest_framework.response import Response
@ -58,7 +56,6 @@ from plane.api.serializers import (
IssuePublicSerializer, IssuePublicSerializer,
) )
from plane.api.permissions import ( from plane.api.permissions import (
WorkspaceEntityPermission,
ProjectEntityPermission, ProjectEntityPermission,
WorkSpaceAdminPermission, WorkSpaceAdminPermission,
ProjectMemberPermission, ProjectMemberPermission,
@ -82,8 +79,8 @@ from plane.db.models import (
IssueVote, IssueVote,
IssueRelation, IssueRelation,
ProjectPublicMember, ProjectPublicMember,
IssueLabel,
IssueAssignee, IssueAssignee,
IssueLabel,
) )
from plane.bgtasks.issue_activites_task import issue_activity from plane.bgtasks.issue_activites_task import issue_activity
from plane.utils.grouper import group_results from plane.utils.grouper import group_results
@ -132,7 +129,7 @@ class IssueViewSet(BaseViewSet):
current_instance=json.dumps( current_instance=json.dumps(
IssueSerializer(current_instance).data, cls=DjangoJSONEncoder IssueSerializer(current_instance).data, cls=DjangoJSONEncoder
), ),
epoch=int(timezone.now().timestamp()), epoch=int(timezone.now().timestamp())
) )
return super().perform_update(serializer) return super().perform_update(serializer)
@ -153,7 +150,7 @@ class IssueViewSet(BaseViewSet):
current_instance=json.dumps( current_instance=json.dumps(
IssueSerializer(current_instance).data, cls=DjangoJSONEncoder IssueSerializer(current_instance).data, cls=DjangoJSONEncoder
), ),
epoch=int(timezone.now().timestamp()), epoch=int(timezone.now().timestamp())
) )
return super().perform_destroy(instance) return super().perform_destroy(instance)
@ -284,8 +281,7 @@ class IssueViewSet(BaseViewSet):
if group_by: if group_by:
return Response( return Response(
group_results(issues, group_by, sub_group_by), group_results(issues, group_by, sub_group_by), status=status.HTTP_200_OK
status=status.HTTP_200_OK,
) )
return Response(issues, status=status.HTTP_200_OK) return Response(issues, status=status.HTTP_200_OK)
@ -321,7 +317,7 @@ class IssueViewSet(BaseViewSet):
issue_id=str(serializer.data.get("id", None)), issue_id=str(serializer.data.get("id", None)),
project_id=str(project_id), project_id=str(project_id),
current_instance=None, current_instance=None,
epoch=int(timezone.now().timestamp()), epoch=int(timezone.now().timestamp())
) )
return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@ -338,7 +334,9 @@ class IssueViewSet(BaseViewSet):
.order_by() .order_by()
.annotate(count=Func(F("id"), function="Count")) .annotate(count=Func(F("id"), function="Count"))
.values("count") .values("count")
).get(workspace__slug=slug, project_id=project_id, pk=pk) ).get(
workspace__slug=slug, project_id=project_id, pk=pk
)
return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK) return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK)
except Issue.DoesNotExist: except Issue.DoesNotExist:
return Response( return Response(
@ -473,8 +471,7 @@ class UserWorkSpaceIssues(BaseAPIView):
if group_by: if group_by:
return Response( return Response(
group_results(issues, group_by, sub_group_by), group_results(issues, group_by, sub_group_by), status=status.HTTP_200_OK
status=status.HTTP_200_OK,
) )
return Response(issues, status=status.HTTP_200_OK) return Response(issues, status=status.HTTP_200_OK)
@ -579,7 +576,7 @@ class IssueCommentViewSet(BaseViewSet):
issue_id=str(self.kwargs.get("issue_id")), issue_id=str(self.kwargs.get("issue_id")),
project_id=str(self.kwargs.get("project_id")), project_id=str(self.kwargs.get("project_id")),
current_instance=None, current_instance=None,
epoch=int(timezone.now().timestamp()), epoch=int(timezone.now().timestamp())
) )
def perform_update(self, serializer): def perform_update(self, serializer):
@ -598,7 +595,7 @@ class IssueCommentViewSet(BaseViewSet):
IssueCommentSerializer(current_instance).data, IssueCommentSerializer(current_instance).data,
cls=DjangoJSONEncoder, cls=DjangoJSONEncoder,
), ),
epoch=int(timezone.now().timestamp()), epoch=int(timezone.now().timestamp())
) )
return super().perform_update(serializer) return super().perform_update(serializer)
@ -620,7 +617,7 @@ class IssueCommentViewSet(BaseViewSet):
IssueCommentSerializer(current_instance).data, IssueCommentSerializer(current_instance).data,
cls=DjangoJSONEncoder, cls=DjangoJSONEncoder,
), ),
epoch=int(timezone.now().timestamp()), epoch=int(timezone.now().timestamp())
) )
return super().perform_destroy(instance) return super().perform_destroy(instance)
@ -716,10 +713,18 @@ class LabelViewSet(BaseViewSet):
ProjectMemberPermission, ProjectMemberPermission,
] ]
def perform_create(self, serializer): def create(self, request, slug, project_id):
serializer.save( try:
project_id=self.kwargs.get("project_id"), serializer = LabelSerializer(data=request.data)
) if serializer.is_valid():
serializer.save(project_id=project_id)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except IntegrityError:
return Response({"error": "Label with the same name already exists in the project"}, 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)
def get_queryset(self): def get_queryset(self):
return self.filter_queryset( return self.filter_queryset(
@ -904,7 +909,7 @@ class IssueLinkViewSet(BaseViewSet):
issue_id=str(self.kwargs.get("issue_id")), issue_id=str(self.kwargs.get("issue_id")),
project_id=str(self.kwargs.get("project_id")), project_id=str(self.kwargs.get("project_id")),
current_instance=None, current_instance=None,
epoch=int(timezone.now().timestamp()), epoch=int(timezone.now().timestamp())
) )
def perform_update(self, serializer): def perform_update(self, serializer):
@ -923,7 +928,7 @@ class IssueLinkViewSet(BaseViewSet):
IssueLinkSerializer(current_instance).data, IssueLinkSerializer(current_instance).data,
cls=DjangoJSONEncoder, cls=DjangoJSONEncoder,
), ),
epoch=int(timezone.now().timestamp()), epoch=int(timezone.now().timestamp())
) )
return super().perform_update(serializer) return super().perform_update(serializer)
@ -945,7 +950,7 @@ class IssueLinkViewSet(BaseViewSet):
IssueLinkSerializer(current_instance).data, IssueLinkSerializer(current_instance).data,
cls=DjangoJSONEncoder, cls=DjangoJSONEncoder,
), ),
epoch=int(timezone.now().timestamp()), epoch=int(timezone.now().timestamp())
) )
return super().perform_destroy(instance) return super().perform_destroy(instance)
@ -1024,7 +1029,7 @@ class IssueAttachmentEndpoint(BaseAPIView):
serializer.data, serializer.data,
cls=DjangoJSONEncoder, cls=DjangoJSONEncoder,
), ),
epoch=int(timezone.now().timestamp()), epoch=int(timezone.now().timestamp())
) )
return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@ -1047,7 +1052,7 @@ class IssueAttachmentEndpoint(BaseAPIView):
issue_id=str(self.kwargs.get("issue_id", None)), issue_id=str(self.kwargs.get("issue_id", None)),
project_id=str(self.kwargs.get("project_id", None)), project_id=str(self.kwargs.get("project_id", None)),
current_instance=None, current_instance=None,
epoch=int(timezone.now().timestamp()), epoch=int(timezone.now().timestamp())
) )
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
@ -1250,7 +1255,7 @@ class IssueArchiveViewSet(BaseViewSet):
issue_id=str(issue.id), issue_id=str(issue.id),
project_id=str(project_id), project_id=str(project_id),
current_instance=None, current_instance=None,
epoch=int(timezone.now().timestamp()), epoch=int(timezone.now().timestamp())
) )
return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK) return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK)
@ -1455,7 +1460,7 @@ class IssueReactionViewSet(BaseViewSet):
issue_id=str(self.kwargs.get("issue_id", None)), issue_id=str(self.kwargs.get("issue_id", None)),
project_id=str(self.kwargs.get("project_id", None)), project_id=str(self.kwargs.get("project_id", None)),
current_instance=None, current_instance=None,
epoch=int(timezone.now().timestamp()), epoch=int(timezone.now().timestamp())
) )
def destroy(self, request, slug, project_id, issue_id, reaction_code): def destroy(self, request, slug, project_id, issue_id, reaction_code):
@ -1479,7 +1484,7 @@ class IssueReactionViewSet(BaseViewSet):
"identifier": str(issue_reaction.id), "identifier": str(issue_reaction.id),
} }
), ),
epoch=int(timezone.now().timestamp()), epoch=int(timezone.now().timestamp())
) )
issue_reaction.delete() issue_reaction.delete()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
@ -1528,7 +1533,7 @@ class CommentReactionViewSet(BaseViewSet):
issue_id=None, issue_id=None,
project_id=str(self.kwargs.get("project_id", None)), project_id=str(self.kwargs.get("project_id", None)),
current_instance=None, current_instance=None,
epoch=int(timezone.now().timestamp()), epoch=int(timezone.now().timestamp())
) )
def destroy(self, request, slug, project_id, comment_id, reaction_code): def destroy(self, request, slug, project_id, comment_id, reaction_code):
@ -1553,7 +1558,7 @@ class CommentReactionViewSet(BaseViewSet):
"comment_id": str(comment_id), "comment_id": str(comment_id),
} }
), ),
epoch=int(timezone.now().timestamp()), epoch=int(timezone.now().timestamp())
) )
comment_reaction.delete() comment_reaction.delete()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
@ -1650,7 +1655,7 @@ class IssueCommentPublicViewSet(BaseViewSet):
issue_id=str(issue_id), issue_id=str(issue_id),
project_id=str(project_id), project_id=str(project_id),
current_instance=None, current_instance=None,
epoch=int(timezone.now().timestamp()), epoch=int(timezone.now().timestamp())
) )
if not ProjectMember.objects.filter( if not ProjectMember.objects.filter(
project_id=project_id, project_id=project_id,
@ -1700,7 +1705,7 @@ class IssueCommentPublicViewSet(BaseViewSet):
IssueCommentSerializer(comment).data, IssueCommentSerializer(comment).data,
cls=DjangoJSONEncoder, cls=DjangoJSONEncoder,
), ),
epoch=int(timezone.now().timestamp()), epoch=int(timezone.now().timestamp())
) )
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@ -1734,7 +1739,7 @@ class IssueCommentPublicViewSet(BaseViewSet):
IssueCommentSerializer(comment).data, IssueCommentSerializer(comment).data,
cls=DjangoJSONEncoder, cls=DjangoJSONEncoder,
), ),
epoch=int(timezone.now().timestamp()), epoch=int(timezone.now().timestamp())
) )
comment.delete() comment.delete()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
@ -1809,7 +1814,7 @@ class IssueReactionPublicViewSet(BaseViewSet):
issue_id=str(self.kwargs.get("issue_id", None)), issue_id=str(self.kwargs.get("issue_id", None)),
project_id=str(self.kwargs.get("project_id", None)), project_id=str(self.kwargs.get("project_id", None)),
current_instance=None, current_instance=None,
epoch=int(timezone.now().timestamp()), epoch=int(timezone.now().timestamp())
) )
return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@ -1854,7 +1859,7 @@ class IssueReactionPublicViewSet(BaseViewSet):
"identifier": str(issue_reaction.id), "identifier": str(issue_reaction.id),
} }
), ),
epoch=int(timezone.now().timestamp()), epoch=int(timezone.now().timestamp())
) )
issue_reaction.delete() issue_reaction.delete()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
@ -1928,7 +1933,7 @@ class CommentReactionPublicViewSet(BaseViewSet):
issue_id=None, issue_id=None,
project_id=str(self.kwargs.get("project_id", None)), project_id=str(self.kwargs.get("project_id", None)),
current_instance=None, current_instance=None,
epoch=int(timezone.now().timestamp()), epoch=int(timezone.now().timestamp())
) )
return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@ -1980,7 +1985,7 @@ class CommentReactionPublicViewSet(BaseViewSet):
"comment_id": str(comment_id), "comment_id": str(comment_id),
} }
), ),
epoch=int(timezone.now().timestamp()), epoch=int(timezone.now().timestamp())
) )
comment_reaction.delete() comment_reaction.delete()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
@ -2044,7 +2049,7 @@ class IssueVotePublicViewSet(BaseViewSet):
issue_id=str(self.kwargs.get("issue_id", None)), issue_id=str(self.kwargs.get("issue_id", None)),
project_id=str(self.kwargs.get("project_id", None)), project_id=str(self.kwargs.get("project_id", None)),
current_instance=None, current_instance=None,
epoch=int(timezone.now().timestamp()), epoch=int(timezone.now().timestamp())
) )
serializer = IssueVoteSerializer(issue_vote) serializer = IssueVoteSerializer(issue_vote)
return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.data, status=status.HTTP_201_CREATED)
@ -2079,7 +2084,7 @@ class IssueVotePublicViewSet(BaseViewSet):
"identifier": str(issue_vote.id), "identifier": str(issue_vote.id),
} }
), ),
epoch=int(timezone.now().timestamp()), epoch=int(timezone.now().timestamp())
) )
issue_vote.delete() issue_vote.delete()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
@ -2113,7 +2118,7 @@ class IssueRelationViewSet(BaseViewSet):
IssueRelationSerializer(current_instance).data, IssueRelationSerializer(current_instance).data,
cls=DjangoJSONEncoder, cls=DjangoJSONEncoder,
), ),
epoch=int(timezone.now().timestamp()), epoch=int(timezone.now().timestamp())
) )
return super().perform_destroy(instance) return super().perform_destroy(instance)
@ -2147,7 +2152,7 @@ class IssueRelationViewSet(BaseViewSet):
issue_id=str(issue_id), issue_id=str(issue_id),
project_id=str(project_id), project_id=str(project_id),
current_instance=None, current_instance=None,
epoch=int(timezone.now().timestamp()), epoch=int(timezone.now().timestamp())
) )
if relation == "blocking": if relation == "blocking":
@ -2403,25 +2408,6 @@ class IssueDraftViewSet(BaseViewSet):
serializer_class = IssueFlatSerializer serializer_class = IssueFlatSerializer
model = Issue model = Issue
def perform_update(self, serializer):
requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder)
current_instance = (
self.get_queryset().filter(pk=self.kwargs.get("pk", None)).first()
)
if current_instance is not None:
issue_activity.delay(
type="issue_draft.activity.updated",
requested_data=requested_data,
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("pk", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=json.dumps(
IssueSerializer(current_instance).data, cls=DjangoJSONEncoder
),
epoch=int(timezone.now().timestamp()),
)
return super().perform_update(serializer)
def perform_destroy(self, instance): def perform_destroy(self, instance):
current_instance = ( current_instance = (
@ -2439,9 +2425,11 @@ class IssueDraftViewSet(BaseViewSet):
current_instance=json.dumps( current_instance=json.dumps(
IssueSerializer(current_instance).data, cls=DjangoJSONEncoder IssueSerializer(current_instance).data, cls=DjangoJSONEncoder
), ),
epoch=int(timezone.now().timestamp())
) )
return super().perform_destroy(instance) return super().perform_destroy(instance)
def get_queryset(self): def get_queryset(self):
return ( return (
Issue.objects.annotate( Issue.objects.annotate(
@ -2467,6 +2455,7 @@ class IssueDraftViewSet(BaseViewSet):
) )
) )
@method_decorator(gzip_page) @method_decorator(gzip_page)
def list(self, request, slug, project_id): def list(self, request, slug, project_id):
try: try:
@ -2575,6 +2564,7 @@ class IssueDraftViewSet(BaseViewSet):
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
def create(self, request, slug, project_id): def create(self, request, slug, project_id):
try: try:
project = Project.objects.get(pk=project_id) project = Project.objects.get(pk=project_id)
@ -2599,7 +2589,7 @@ class IssueDraftViewSet(BaseViewSet):
issue_id=str(serializer.data.get("id", None)), issue_id=str(serializer.data.get("id", None)),
project_id=str(project_id), project_id=str(project_id),
current_instance=None, current_instance=None,
epoch=int(timezone.now().timestamp()), epoch=int(timezone.now().timestamp())
) )
return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@ -2609,6 +2599,48 @@ class IssueDraftViewSet(BaseViewSet):
{"error": "Project was not found"}, status=status.HTTP_404_NOT_FOUND {"error": "Project was not found"}, status=status.HTTP_404_NOT_FOUND
) )
def partial_update(self, request, slug, project_id, pk):
try:
issue = Issue.objects.get(
workspace__slug=slug, project_id=project_id, pk=pk
)
serializer = IssueSerializer(
issue, data=request.data, partial=True
)
if serializer.is_valid():
if(request.data.get("is_draft") is not None and not request.data.get("is_draft")):
serializer.save(created_at=timezone.now(), updated_at=timezone.now())
else:
serializer.save()
issue_activity.delay(
type="issue_draft.activity.updated",
requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("pk", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=json.dumps(
IssueSerializer(issue).data,
cls=DjangoJSONEncoder,
),
epoch=int(timezone.now().timestamp())
)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except Issue.DoesNotExist:
return Response(
{"error": "Issue 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,
)
def retrieve(self, request, slug, project_id, pk=None): def retrieve(self, request, slug, project_id, pk=None):
try: try:
issue = Issue.objects.get( issue = Issue.objects.get(
@ -2841,3 +2873,5 @@ class BulkIssueOperationsEndpoint(BaseAPIView):
{"error": "Something went wrong please try again later"}, {"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )

View File

@ -40,6 +40,7 @@ from plane.utils.grouper import group_results
from plane.utils.issue_filters import issue_filters from plane.utils.issue_filters import issue_filters
from plane.utils.analytics_plot import burndown_plot from plane.utils.analytics_plot import burndown_plot
class ModuleViewSet(BaseViewSet): class ModuleViewSet(BaseViewSet):
model = Module model = Module
permission_classes = [ permission_classes = [
@ -78,65 +79,69 @@ class ModuleViewSet(BaseViewSet):
queryset=ModuleLink.objects.select_related("module", "created_by"), queryset=ModuleLink.objects.select_related("module", "created_by"),
) )
) )
.annotate(total_issues=Count("issue_module")) .annotate(
total_issues=Count(
"issue_module",
filter=Q(
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
),
)
.annotate( .annotate(
completed_issues=Count( completed_issues=Count(
"issue_module__issue__state__group", "issue_module__issue__state__group",
filter=Q(issue_module__issue__state__group="completed"), filter=Q(
issue_module__issue__state__group="completed",
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
) )
) )
.annotate( .annotate(
cancelled_issues=Count( cancelled_issues=Count(
"issue_module__issue__state__group", "issue_module__issue__state__group",
filter=Q(issue_module__issue__state__group="cancelled"), filter=Q(
issue_module__issue__state__group="cancelled",
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
) )
) )
.annotate( .annotate(
started_issues=Count( started_issues=Count(
"issue_module__issue__state__group", "issue_module__issue__state__group",
filter=Q(issue_module__issue__state__group="started"), filter=Q(
issue_module__issue__state__group="started",
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
) )
) )
.annotate( .annotate(
unstarted_issues=Count( unstarted_issues=Count(
"issue_module__issue__state__group", "issue_module__issue__state__group",
filter=Q(issue_module__issue__state__group="unstarted"), filter=Q(
issue_module__issue__state__group="unstarted",
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
) )
) )
.annotate( .annotate(
backlog_issues=Count( backlog_issues=Count(
"issue_module__issue__state__group", "issue_module__issue__state__group",
filter=Q(issue_module__issue__state__group="backlog"), filter=Q(
issue_module__issue__state__group="backlog",
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
) )
) )
.order_by(order_by, "name") .order_by(order_by, "name")
) )
def perform_destroy(self, instance):
module_issues = list(
ModuleIssue.objects.filter(module_id=self.kwargs.get("pk")).values_list(
"issue", flat=True
)
)
issue_activity.delay(
type="module.activity.deleted",
requested_data=json.dumps(
{
"module_id": str(self.kwargs.get("pk")),
"issues": [str(issue_id) for issue_id in module_issues],
}
),
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("pk", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=None,
epoch = int(timezone.now().timestamp())
)
return super().perform_destroy(instance)
def create(self, request, slug, project_id): def create(self, request, slug, project_id):
try:
project = Project.objects.get(workspace__slug=slug, pk=project_id) project = Project.objects.get(workspace__slug=slug, pk=project_id)
serializer = ModuleWriteSerializer( serializer = ModuleWriteSerializer(
data=request.data, context={"project": project} data=request.data, context={"project": project}
@ -144,28 +149,13 @@ class ModuleViewSet(BaseViewSet):
if serializer.is_valid(): if serializer.is_valid():
serializer.save() serializer.save()
module = Module.objects.get(pk=serializer.data["id"])
serializer = ModuleSerializer(module)
return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except Project.DoesNotExist:
return Response(
{"error": "Project was not found"}, status=status.HTTP_404_NOT_FOUND
)
except IntegrityError as e:
if "already exists" in str(e):
return Response(
{"name": "The module name is already taken"},
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,
)
def retrieve(self, request, slug, project_id, pk): def retrieve(self, request, slug, project_id, pk):
try:
queryset = self.get_queryset().get(pk=pk) queryset = self.get_queryset().get(pk=pk)
assignee_distribution = ( assignee_distribution = (
@ -180,17 +170,33 @@ class ModuleViewSet(BaseViewSet):
.annotate(display_name=F("assignees__display_name")) .annotate(display_name=F("assignees__display_name"))
.annotate(avatar=F("assignees__avatar")) .annotate(avatar=F("assignees__avatar"))
.values("first_name", "last_name", "assignee_id", "avatar", "display_name") .values("first_name", "last_name", "assignee_id", "avatar", "display_name")
.annotate(total_issues=Count("assignee_id")) .annotate(
total_issues=Count(
"assignee_id",
filter=Q(
archived_at__isnull=True,
is_draft=False,
),
)
)
.annotate( .annotate(
completed_issues=Count( completed_issues=Count(
"assignee_id", "assignee_id",
filter=Q(completed_at__isnull=False), filter=Q(
completed_at__isnull=False,
archived_at__isnull=True,
is_draft=False,
),
) )
) )
.annotate( .annotate(
pending_issues=Count( pending_issues=Count(
"assignee_id", "assignee_id",
filter=Q(completed_at__isnull=True), filter=Q(
completed_at__isnull=True,
archived_at__isnull=True,
is_draft=False,
),
) )
) )
.order_by("first_name", "last_name") .order_by("first_name", "last_name")
@ -206,17 +212,33 @@ class ModuleViewSet(BaseViewSet):
.annotate(color=F("labels__color")) .annotate(color=F("labels__color"))
.annotate(label_id=F("labels__id")) .annotate(label_id=F("labels__id"))
.values("label_name", "color", "label_id") .values("label_name", "color", "label_id")
.annotate(total_issues=Count("label_id")) .annotate(
total_issues=Count(
"label_id",
filter=Q(
archived_at__isnull=True,
is_draft=False,
),
),
)
.annotate( .annotate(
completed_issues=Count( completed_issues=Count(
"label_id", "label_id",
filter=Q(completed_at__isnull=False), filter=Q(
completed_at__isnull=False,
archived_at__isnull=True,
is_draft=False,
),
) )
) )
.annotate( .annotate(
pending_issues=Count( pending_issues=Count(
"label_id", "label_id",
filter=Q(completed_at__isnull=True), filter=Q(
completed_at__isnull=True,
archived_at__isnull=True,
is_draft=False,
),
) )
) )
.order_by("label_name") .order_by("label_name")
@ -239,12 +261,27 @@ class ModuleViewSet(BaseViewSet):
status=status.HTTP_200_OK, status=status.HTTP_200_OK,
) )
except Exception as e: def destroy(self, request, slug, project_id, pk):
capture_exception(e) module = Module.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
return Response( module_issues = list(
{"error": "Something went wrong please try again later"}, ModuleIssue.objects.filter(module_id=pk).values_list("issue", flat=True)
status=status.HTTP_400_BAD_REQUEST,
) )
module.delete()
issue_activity.delay(
type="module.activity.deleted",
requested_data=json.dumps(
{
"module_id": str(pk),
"issues": [str(issue_id) for issue_id in module_issues],
}
),
actor_id=str(request.user.id),
issue_id=str(pk),
project_id=str(project_id),
current_instance=None,
epoch=int(timezone.now().timestamp()),
)
return Response(status=status.HTTP_204_NO_CONTENT)
class ModuleIssueViewSet(BaseViewSet): class ModuleIssueViewSet(BaseViewSet):
@ -266,23 +303,6 @@ class ModuleIssueViewSet(BaseViewSet):
module_id=self.kwargs.get("module_id"), module_id=self.kwargs.get("module_id"),
) )
def perform_destroy(self, instance):
issue_activity.delay(
type="module.activity.deleted",
requested_data=json.dumps(
{
"module_id": str(self.kwargs.get("module_id")),
"issues": [str(instance.issue_id)],
}
),
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("pk", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=None,
epoch = int(timezone.now().timestamp())
)
return super().perform_destroy(instance)
def get_queryset(self): def get_queryset(self):
return self.filter_queryset( return self.filter_queryset(
super() super()
@ -308,7 +328,6 @@ class ModuleIssueViewSet(BaseViewSet):
@method_decorator(gzip_page) @method_decorator(gzip_page)
def list(self, request, slug, project_id, module_id): def list(self, request, slug, project_id, module_id):
try:
order_by = request.GET.get("order_by", "created_at") order_by = request.GET.get("order_by", "created_at")
group_by = request.GET.get("group_by", False) group_by = request.GET.get("group_by", False)
sub_group_by = request.GET.get("sub_group_by", False) sub_group_by = request.GET.get("sub_group_by", False)
@ -339,15 +358,12 @@ class ModuleIssueViewSet(BaseViewSet):
.values("count") .values("count")
) )
.annotate( .annotate(
attachment_count=IssueAttachment.objects.filter( attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id"))
issue=OuterRef("id")
)
.order_by() .order_by()
.annotate(count=Func(F("id"), function="Count")) .annotate(count=Func(F("id"), function="Count"))
.values("count") .values("count")
) )
) )
issues_data = IssueStateSerializer(issues, many=True).data issues_data = IssueStateSerializer(issues, many=True).data
if sub_group_by and sub_group_by == group_by: if sub_group_by and sub_group_by == group_by:
@ -357,24 +373,17 @@ class ModuleIssueViewSet(BaseViewSet):
) )
if group_by: if group_by:
grouped_results = group_results(issues_data, group_by, sub_group_by)
return Response( return Response(
group_results(issues_data, group_by, sub_group_by), grouped_results,
status=status.HTTP_200_OK, status=status.HTTP_200_OK,
) )
return Response( return Response(
issues_data, issues_data, status=status.HTTP_200_OK
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,
) )
def create(self, request, slug, project_id, module_id): def create(self, request, slug, project_id, module_id):
try:
issues = request.data.get("issues", []) issues = request.data.get("issues", [])
if not len(issues): if not len(issues):
return Response( return Response(
@ -447,23 +456,34 @@ class ModuleIssueViewSet(BaseViewSet):
), ),
} }
), ),
epoch = int(timezone.now().timestamp()) epoch=int(timezone.now().timestamp()),
) )
return Response( return Response(
ModuleIssueSerializer(self.get_queryset(), many=True).data, ModuleIssueSerializer(self.get_queryset(), many=True).data,
status=status.HTTP_200_OK, status=status.HTTP_200_OK,
) )
except Module.DoesNotExist:
return Response( def destroy(self, request, slug, project_id, module_id, pk):
{"error": "Module Does not exists"}, status=status.HTTP_400_BAD_REQUEST module_issue = ModuleIssue.objects.get(
workspace__slug=slug, project_id=project_id, module_id=module_id, pk=pk
) )
except Exception as e: module_issue.delete()
capture_exception(e) issue_activity.delay(
return Response( type="module.activity.deleted",
{"error": "Something went wrong please try again later"}, requested_data=json.dumps(
status=status.HTTP_400_BAD_REQUEST, {
"module_id": str(module_id),
"issues": [str(module_issue.issue_id)],
}
),
actor_id=str(request.user.id),
issue_id=str(pk),
project_id=str(project_id),
current_instance=None,
epoch=int(timezone.now().timestamp()),
) )
return Response(status=status.HTTP_204_NO_CONTENT)
class ModuleLinkViewSet(BaseViewSet): class ModuleLinkViewSet(BaseViewSet):
@ -494,7 +514,6 @@ class ModuleLinkViewSet(BaseViewSet):
class ModuleFavoriteViewSet(BaseViewSet): class ModuleFavoriteViewSet(BaseViewSet):
serializer_class = ModuleFavoriteSerializer serializer_class = ModuleFavoriteSerializer
model = ModuleFavorite model = ModuleFavorite
@ -508,33 +527,13 @@ class ModuleFavoriteViewSet(BaseViewSet):
) )
def create(self, request, slug, project_id): def create(self, request, slug, project_id):
try:
serializer = ModuleFavoriteSerializer(data=request.data) serializer = ModuleFavoriteSerializer(data=request.data)
if serializer.is_valid(): if serializer.is_valid():
serializer.save(user=request.user, project_id=project_id) serializer.save(user=request.user, project_id=project_id)
return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except IntegrityError as e:
if "already exists" in str(e):
return Response(
{"error": "The module is already added to favorites"},
status=status.HTTP_410_GONE,
)
else:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
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,
)
def destroy(self, request, slug, project_id, module_id): def destroy(self, request, slug, project_id, module_id):
try:
module_favorite = ModuleFavorite.objects.get( module_favorite = ModuleFavorite.objects.get(
project=project_id, project=project_id,
user=request.user, user=request.user,
@ -543,14 +542,3 @@ class ModuleFavoriteViewSet(BaseViewSet):
) )
module_favorite.delete() module_favorite.delete()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
except ModuleFavorite.DoesNotExist:
return Response(
{"error": "Module is not in favorites"},
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,
)

View File

@ -36,12 +36,10 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
) )
def list(self, request, slug): def list(self, request, slug):
try: # Get query parameters
snoozed = request.GET.get("snoozed", "false") snoozed = request.GET.get("snoozed", "false")
archived = request.GET.get("archived", "false") archived = request.GET.get("archived", "false")
read = request.GET.get("read", "true") read = request.GET.get("read", "true")
# Filter type
type = request.GET.get("type", "all") type = request.GET.get("type", "all")
notifications = ( notifications = (
@ -52,27 +50,24 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
.order_by("snoozed_till", "-created_at") .order_by("snoozed_till", "-created_at")
) )
# Filter for snoozed notifications # Filters based on query parameters
if snoozed == "false": snoozed_filters = {
notifications = notifications.filter( "true": Q(snoozed_till__lt=timezone.now()) | Q(snoozed_till__isnull=False),
Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True), "false": Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True),
) }
if snoozed == "true": notifications = notifications.filter(snoozed_filters[snoozed])
notifications = notifications.filter(
Q(snoozed_till__lt=timezone.now()) | Q(snoozed_till__isnull=False) archived_filters = {
) "true": Q(archived_at__isnull=False),
"false": Q(archived_at__isnull=True),
}
notifications = notifications.filter(archived_filters[archived])
if read == "false": if read == "false":
notifications = notifications.filter(read_at__isnull=True) notifications = notifications.filter(read_at__isnull=True)
# Filter for archived or unarchive
if archived == "false":
notifications = notifications.filter(archived_at__isnull=True)
if archived == "true":
notifications = notifications.filter(archived_at__isnull=False)
# Subscribed issues # Subscribed issues
if type == "watching": if type == "watching":
issue_ids = IssueSubscriber.objects.filter( issue_ids = IssueSubscriber.objects.filter(
@ -97,9 +92,7 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
issue_ids = Issue.objects.filter( issue_ids = Issue.objects.filter(
workspace__slug=slug, created_by=request.user workspace__slug=slug, created_by=request.user
).values_list("pk", flat=True) ).values_list("pk", flat=True)
notifications = notifications.filter( notifications = notifications.filter(entity_identifier__in=issue_ids)
entity_identifier__in=issue_ids
)
# Pagination # Pagination
if request.GET.get("per_page", False) and request.GET.get("cursor", False): if request.GET.get("per_page", False) and request.GET.get("cursor", False):
@ -113,15 +106,8 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
serializer = NotificationSerializer(notifications, many=True) serializer = NotificationSerializer(notifications, many=True)
return Response(serializer.data, status=status.HTTP_200_OK) 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,
)
def partial_update(self, request, slug, pk): def partial_update(self, request, slug, pk):
try:
notification = Notification.objects.get( notification = Notification.objects.get(
workspace__slug=slug, pk=pk, receiver=request.user workspace__slug=slug, pk=pk, receiver=request.user
) )
@ -137,20 +123,8 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
serializer.save() serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except Notification.DoesNotExist:
return Response(
{"error": "Notification 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,
)
def mark_read(self, request, slug, pk): def mark_read(self, request, slug, pk):
try:
notification = Notification.objects.get( notification = Notification.objects.get(
receiver=request.user, workspace__slug=slug, pk=pk receiver=request.user, workspace__slug=slug, pk=pk
) )
@ -158,20 +132,8 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
notification.save() notification.save()
serializer = NotificationSerializer(notification) serializer = NotificationSerializer(notification)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
except Notification.DoesNotExist:
return Response(
{"error": "Notification 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,
)
def mark_unread(self, request, slug, pk): def mark_unread(self, request, slug, pk):
try:
notification = Notification.objects.get( notification = Notification.objects.get(
receiver=request.user, workspace__slug=slug, pk=pk receiver=request.user, workspace__slug=slug, pk=pk
) )
@ -179,20 +141,8 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
notification.save() notification.save()
serializer = NotificationSerializer(notification) serializer = NotificationSerializer(notification)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
except Notification.DoesNotExist:
return Response(
{"error": "Notification 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,
)
def archive(self, request, slug, pk): def archive(self, request, slug, pk):
try:
notification = Notification.objects.get( notification = Notification.objects.get(
receiver=request.user, workspace__slug=slug, pk=pk receiver=request.user, workspace__slug=slug, pk=pk
) )
@ -200,20 +150,8 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
notification.save() notification.save()
serializer = NotificationSerializer(notification) serializer = NotificationSerializer(notification)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
except Notification.DoesNotExist:
return Response(
{"error": "Notification 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,
)
def unarchive(self, request, slug, pk): def unarchive(self, request, slug, pk):
try:
notification = Notification.objects.get( notification = Notification.objects.get(
receiver=request.user, workspace__slug=slug, pk=pk receiver=request.user, workspace__slug=slug, pk=pk
) )
@ -221,22 +159,10 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
notification.save() notification.save()
serializer = NotificationSerializer(notification) serializer = NotificationSerializer(notification)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
except Notification.DoesNotExist:
return Response(
{"error": "Notification 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 UnreadNotificationEndpoint(BaseAPIView): class UnreadNotificationEndpoint(BaseAPIView):
def get(self, request, slug): def get(self, request, slug):
try:
# Watching Issues Count # Watching Issues Count
watching_issues_count = Notification.objects.filter( watching_issues_count = Notification.objects.filter(
workspace__slug=slug, workspace__slug=slug,
@ -278,17 +204,10 @@ class UnreadNotificationEndpoint(BaseAPIView):
}, },
status=status.HTTP_200_OK, 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 MarkAllReadNotificationViewSet(BaseViewSet): class MarkAllReadNotificationViewSet(BaseViewSet):
def create(self, request, slug): def create(self, request, slug):
try:
snoozed = request.data.get("snoozed", False) snoozed = request.data.get("snoozed", False)
archived = request.data.get("archived", False) archived = request.data.get("archived", False)
type = request.data.get("type", "all") type = request.data.get("type", "all")
@ -343,9 +262,7 @@ class MarkAllReadNotificationViewSet(BaseViewSet):
issue_ids = Issue.objects.filter( issue_ids = Issue.objects.filter(
workspace__slug=slug, created_by=request.user workspace__slug=slug, created_by=request.user
).values_list("pk", flat=True) ).values_list("pk", flat=True)
notifications = notifications.filter( notifications = notifications.filter(entity_identifier__in=issue_ids)
entity_identifier__in=issue_ids
)
updated_notifications = [] updated_notifications = []
for notification in notifications: for notification in notifications:
@ -355,9 +272,3 @@ class MarkAllReadNotificationViewSet(BaseViewSet):
updated_notifications, ["read_at"], batch_size=100 updated_notifications, ["read_at"], batch_size=100
) )
return Response({"message": "Successful"}, status=status.HTTP_200_OK) return Response({"message": "Successful"}, 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,
)

View File

@ -11,10 +11,10 @@ from django.conf import settings
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework import exceptions from rest_framework import exceptions
from rest_framework.permissions import AllowAny from rest_framework.permissions import AllowAny
from rest_framework.views import APIView
from rest_framework_simplejwt.tokens import RefreshToken from rest_framework_simplejwt.tokens import RefreshToken
from rest_framework import status from rest_framework import status
from sentry_sdk import capture_exception from sentry_sdk import capture_exception
# sso authentication # sso authentication
from google.oauth2 import id_token from google.oauth2 import id_token
from google.auth.transport import requests as google_auth_request from google.auth.transport import requests as google_auth_request
@ -112,7 +112,7 @@ def get_user_data(access_token: str) -> dict:
url="https://api.github.com/user/emails", headers=headers url="https://api.github.com/user/emails", headers=headers
).json() ).json()
[ _ = [
user_data.update({"email": item.get("email")}) user_data.update({"email": item.get("email")})
for item in response for item in response
if item.get("primary") is True if item.get("primary") is True
@ -146,7 +146,7 @@ class OauthEndpoint(BaseAPIView):
data = get_user_data(access_token) data = get_user_data(access_token)
email = data.get("email", None) email = data.get("email", None)
if email == None: if email is None:
return Response( return Response(
{ {
"error": "Something went wrong. Please try again later or contact the support team." "error": "Something went wrong. Please try again later or contact the support team."
@ -157,7 +157,6 @@ class OauthEndpoint(BaseAPIView):
if "@" in email: if "@" in email:
user = User.objects.get(email=email) user = User.objects.get(email=email)
email = data["email"] email = data["email"]
channel = "email"
mobile_number = uuid.uuid4().hex mobile_number = uuid.uuid4().hex
email_verified = True email_verified = True
else: else:
@ -181,19 +180,16 @@ class OauthEndpoint(BaseAPIView):
user.last_active = timezone.now() user.last_active = timezone.now()
user.last_login_time = timezone.now() user.last_login_time = timezone.now()
user.last_login_ip = request.META.get("REMOTE_ADDR") user.last_login_ip = request.META.get("REMOTE_ADDR")
user.last_login_medium = f"oauth" user.last_login_medium = "oauth"
user.last_login_uagent = request.META.get("HTTP_USER_AGENT") user.last_login_uagent = request.META.get("HTTP_USER_AGENT")
user.is_email_verified = email_verified user.is_email_verified = email_verified
user.save() user.save()
serialized_user = UserSerializer(user).data
access_token, refresh_token = get_tokens_for_user(user) access_token, refresh_token = get_tokens_for_user(user)
data = { data = {
"access_token": access_token, "access_token": access_token,
"refresh_token": refresh_token, "refresh_token": refresh_token,
"user": serialized_user,
} }
SocialLoginConnection.objects.update_or_create( SocialLoginConnection.objects.update_or_create(
@ -235,7 +231,6 @@ class OauthEndpoint(BaseAPIView):
if "@" in email: if "@" in email:
email = data["email"] email = data["email"]
mobile_number = uuid.uuid4().hex mobile_number = uuid.uuid4().hex
channel = "email"
email_verified = True email_verified = True
else: else:
return Response( return Response(
@ -264,14 +259,11 @@ class OauthEndpoint(BaseAPIView):
user.last_login_uagent = request.META.get("HTTP_USER_AGENT") user.last_login_uagent = request.META.get("HTTP_USER_AGENT")
user.token_updated_at = timezone.now() user.token_updated_at = timezone.now()
user.save() user.save()
serialized_user = UserSerializer(user).data
access_token, refresh_token = get_tokens_for_user(user) access_token, refresh_token = get_tokens_for_user(user)
data = { data = {
"access_token": access_token, "access_token": access_token,
"refresh_token": refresh_token, "refresh_token": refresh_token,
"user": serialized_user,
"permissions": [],
} }
if settings.ANALYTICS_BASE_API: if settings.ANALYTICS_BASE_API:
_ = requests.post( _ = requests.post(
@ -304,11 +296,3 @@ class OauthEndpoint(BaseAPIView):
}, },
) )
return Response(data, status=status.HTTP_201_CREATED) return Response(data, status=status.HTTP_201_CREATED)
except Exception as e:
capture_exception(e)
return Response(
{
"error": "Something went wrong. Please try again later or contact the support team."
},
status=status.HTTP_400_BAD_REQUEST,
)

View File

@ -1,8 +1,7 @@
# Python imports # Python imports
from datetime import timedelta, datetime, date from datetime import timedelta, date
# Django imports # Django imports
from django.db import IntegrityError
from django.db.models import Exists, OuterRef, Q, Prefetch from django.db.models import Exists, OuterRef, Q, Prefetch
from django.utils import timezone from django.utils import timezone
@ -78,7 +77,6 @@ class PageViewSet(BaseViewSet):
) )
def create(self, request, slug, project_id): def create(self, request, slug, project_id):
try:
serializer = PageSerializer( serializer = PageSerializer(
data=request.data, data=request.data,
context={"project_id": project_id, "owned_by_id": request.user.id}, context={"project_id": project_id, "owned_by_id": request.user.id},
@ -89,15 +87,7 @@ class PageViewSet(BaseViewSet):
return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, 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,
)
def partial_update(self, request, slug, project_id, pk): def partial_update(self, request, slug, project_id, pk):
try:
page = Page.objects.get(pk=pk, workspace__slug=slug, project_id=project_id) page = Page.objects.get(pk=pk, workspace__slug=slug, project_id=project_id)
# Only update access if the page owner is the requesting user # Only update access if the page owner is the requesting user
if ( if (
@ -115,19 +105,8 @@ class PageViewSet(BaseViewSet):
serializer.save() serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except Page.DoesNotExist:
return Response(
{"error": "Page 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,
)
def list(self, request, slug, project_id): def list(self, request, slug, project_id):
try:
queryset = self.get_queryset() queryset = self.get_queryset()
page_view = request.GET.get("page_view", False) page_view = request.GET.get("page_view", False)
@ -173,9 +152,7 @@ class PageViewSet(BaseViewSet):
return Response(PageSerializer(queryset, many=True).data, status=status.HTTP_200_OK) return Response(PageSerializer(queryset, many=True).data, status=status.HTTP_200_OK)
return Response({"error": "No matching view found"}, status=status.HTTP_400_BAD_REQUEST) return Response({"error": "No matching view found"}, 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 PageBlockViewSet(BaseViewSet): class PageBlockViewSet(BaseViewSet):
serializer_class = PageBlockSerializer serializer_class = PageBlockSerializer
@ -225,33 +202,13 @@ class PageFavoriteViewSet(BaseViewSet):
) )
def create(self, request, slug, project_id): def create(self, request, slug, project_id):
try:
serializer = PageFavoriteSerializer(data=request.data) serializer = PageFavoriteSerializer(data=request.data)
if serializer.is_valid(): if serializer.is_valid():
serializer.save(user=request.user, project_id=project_id) serializer.save(user=request.user, project_id=project_id)
return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except IntegrityError as e:
if "already exists" in str(e):
return Response(
{"error": "The page is already added to favorites"},
status=status.HTTP_410_GONE,
)
else:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
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,
)
def destroy(self, request, slug, project_id, page_id): def destroy(self, request, slug, project_id, page_id):
try:
page_favorite = PageFavorite.objects.get( page_favorite = PageFavorite.objects.get(
project=project_id, project=project_id,
user=request.user, user=request.user,
@ -260,18 +217,6 @@ class PageFavoriteViewSet(BaseViewSet):
) )
page_favorite.delete() page_favorite.delete()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
except PageFavorite.DoesNotExist:
return Response(
{"error": "Page is not in favorites"},
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 CreateIssueFromPageBlockEndpoint(BaseAPIView): class CreateIssueFromPageBlockEndpoint(BaseAPIView):
permission_classes = [ permission_classes = [
@ -279,7 +224,6 @@ class CreateIssueFromPageBlockEndpoint(BaseAPIView):
] ]
def post(self, request, slug, project_id, page_id, page_block_id): def post(self, request, slug, project_id, page_id, page_block_id):
try:
page_block = PageBlock.objects.get( page_block = PageBlock.objects.get(
pk=page_block_id, pk=page_block_id,
workspace__slug=slug, workspace__slug=slug,
@ -309,13 +253,3 @@ class CreateIssueFromPageBlockEndpoint(BaseAPIView):
page_block.save() page_block.save()
return Response(IssueLiteSerializer(issue).data, status=status.HTTP_200_OK) return Response(IssueLiteSerializer(issue).data, status=status.HTTP_200_OK)
except PageBlock.DoesNotExist:
return Response(
{"error": "Page Block does not exist"}, 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,
)

View File

@ -1,15 +1,16 @@
# Python imports # Python imports
import jwt import jwt
import boto3
from datetime import datetime from datetime import datetime
# Django imports # Django imports
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import IntegrityError from django.db import IntegrityError
from django.db.models import ( from django.db.models import (
Prefetch,
Q, Q,
Exists, Exists,
OuterRef, OuterRef,
Func,
F, F,
Func, Func,
Subquery, Subquery,
@ -28,11 +29,11 @@ from sentry_sdk import capture_exception
from .base import BaseViewSet, BaseAPIView from .base import BaseViewSet, BaseAPIView
from plane.api.serializers import ( from plane.api.serializers import (
ProjectSerializer, ProjectSerializer,
ProjectListSerializer,
ProjectMemberSerializer, ProjectMemberSerializer,
ProjectDetailSerializer, ProjectDetailSerializer,
ProjectMemberInviteSerializer, ProjectMemberInviteSerializer,
ProjectFavoriteSerializer, ProjectFavoriteSerializer,
IssueLiteSerializer,
ProjectDeployBoardSerializer, ProjectDeployBoardSerializer,
ProjectMemberAdminSerializer, ProjectMemberAdminSerializer,
) )
@ -66,6 +67,7 @@ from plane.db.models import (
ModuleMember, ModuleMember,
Inbox, Inbox,
ProjectDeployBoard, ProjectDeployBoard,
IssueProperty,
) )
from plane.bgtasks.project_invitation_task import project_invitation from plane.bgtasks.project_invitation_task import project_invitation
@ -80,17 +82,11 @@ class ProjectViewSet(BaseViewSet):
] ]
def get_serializer_class(self, *args, **kwargs): def get_serializer_class(self, *args, **kwargs):
if self.action == "update" or self.action == "partial_update": if self.action in ["update", "partial_update"]:
return ProjectSerializer return ProjectSerializer
return ProjectDetailSerializer return ProjectDetailSerializer
def get_queryset(self): def get_queryset(self):
subquery = ProjectFavorite.objects.filter(
user=self.request.user,
project_id=OuterRef("pk"),
workspace__slug=self.kwargs.get("slug"),
)
return self.filter_queryset( return self.filter_queryset(
super() super()
.get_queryset() .get_queryset()
@ -99,7 +95,15 @@ class ProjectViewSet(BaseViewSet):
.select_related( .select_related(
"workspace", "workspace__owner", "default_assignee", "project_lead" "workspace", "workspace__owner", "default_assignee", "project_lead"
) )
.annotate(is_favorite=Exists(subquery)) .annotate(
is_favorite=Exists(
ProjectFavorite.objects.filter(
user=self.request.user,
project_id=OuterRef("pk"),
workspace__slug=self.kwargs.get("slug"),
)
)
)
.annotate( .annotate(
is_member=Exists( is_member=Exists(
ProjectMember.objects.filter( ProjectMember.objects.filter(
@ -147,13 +151,8 @@ class ProjectViewSet(BaseViewSet):
) )
def list(self, request, slug): def list(self, request, slug):
try: fields = [field for field in request.GET.get("fields", "").split(",") if field]
is_favorite = request.GET.get("is_favorite", "all")
subquery = ProjectFavorite.objects.filter(
user=self.request.user,
project_id=OuterRef("pk"),
workspace__slug=self.kwargs.get("slug"),
)
sort_order_query = ProjectMember.objects.filter( sort_order_query = ProjectMember.objects.filter(
member=request.user, member=request.user,
project_id=OuterRef("pk"), project_id=OuterRef("pk"),
@ -161,42 +160,30 @@ class ProjectViewSet(BaseViewSet):
).values("sort_order") ).values("sort_order")
projects = ( projects = (
self.get_queryset() self.get_queryset()
.annotate(is_favorite=Exists(subquery))
.annotate(sort_order=Subquery(sort_order_query)) .annotate(sort_order=Subquery(sort_order_query))
.prefetch_related(
Prefetch(
"project_projectmember",
queryset=ProjectMember.objects.filter(
workspace__slug=slug,
).select_related("member"),
)
)
.order_by("sort_order", "name") .order_by("sort_order", "name")
.annotate(
total_members=ProjectMember.objects.filter(
project_id=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
total_cycles=Cycle.objects.filter(project_id=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
total_modules=Module.objects.filter(project_id=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
) )
if request.GET.get("per_page", False) and request.GET.get("cursor", False):
return self.paginate(
request=request,
queryset=(projects),
on_results=lambda projects: ProjectListSerializer(
projects, many=True
).data,
) )
if is_favorite == "true":
projects = projects.filter(is_favorite=True)
if is_favorite == "false":
projects = projects.filter(is_favorite=False)
return Response(ProjectDetailSerializer(projects, many=True).data)
except Exception as e:
capture_exception(e)
return Response( return Response(
{"error": "Something went wrong please try again later"}, ProjectListSerializer(
status=status.HTTP_400_BAD_REQUEST, projects, many=True, fields=fields if fields else None
).data
) )
def create(self, request, slug): def create(self, request, slug):
@ -213,6 +200,11 @@ class ProjectViewSet(BaseViewSet):
project_member = ProjectMember.objects.create( project_member = ProjectMember.objects.create(
project_id=serializer.data["id"], member=request.user, role=20 project_id=serializer.data["id"], member=request.user, role=20
) )
# Also create the issue property for the user
_ = IssueProperty.objects.create(
project_id=serializer.data["id"],
user=request.user,
)
if serializer.data["project_lead"] is not None and str( if serializer.data["project_lead"] is not None and str(
serializer.data["project_lead"] serializer.data["project_lead"]
@ -222,6 +214,11 @@ class ProjectViewSet(BaseViewSet):
member_id=serializer.data["project_lead"], member_id=serializer.data["project_lead"],
role=20, role=20,
) )
# Also create the issue property for the user
IssueProperty.objects.create(
project_id=serializer.data["id"],
user_id=serializer.data["project_lead"],
)
# Default states # Default states
states = [ states = [
@ -274,12 +271,9 @@ class ProjectViewSet(BaseViewSet):
] ]
) )
data = serializer.data project = self.get_queryset().filter(pk=serializer.data["id"]).first()
# Additional fields of the member serializer = ProjectListSerializer(project)
data["sort_order"] = project_member.sort_order return Response(serializer.data, status=status.HTTP_201_CREATED)
data["member_role"] = project_member.role
data["is_member"] = True
return Response(data, status=status.HTTP_201_CREATED)
return Response( return Response(
serializer.errors, serializer.errors,
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
@ -290,12 +284,6 @@ class ProjectViewSet(BaseViewSet):
{"name": "The project name is already taken"}, {"name": "The project name is already taken"},
status=status.HTTP_410_GONE, status=status.HTTP_410_GONE,
) )
else:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_410_GONE,
)
except Workspace.DoesNotExist as e: except Workspace.DoesNotExist as e:
return Response( return Response(
{"error": "Workspace does not exist"}, status=status.HTTP_404_NOT_FOUND {"error": "Workspace does not exist"}, status=status.HTTP_404_NOT_FOUND
@ -305,12 +293,6 @@ class ProjectViewSet(BaseViewSet):
{"identifier": "The project identifier is already taken"}, {"identifier": "The project identifier is already taken"},
status=status.HTTP_410_GONE, 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,
)
def partial_update(self, request, slug, pk=None): def partial_update(self, request, slug, pk=None):
try: try:
@ -341,6 +323,8 @@ class ProjectViewSet(BaseViewSet):
color="#ff7700", color="#ff7700",
) )
project = self.get_queryset().filter(pk=serializer.data["id"]).first()
serializer = ProjectListSerializer(project)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@ -350,7 +334,7 @@ class ProjectViewSet(BaseViewSet):
{"name": "The project name is already taken"}, {"name": "The project name is already taken"},
status=status.HTTP_410_GONE, status=status.HTTP_410_GONE,
) )
except Project.DoesNotExist or Workspace.DoesNotExist as e: except (Project.DoesNotExist, Workspace.DoesNotExist):
return Response( return Response(
{"error": "Project does not exist"}, status=status.HTTP_404_NOT_FOUND {"error": "Project does not exist"}, status=status.HTTP_404_NOT_FOUND
) )
@ -359,12 +343,6 @@ class ProjectViewSet(BaseViewSet):
{"identifier": "The project identifier is already taken"}, {"identifier": "The project identifier is already taken"},
status=status.HTTP_410_GONE, 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 InviteProjectEndpoint(BaseAPIView): class InviteProjectEndpoint(BaseAPIView):
@ -373,7 +351,6 @@ class InviteProjectEndpoint(BaseAPIView):
] ]
def post(self, request, slug, project_id): def post(self, request, slug, project_id):
try:
email = request.data.get("email", False) email = request.data.get("email", False)
role = request.data.get("role", False) role = request.data.get("role", False)
@ -424,29 +401,12 @@ class InviteProjectEndpoint(BaseAPIView):
member=user, project_id=project_id, role=role member=user, project_id=project_id, role=role
) )
_ = IssueProperty.objects.create(user=user, project_id=project_id)
return Response( return Response(
ProjectMemberSerializer(project_member).data, status=status.HTTP_200_OK ProjectMemberSerializer(project_member).data, status=status.HTTP_200_OK
) )
except ValidationError:
return Response(
{
"error": "Invalid email address provided a valid email address is required to send the invite"
},
status=status.HTTP_400_BAD_REQUEST,
)
except (Workspace.DoesNotExist, Project.DoesNotExist) as e:
return Response(
{"error": "Workspace or Project 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 UserProjectInvitationsViewset(BaseViewSet): class UserProjectInvitationsViewset(BaseViewSet):
serializer_class = ProjectMemberInviteSerializer serializer_class = ProjectMemberInviteSerializer
@ -461,7 +421,6 @@ class UserProjectInvitationsViewset(BaseViewSet):
) )
def create(self, request): def create(self, request):
try:
invitations = request.data.get("invitations") invitations = request.data.get("invitations")
project_invitations = ProjectMemberInvite.objects.filter( project_invitations = ProjectMemberInvite.objects.filter(
pk__in=invitations, accepted=True pk__in=invitations, accepted=True
@ -479,23 +438,29 @@ class UserProjectInvitationsViewset(BaseViewSet):
] ]
) )
IssueProperty.objects.bulk_create(
[
ProjectMember(
project=invitation.project,
workspace=invitation.project.workspace,
user=request.user,
created_by=request.user,
)
for invitation in project_invitations
]
)
# Delete joined project invites # Delete joined project invites
project_invitations.delete() project_invitations.delete()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class ProjectMemberViewSet(BaseViewSet): class ProjectMemberViewSet(BaseViewSet):
serializer_class = ProjectMemberAdminSerializer serializer_class = ProjectMemberAdminSerializer
model = ProjectMember model = ProjectMember
permission_classes = [ permission_classes = [
ProjectBasePermission, ProjectMemberPermission,
] ]
search_fields = [ search_fields = [
@ -515,8 +480,84 @@ class ProjectMemberViewSet(BaseViewSet):
.select_related("workspace", "workspace__owner") .select_related("workspace", "workspace__owner")
) )
def create(self, request, slug, project_id):
members = request.data.get("members", [])
# get the project
project = Project.objects.get(pk=project_id, workspace__slug=slug)
if not len(members):
return Response(
{"error": "Atleast one member is required"},
status=status.HTTP_400_BAD_REQUEST,
)
bulk_project_members = []
bulk_issue_props = []
project_members = (
ProjectMember.objects.filter(
workspace__slug=slug,
member_id__in=[member.get("member_id") for member in members],
)
.values("member_id", "sort_order")
.order_by("sort_order")
)
for member in members:
sort_order = [
project_member.get("sort_order")
for project_member in project_members
if str(project_member.get("member_id")) == str(member.get("member_id"))
]
bulk_project_members.append(
ProjectMember(
member_id=member.get("member_id"),
role=member.get("role", 10),
project_id=project_id,
workspace_id=project.workspace_id,
sort_order=sort_order[0] - 10000 if len(sort_order) else 65535,
)
)
bulk_issue_props.append(
IssueProperty(
user_id=member.get("member_id"),
project_id=project_id,
workspace_id=project.workspace_id,
)
)
project_members = ProjectMember.objects.bulk_create(
bulk_project_members,
batch_size=10,
ignore_conflicts=True,
)
_ = IssueProperty.objects.bulk_create(
bulk_issue_props, batch_size=10, ignore_conflicts=True
)
serializer = ProjectMemberSerializer(project_members, many=True)
return Response(serializer.data, status=status.HTTP_201_CREATED)
def list(self, request, slug, project_id):
project_member = ProjectMember.objects.get(
member=request.user, workspace__slug=slug, project_id=project_id
)
project_members = ProjectMember.objects.filter(
project_id=project_id,
workspace__slug=slug,
member__is_bot=False,
).select_related("project", "member", "workspace")
if project_member.role > 10:
serializer = ProjectMemberAdminSerializer(project_members, many=True)
else:
serializer = ProjectMemberSerializer(project_members, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
def partial_update(self, request, slug, project_id, pk): def partial_update(self, request, slug, project_id, pk):
try:
project_member = ProjectMember.objects.get( project_member = ProjectMember.objects.get(
pk=pk, workspace__slug=slug, project_id=project_id pk=pk, workspace__slug=slug, project_id=project_id
) )
@ -535,9 +576,7 @@ class ProjectMemberViewSet(BaseViewSet):
> requested_project_member.role > requested_project_member.role
): ):
return Response( return Response(
{ {"error": "You cannot update a role that is higher than your own role"},
"error": "You cannot update a role that is higher than your own role"
},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
@ -549,20 +588,8 @@ class ProjectMemberViewSet(BaseViewSet):
serializer.save() serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except ProjectMember.DoesNotExist:
return Response(
{"error": "Project Member 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,
)
def destroy(self, request, slug, project_id, pk): def destroy(self, request, slug, project_id, pk):
try:
project_member = ProjectMember.objects.get( project_member = ProjectMember.objects.get(
workspace__slug=slug, project_id=project_id, pk=pk workspace__slug=slug, project_id=project_id, pk=pk
) )
@ -572,9 +599,7 @@ class ProjectMemberViewSet(BaseViewSet):
) )
if requesting_project_member.role < project_member.role: if requesting_project_member.role < project_member.role:
return Response( return Response(
{ {"error": "You cannot remove a user having role higher than yourself"},
"error": "You cannot remove a user having role higher than yourself"
},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
@ -615,88 +640,6 @@ class ProjectMemberViewSet(BaseViewSet):
).delete() ).delete()
project_member.delete() project_member.delete()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
except ProjectMember.DoesNotExist:
return Response(
{"error": "Project Member 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"})
class AddMemberToProjectEndpoint(BaseAPIView):
permission_classes = [
ProjectBasePermission,
]
def post(self, request, slug, project_id):
try:
members = request.data.get("members", [])
# get the project
project = Project.objects.get(pk=project_id, workspace__slug=slug)
if not len(members):
return Response(
{"error": "Atleast one member is required"},
status=status.HTTP_400_BAD_REQUEST,
)
bulk_project_members = []
project_members = (
ProjectMember.objects.filter(
workspace__slug=slug,
member_id__in=[member.get("member_id") for member in members],
)
.values("member_id", "sort_order")
.order_by("sort_order")
)
for member in members:
sort_order = [
project_member.get("sort_order")
for project_member in project_members
if str(project_member.get("member_id"))
== str(member.get("member_id"))
]
bulk_project_members.append(
ProjectMember(
member_id=member.get("member_id"),
role=member.get("role", 10),
project_id=project_id,
workspace_id=project.workspace_id,
sort_order=sort_order[0] - 10000 if len(sort_order) else 65535,
)
)
project_members = ProjectMember.objects.bulk_create(
bulk_project_members,
batch_size=10,
ignore_conflicts=True,
)
serializer = ProjectMemberSerializer(project_members, many=True)
return Response(serializer.data, status=status.HTTP_201_CREATED)
except KeyError:
return Response(
{"error": "Incorrect data sent"}, status=status.HTTP_400_BAD_REQUEST
)
except Project.DoesNotExist:
return Response(
{"error": "Project does not exist"}, status=status.HTTP_400_BAD_REQUEST
)
except IntegrityError:
return Response(
{"error": "User not member of the workspace"},
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 AddTeamToProjectEndpoint(BaseAPIView): class AddTeamToProjectEndpoint(BaseAPIView):
@ -705,7 +648,6 @@ class AddTeamToProjectEndpoint(BaseAPIView):
] ]
def post(self, request, slug, project_id): def post(self, request, slug, project_id):
try:
team_members = TeamMember.objects.filter( team_members = TeamMember.objects.filter(
workspace__slug=slug, team__in=request.data.get("teams", []) workspace__slug=slug, team__in=request.data.get("teams", [])
).values_list("member", flat=True) ).values_list("member", flat=True)
@ -718,6 +660,7 @@ class AddTeamToProjectEndpoint(BaseAPIView):
workspace = Workspace.objects.get(slug=slug) workspace = Workspace.objects.get(slug=slug)
project_members = [] project_members = []
issue_props = []
for member in team_members: for member in team_members:
project_members.append( project_members.append(
ProjectMember( ProjectMember(
@ -727,30 +670,25 @@ class AddTeamToProjectEndpoint(BaseAPIView):
created_by=request.user, created_by=request.user,
) )
) )
issue_props.append(
IssueProperty(
project_id=project_id,
user_id=member,
workspace=workspace,
created_by=request.user,
)
)
ProjectMember.objects.bulk_create( ProjectMember.objects.bulk_create(
project_members, batch_size=10, ignore_conflicts=True project_members, batch_size=10, ignore_conflicts=True
) )
_ = IssueProperty.objects.bulk_create(
issue_props, batch_size=10, ignore_conflicts=True
)
serializer = ProjectMemberSerializer(project_members, many=True) serializer = ProjectMemberSerializer(project_members, many=True)
return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.data, status=status.HTTP_201_CREATED)
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 Workspace.DoesNotExist:
return Response(
{"error": "The requested workspace 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 ProjectMemberInvitationsViewset(BaseViewSet): class ProjectMemberInvitationsViewset(BaseViewSet):
@ -799,7 +737,6 @@ class ProjectIdentifierEndpoint(BaseAPIView):
] ]
def get(self, request, slug): def get(self, request, slug):
try:
name = request.GET.get("name", "").strip().upper() name = request.GET.get("name", "").strip().upper()
if name == "": if name == "":
@ -815,15 +752,8 @@ class ProjectIdentifierEndpoint(BaseAPIView):
{"exists": len(exists), "identifiers": exists}, {"exists": len(exists), "identifiers": exists},
status=status.HTTP_200_OK, 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,
)
def delete(self, request, slug): def delete(self, request, slug):
try:
name = request.data.get("name", "").strip().upper() name = request.data.get("name", "").strip().upper()
if name == "": if name == "":
@ -842,17 +772,10 @@ class ProjectIdentifierEndpoint(BaseAPIView):
return Response( return Response(
status=status.HTTP_204_NO_CONTENT, status=status.HTTP_204_NO_CONTENT,
) )
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class ProjectJoinEndpoint(BaseAPIView): class ProjectJoinEndpoint(BaseAPIView):
def post(self, request, slug): def post(self, request, slug):
try:
project_ids = request.data.get("project_ids", []) project_ids = request.data.get("project_ids", [])
# Get the workspace user role # Get the workspace user role
@ -879,26 +802,27 @@ class ProjectJoinEndpoint(BaseAPIView):
ignore_conflicts=True, ignore_conflicts=True,
) )
IssueProperty.objects.bulk_create(
[
IssueProperty(
project_id=project_id,
user=request.user,
workspace=workspace,
created_by=request.user,
)
for project_id in project_ids
],
ignore_conflicts=True,
)
return Response( return Response(
{"message": "Projects joined successfully"}, {"message": "Projects joined successfully"},
status=status.HTTP_201_CREATED, status=status.HTTP_201_CREATED,
) )
except WorkspaceMember.DoesNotExist:
return Response(
{"error": "User is 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 ProjectUserViewsEndpoint(BaseAPIView): class ProjectUserViewsEndpoint(BaseAPIView):
def post(self, request, slug, project_id): def post(self, request, slug, project_id):
try:
project = Project.objects.get(pk=project_id, workspace__slug=slug) project = Project.objects.get(pk=project_id, workspace__slug=slug)
project_member = ProjectMember.objects.filter( project_member = ProjectMember.objects.filter(
@ -906,9 +830,7 @@ class ProjectUserViewsEndpoint(BaseAPIView):
).first() ).first()
if project_member is None: if project_member is None:
return Response( return Response({"error": "Forbidden"}, status=status.HTTP_403_FORBIDDEN)
{"error": "Forbidden"}, status=status.HTTP_403_FORBIDDEN
)
view_props = project_member.view_props view_props = project_member.view_props
default_props = project_member.default_props default_props = project_member.default_props
@ -916,30 +838,17 @@ class ProjectUserViewsEndpoint(BaseAPIView):
sort_order = project_member.sort_order sort_order = project_member.sort_order
project_member.view_props = request.data.get("view_props", view_props) project_member.view_props = request.data.get("view_props", view_props)
project_member.default_props = request.data.get( project_member.default_props = request.data.get("default_props", default_props)
"default_props", default_props
)
project_member.preferences = request.data.get("preferences", preferences) project_member.preferences = request.data.get("preferences", preferences)
project_member.sort_order = request.data.get("sort_order", sort_order) project_member.sort_order = request.data.get("sort_order", sort_order)
project_member.save() project_member.save()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
except Project.DoesNotExist:
return Response(
{"error": "The requested resource does not exists"},
status=status.HTTP_404_NOT_FOUND,
)
except Exception as e:
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class ProjectMemberUserEndpoint(BaseAPIView): class ProjectMemberUserEndpoint(BaseAPIView):
def get(self, request, slug, project_id): def get(self, request, slug, project_id):
try:
project_member = ProjectMember.objects.get( project_member = ProjectMember.objects.get(
project_id=project_id, workspace__slug=slug, member=request.user project_id=project_id, workspace__slug=slug, member=request.user
) )
@ -947,18 +856,6 @@ class ProjectMemberUserEndpoint(BaseAPIView):
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
except ProjectMember.DoesNotExist:
return Response(
{"error": "User not a member of the project"},
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 ProjectFavoritesViewSet(BaseViewSet): class ProjectFavoritesViewSet(BaseViewSet):
serializer_class = ProjectFavoriteSerializer serializer_class = ProjectFavoriteSerializer
@ -980,50 +877,18 @@ class ProjectFavoritesViewSet(BaseViewSet):
serializer.save(user=self.request.user) serializer.save(user=self.request.user)
def create(self, request, slug): def create(self, request, slug):
try:
serializer = ProjectFavoriteSerializer(data=request.data) serializer = ProjectFavoriteSerializer(data=request.data)
if serializer.is_valid(): if serializer.is_valid():
serializer.save(user=request.user) serializer.save(user=request.user)
return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except IntegrityError as e:
print(str(e))
if "already exists" in str(e):
return Response(
{"error": "The project is already added to favorites"},
status=status.HTTP_410_GONE,
)
else:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
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,
)
def destroy(self, request, slug, project_id): def destroy(self, request, slug, project_id):
try:
project_favorite = ProjectFavorite.objects.get( project_favorite = ProjectFavorite.objects.get(
project=project_id, user=request.user, workspace__slug=slug project=project_id, user=request.user, workspace__slug=slug
) )
project_favorite.delete() project_favorite.delete()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
except ProjectFavorite.DoesNotExist:
return Response(
{"error": "Project is not in favorites"},
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 ProjectDeployBoardViewSet(BaseViewSet): class ProjectDeployBoardViewSet(BaseViewSet):
@ -1045,7 +910,6 @@ class ProjectDeployBoardViewSet(BaseViewSet):
) )
def create(self, request, slug, project_id): def create(self, request, slug, project_id):
try:
comments = request.data.get("comments", False) comments = request.data.get("comments", False)
reactions = request.data.get("reactions", False) reactions = request.data.get("reactions", False)
inbox = request.data.get("inbox", None) inbox = request.data.get("inbox", None)
@ -1075,34 +939,6 @@ class ProjectDeployBoardViewSet(BaseViewSet):
serializer = ProjectDeployBoardSerializer(project_deploy_board) serializer = ProjectDeployBoardSerializer(project_deploy_board)
return Response(serializer.data, status=status.HTTP_200_OK) 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 ProjectMemberEndpoint(BaseAPIView):
permission_classes = [
ProjectEntityPermission,
]
def get(self, request, slug, project_id):
try:
project_members = ProjectMember.objects.filter(
project_id=project_id,
workspace__slug=slug,
member__is_bot=False,
).select_related("project", "member")
serializer = ProjectMemberSerializer(project_members, 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 ProjectDeployBoardPublicSettingsEndpoint(BaseAPIView): class ProjectDeployBoardPublicSettingsEndpoint(BaseAPIView):
@ -1111,23 +947,11 @@ class ProjectDeployBoardPublicSettingsEndpoint(BaseAPIView):
] ]
def get(self, request, slug, project_id): def get(self, request, slug, project_id):
try:
project_deploy_board = ProjectDeployBoard.objects.get( project_deploy_board = ProjectDeployBoard.objects.get(
workspace__slug=slug, project_id=project_id workspace__slug=slug, project_id=project_id
) )
serializer = ProjectDeployBoardSerializer(project_deploy_board) serializer = ProjectDeployBoardSerializer(project_deploy_board)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
except ProjectDeployBoard.DoesNotExist:
return Response(
{"error": "Project Deploy Board does not exists"},
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 WorkspaceProjectDeployBoardEndpoint(BaseAPIView): class WorkspaceProjectDeployBoardEndpoint(BaseAPIView):
@ -1136,7 +960,6 @@ class WorkspaceProjectDeployBoardEndpoint(BaseAPIView):
] ]
def get(self, request, slug): def get(self, request, slug):
try:
projects = ( projects = (
Project.objects.filter(workspace__slug=slug) Project.objects.filter(workspace__slug=slug)
.annotate( .annotate(
@ -1158,12 +981,6 @@ class WorkspaceProjectDeployBoardEndpoint(BaseAPIView):
) )
return Response(projects, status=status.HTTP_200_OK) return Response(projects, 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 LeaveProjectEndpoint(BaseAPIView): class LeaveProjectEndpoint(BaseAPIView):
@ -1172,7 +989,6 @@ class LeaveProjectEndpoint(BaseAPIView):
] ]
def delete(self, request, slug, project_id): def delete(self, request, slug, project_id):
try:
project_member = ProjectMember.objects.get( project_member = ProjectMember.objects.get(
workspace__slug=slug, workspace__slug=slug,
member=request.user, member=request.user,
@ -1198,14 +1014,34 @@ class LeaveProjectEndpoint(BaseAPIView):
# Delete the member from workspace # Delete the member from workspace
project_member.delete() project_member.delete()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
except ProjectMember.DoesNotExist:
return Response(
{"error": "Workspace member does not exists"}, class ProjectPublicCoverImagesEndpoint(BaseAPIView):
status=status.HTTP_400_BAD_REQUEST, permission_classes = [
AllowAny,
]
def get(self, request):
files = []
s3 = boto3.client(
"s3",
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
) )
except Exception as e: params = {
capture_exception(e) "Bucket": settings.AWS_S3_BUCKET_NAME,
return Response( "Prefix": "static/project-cover/",
{"error": "Something went wrong please try again later"}, }
status=status.HTTP_400_BAD_REQUEST,
response = s3.list_objects_v2(**params)
# Extracting file keys from the response
if "Contents" in response:
for content in response["Contents"]:
if not content["Key"].endswith(
"/"
): # This line ensures we're only getting files, not "sub-folders"
files.append(
f"https://{settings.AWS_S3_BUCKET_NAME}.s3.{settings.AWS_REGION}.amazonaws.com/{content['Key']}"
) )
return Response(files, status=status.HTTP_200_OK)

View File

@ -1,21 +0,0 @@
# Third party imports
from rest_framework.response import Response
from rest_framework import status
from sentry_sdk import capture_exception
# Module imports
from .base import BaseAPIView
from plane.utils.integrations.github import get_release_notes
class ReleaseNotesEndpoint(BaseAPIView):
def get(self, request):
try:
release_notes = get_release_notes()
return Response(release_notes, 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,
)

View File

@ -168,7 +168,6 @@ class GlobalSearchEndpoint(BaseAPIView):
) )
def get(self, request, slug): def get(self, request, slug):
try:
query = request.query_params.get("search", False) query = request.query_params.get("search", False)
workspace_search = request.query_params.get("workspace_search", "false") workspace_search = request.query_params.get("workspace_search", "false")
project_id = request.query_params.get("project_id", False) project_id = request.query_params.get("project_id", False)
@ -206,17 +205,9 @@ class GlobalSearchEndpoint(BaseAPIView):
results[model] = func(query, slug, project_id, workspace_search) results[model] = func(query, slug, project_id, workspace_search)
return Response({"results": results}, status=status.HTTP_200_OK) return Response({"results": results}, 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 IssueSearchEndpoint(BaseAPIView): class IssueSearchEndpoint(BaseAPIView):
def get(self, request, slug, project_id): def get(self, request, slug, project_id):
try:
query = request.query_params.get("search", False) query = request.query_params.get("search", False)
workspace_search = request.query_params.get("workspace_search", "false") workspace_search = request.query_params.get("workspace_search", "false")
parent = request.query_params.get("parent", "false") parent = request.query_params.get("parent", "false")
@ -281,13 +272,3 @@ class IssueSearchEndpoint(BaseAPIView):
), ),
status=status.HTTP_200_OK, status=status.HTTP_200_OK,
) )
except Issue.DoesNotExist:
return Response(
{"error": "Issue Does not exist"}, status=status.HTTP_400_BAD_REQUEST
)
except Exception as e:
print(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)

View File

@ -2,7 +2,6 @@
from itertools import groupby from itertools import groupby
# Django imports # Django imports
from django.db import IntegrityError
from django.db.models import Q from django.db.models import Q
# Third party imports # Third party imports
@ -41,26 +40,13 @@ class StateViewSet(BaseViewSet):
) )
def create(self, request, slug, project_id): def create(self, request, slug, project_id):
try:
serializer = StateSerializer(data=request.data) serializer = StateSerializer(data=request.data)
if serializer.is_valid(): if serializer.is_valid():
serializer.save(project_id=project_id) serializer.save(project_id=project_id)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except IntegrityError:
return Response(
{"error": "State with the name already 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,
)
def list(self, request, slug, project_id): def list(self, request, slug, project_id):
try:
state_dict = dict() state_dict = dict()
states = StateSerializer(self.get_queryset(), many=True).data states = StateSerializer(self.get_queryset(), many=True).data
@ -71,15 +57,8 @@ class StateViewSet(BaseViewSet):
state_dict[str(key)] = list(value) state_dict[str(key)] = list(value)
return Response(state_dict, status=status.HTTP_200_OK) return Response(state_dict, 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,
)
def destroy(self, request, slug, project_id, pk): def destroy(self, request, slug, project_id, pk):
try:
state = State.objects.get( state = State.objects.get(
~Q(name="Triage"), ~Q(name="Triage"),
pk=pk, project_id=project_id, workspace__slug=slug, pk=pk, project_id=project_id, workspace__slug=slug,
@ -103,5 +82,3 @@ class StateViewSet(BaseViewSet):
state.delete() state.delete()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
except State.DoesNotExist:
return Response({"error": "State does not exists"}, status=status.HTTP_404)

View File

@ -8,6 +8,8 @@ from sentry_sdk import capture_exception
from plane.api.serializers import ( from plane.api.serializers import (
UserSerializer, UserSerializer,
IssueActivitySerializer, IssueActivitySerializer,
UserMeSerializer,
UserMeSettingsSerializer,
) )
from plane.api.views.base import BaseViewSet, BaseAPIView from plane.api.views.base import BaseViewSet, BaseAPIView
@ -17,7 +19,6 @@ from plane.db.models import (
WorkspaceMemberInvite, WorkspaceMemberInvite,
Issue, Issue,
IssueActivity, IssueActivity,
WorkspaceMember,
) )
from plane.utils.paginator import BasePaginator from plane.utils.paginator import BasePaginator
@ -30,115 +31,35 @@ class UserEndpoint(BaseViewSet):
return self.request.user return self.request.user
def retrieve(self, request): def retrieve(self, request):
try: serialized_data = UserMeSerializer(request.user).data
workspace = Workspace.objects.get(
pk=request.user.last_workspace_id, workspace_member__member=request.user
)
workspace_invites = WorkspaceMemberInvite.objects.filter(
email=request.user.email
).count()
assigned_issues = Issue.issue_objects.filter(
assignees__in=[request.user]
).count()
serialized_data = UserSerializer(request.user).data
serialized_data["workspace"] = {
"last_workspace_id": request.user.last_workspace_id,
"last_workspace_slug": workspace.slug,
"fallback_workspace_id": request.user.last_workspace_id,
"fallback_workspace_slug": workspace.slug,
"invites": workspace_invites,
}
serialized_data.setdefault("issues", {})[
"assigned_issues"
] = assigned_issues
return Response( return Response(
serialized_data, serialized_data,
status=status.HTTP_200_OK, status=status.HTTP_200_OK,
) )
except Workspace.DoesNotExist:
# This exception will be hit even when the `last_workspace_id` is None
workspace_invites = WorkspaceMemberInvite.objects.filter( def retrieve_user_settings(self, request):
email=request.user.email serialized_data = UserMeSettingsSerializer(request.user).data
).count() return Response(serialized_data, status=status.HTTP_200_OK)
assigned_issues = Issue.issue_objects.filter(
assignees__in=[request.user]
).count()
fallback_workspace = (
Workspace.objects.filter(workspace_member__member=request.user)
.order_by("created_at")
.first()
)
serialized_data = UserSerializer(request.user).data
serialized_data["workspace"] = {
"last_workspace_id": None,
"last_workspace_slug": None,
"fallback_workspace_id": fallback_workspace.id
if fallback_workspace is not None
else None,
"fallback_workspace_slug": fallback_workspace.slug
if fallback_workspace is not None
else None,
"invites": workspace_invites,
}
serialized_data.setdefault("issues", {})[
"assigned_issues"
] = assigned_issues
return Response(
serialized_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 UpdateUserOnBoardedEndpoint(BaseAPIView): class UpdateUserOnBoardedEndpoint(BaseAPIView):
def patch(self, request): def patch(self, request):
try:
user = User.objects.get(pk=request.user.id) user = User.objects.get(pk=request.user.id)
user.is_onboarded = request.data.get("is_onboarded", False) user.is_onboarded = request.data.get("is_onboarded", False)
user.save() user.save()
return Response( return Response({"message": "Updated successfully"}, status=status.HTTP_200_OK)
{"message": "Updated successfully"}, 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 UpdateUserTourCompletedEndpoint(BaseAPIView): class UpdateUserTourCompletedEndpoint(BaseAPIView):
def patch(self, request): def patch(self, request):
try:
user = User.objects.get(pk=request.user.id) user = User.objects.get(pk=request.user.id)
user.is_tour_completed = request.data.get("is_tour_completed", False) user.is_tour_completed = request.data.get("is_tour_completed", False)
user.save() user.save()
return Response( return Response({"message": "Updated successfully"}, status=status.HTTP_200_OK)
{"message": "Updated successfully"}, 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 UserActivityEndpoint(BaseAPIView, BasePaginator): class UserActivityEndpoint(BaseAPIView, BasePaginator):
def get(self, request, slug): def get(self, request, slug):
try:
queryset = IssueActivity.objects.filter( queryset = IssueActivity.objects.filter(
actor=request.user, workspace__slug=slug actor=request.user, workspace__slug=slug
).select_related("actor", "workspace", "issue", "project") ).select_related("actor", "workspace", "issue", "project")
@ -150,9 +71,3 @@ class UserActivityEndpoint(BaseAPIView, BasePaginator):
issue_activities, many=True issue_activities, many=True
).data, ).data,
) )
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)

View File

@ -13,7 +13,6 @@ from django.db.models import (
) )
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.views.decorators.gzip import gzip_page from django.views.decorators.gzip import gzip_page
from django.db import IntegrityError
from django.db.models import Prefetch, OuterRef, Exists from django.db.models import Prefetch, OuterRef, Exists
# Third party imports # Third party imports
@ -61,7 +60,7 @@ class GlobalViewViewSet(BaseViewSet):
.get_queryset() .get_queryset()
.filter(workspace__slug=self.kwargs.get("slug")) .filter(workspace__slug=self.kwargs.get("slug"))
.select_related("workspace") .select_related("workspace")
.order_by("-created_at") .order_by(self.request.GET.get("order_by", "-created_at"))
.distinct() .distinct()
) )
@ -94,10 +93,8 @@ class GlobalViewIssuesViewSet(BaseViewSet):
) )
) )
@method_decorator(gzip_page) @method_decorator(gzip_page)
def list(self, request, slug): def list(self, request, slug):
try:
filters = issue_filters(request.query_params, "GET") filters = issue_filters(request.query_params, "GET")
# Custom ordering for priority and state # Custom ordering for priority and state
@ -119,9 +116,7 @@ class GlobalViewIssuesViewSet(BaseViewSet):
.values("count") .values("count")
) )
.annotate( .annotate(
attachment_count=IssueAttachment.objects.filter( attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id"))
issue=OuterRef("id")
)
.order_by() .order_by()
.annotate(count=Func(F("id"), function="Count")) .annotate(count=Func(F("id"), function="Count"))
.values("count") .values("count")
@ -131,9 +126,7 @@ class GlobalViewIssuesViewSet(BaseViewSet):
# Priority Ordering # Priority Ordering
if order_by_param == "priority" or order_by_param == "-priority": if order_by_param == "priority" or order_by_param == "-priority":
priority_order = ( priority_order = (
priority_order priority_order if order_by_param == "priority" else priority_order[::-1]
if order_by_param == "priority"
else priority_order[::-1]
) )
issue_queryset = issue_queryset.annotate( issue_queryset = issue_queryset.annotate(
priority_order=Case( priority_order=Case(
@ -185,7 +178,6 @@ class GlobalViewIssuesViewSet(BaseViewSet):
) )
else: else:
issue_queryset = issue_queryset.order_by(order_by_param) issue_queryset = issue_queryset.order_by(order_by_param)
issues = IssueLiteSerializer(issue_queryset, many=True).data issues = IssueLiteSerializer(issue_queryset, many=True).data
## Grouping the results ## Grouping the results
@ -198,19 +190,14 @@ class GlobalViewIssuesViewSet(BaseViewSet):
) )
if group_by: if group_by:
grouped_results = group_results(issues, group_by, sub_group_by)
return Response( return Response(
group_results(issues, group_by, sub_group_by), status=status.HTTP_200_OK grouped_results,
status=status.HTTP_200_OK,
) )
return Response(issues, status=status.HTTP_200_OK) 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 IssueViewViewSet(BaseViewSet): class IssueViewViewSet(BaseViewSet):
serializer_class = IssueViewSerializer serializer_class = IssueViewSerializer
@ -243,51 +230,6 @@ class IssueViewViewSet(BaseViewSet):
) )
class ViewIssuesEndpoint(BaseAPIView):
permission_classes = [
ProjectEntityPermission,
]
def get(self, request, slug, project_id, view_id):
try:
view = IssueView.objects.get(pk=view_id)
queries = view.query
filters = issue_filters(request.query_params, "GET")
issues = (
Issue.issue_objects.filter(
**queries, project_id=project_id, workspace__slug=slug
)
.filter(**filters)
.select_related("project")
.select_related("workspace")
.select_related("state")
.select_related("parent")
.prefetch_related("assignees")
.prefetch_related("labels")
.prefetch_related(
Prefetch(
"issue_reactions",
queryset=IssueReaction.objects.select_related("actor"),
)
)
)
serializer = IssueLiteSerializer(issues, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
except IssueView.DoesNotExist:
return Response(
{"error": "Issue View does not exist"}, 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 IssueViewFavoriteViewSet(BaseViewSet): class IssueViewFavoriteViewSet(BaseViewSet):
serializer_class = IssueViewFavoriteSerializer serializer_class = IssueViewFavoriteSerializer
model = IssueViewFavorite model = IssueViewFavorite
@ -302,33 +244,13 @@ class IssueViewFavoriteViewSet(BaseViewSet):
) )
def create(self, request, slug, project_id): def create(self, request, slug, project_id):
try:
serializer = IssueViewFavoriteSerializer(data=request.data) serializer = IssueViewFavoriteSerializer(data=request.data)
if serializer.is_valid(): if serializer.is_valid():
serializer.save(user=request.user, project_id=project_id) serializer.save(user=request.user, project_id=project_id)
return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except IntegrityError as e:
if "already exists" in str(e):
return Response(
{"error": "The view is already added to favorites"},
status=status.HTTP_410_GONE,
)
else:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
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,
)
def destroy(self, request, slug, project_id, view_id): def destroy(self, request, slug, project_id, view_id):
try:
view_favourite = IssueViewFavorite.objects.get( view_favourite = IssueViewFavorite.objects.get(
project=project_id, project=project_id,
user=request.user, user=request.user,
@ -337,14 +259,3 @@ class IssueViewFavoriteViewSet(BaseViewSet):
) )
view_favourite.delete() view_favourite.delete()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
except IssueViewFavorite.DoesNotExist:
return Response(
{"error": "View is not in favorites"},
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,
)

View File

@ -6,12 +6,10 @@ from uuid import uuid4
# Django imports # Django imports
from django.db import IntegrityError from django.db import IntegrityError
from django.db.models import Prefetch
from django.conf import settings from django.conf import settings
from django.utils import timezone from django.utils import timezone
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.validators import validate_email from django.core.validators import validate_email
from django.contrib.sites.shortcuts import get_current_site
from django.db.models import ( from django.db.models import (
Prefetch, Prefetch,
OuterRef, OuterRef,
@ -48,13 +46,13 @@ from plane.api.serializers import (
IssueActivitySerializer, IssueActivitySerializer,
IssueLiteSerializer, IssueLiteSerializer,
WorkspaceMemberAdminSerializer, WorkspaceMemberAdminSerializer,
WorkspaceMemberMeSerializer,
) )
from plane.api.views.base import BaseAPIView from plane.api.views.base import BaseAPIView
from . import BaseViewSet from . import BaseViewSet
from plane.db.models import ( from plane.db.models import (
User, User,
Workspace, Workspace,
WorkspaceMember,
WorkspaceMemberInvite, WorkspaceMemberInvite,
Team, Team,
ProjectMember, ProjectMember,
@ -164,23 +162,12 @@ class WorkSpaceViewSet(BaseViewSet):
status=status.HTTP_400_BAD_REQUEST, 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: except IntegrityError as e:
if "already exists" in str(e): if "already exists" in str(e):
return Response( return Response(
{"slug": "The workspace with the slug already exists"}, {"slug": "The workspace with the slug already exists"},
status=status.HTTP_410_GONE, 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): class UserWorkSpacesEndpoint(BaseAPIView):
@ -192,7 +179,6 @@ class UserWorkSpacesEndpoint(BaseAPIView):
] ]
def get(self, request): def get(self, request):
try:
member_count = ( member_count = (
WorkspaceMember.objects.filter( WorkspaceMember.objects.filter(
workspace=OuterRef("id"), member__is_bot=False workspace=OuterRef("id"), member__is_bot=False
@ -212,9 +198,7 @@ class UserWorkSpacesEndpoint(BaseAPIView):
workspace = ( workspace = (
( (
Workspace.objects.prefetch_related( Workspace.objects.prefetch_related(
Prefetch( Prefetch("workspace_member", queryset=WorkspaceMember.objects.all())
"workspace_member", queryset=WorkspaceMember.objects.all()
)
) )
.filter( .filter(
workspace_member__member=request.user, workspace_member__member=request.user,
@ -228,17 +212,9 @@ class UserWorkSpacesEndpoint(BaseAPIView):
serializer = WorkSpaceSerializer(self.filter_queryset(workspace), many=True) serializer = WorkSpaceSerializer(self.filter_queryset(workspace), many=True)
return Response(serializer.data, status=status.HTTP_200_OK) 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): class WorkSpaceAvailabilityCheckEndpoint(BaseAPIView):
def get(self, request): def get(self, request):
try:
slug = request.GET.get("slug", False) slug = request.GET.get("slug", False)
if not slug or slug == "": if not slug or slug == "":
@ -249,12 +225,6 @@ class WorkSpaceAvailabilityCheckEndpoint(BaseAPIView):
workspace = Workspace.objects.filter(slug=slug).exists() workspace = Workspace.objects.filter(slug=slug).exists()
return Response({"status": not workspace}, status=status.HTTP_200_OK) 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): class InviteWorkspaceEndpoint(BaseAPIView):
@ -263,7 +233,6 @@ class InviteWorkspaceEndpoint(BaseAPIView):
] ]
def post(self, request, slug): def post(self, request, slug):
try:
emails = request.data.get("emails", False) emails = request.data.get("emails", False)
# Check if email is provided # Check if email is provided
if not emails or not len(emails): if not emails or not len(emails):
@ -372,18 +341,6 @@ class InviteWorkspaceEndpoint(BaseAPIView):
status=status.HTTP_200_OK, 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): class JoinWorkspaceEndpoint(BaseAPIView):
permission_classes = [ permission_classes = [
@ -391,7 +348,6 @@ class JoinWorkspaceEndpoint(BaseAPIView):
] ]
def post(self, request, slug, pk): def post(self, request, slug, pk):
try:
workspace_invite = WorkspaceMemberInvite.objects.get( workspace_invite = WorkspaceMemberInvite.objects.get(
pk=pk, workspace__slug=slug pk=pk, workspace__slug=slug
) )
@ -442,18 +398,6 @@ class JoinWorkspaceEndpoint(BaseAPIView):
status=status.HTTP_400_BAD_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): class WorkspaceInvitationsViewset(BaseViewSet):
serializer_class = WorkSpaceMemberInviteSerializer serializer_class = WorkSpaceMemberInviteSerializer
@ -472,7 +416,6 @@ class WorkspaceInvitationsViewset(BaseViewSet):
) )
def destroy(self, request, slug, pk): def destroy(self, request, slug, pk):
try:
workspace_member_invite = WorkspaceMemberInvite.objects.get( workspace_member_invite = WorkspaceMemberInvite.objects.get(
pk=pk, workspace__slug=slug pk=pk, workspace__slug=slug
) )
@ -483,17 +426,6 @@ class WorkspaceInvitationsViewset(BaseViewSet):
user.delete() user.delete()
workspace_member_invite.delete() workspace_member_invite.delete()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
except WorkspaceMemberInvite.DoesNotExist:
return Response(
{"error": "Workspace member invite 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 UserWorkspaceInvitationsEndpoint(BaseViewSet): class UserWorkspaceInvitationsEndpoint(BaseViewSet):
@ -510,11 +442,8 @@ class UserWorkspaceInvitationsEndpoint(BaseViewSet):
) )
def create(self, request): def create(self, request):
try:
invitations = request.data.get("invitations") invitations = request.data.get("invitations")
workspace_invitations = WorkspaceMemberInvite.objects.filter( workspace_invitations = WorkspaceMemberInvite.objects.filter(pk__in=invitations)
pk__in=invitations
)
WorkspaceMember.objects.bulk_create( WorkspaceMember.objects.bulk_create(
[ [
@ -533,12 +462,6 @@ class UserWorkspaceInvitationsEndpoint(BaseViewSet):
workspace_invitations.delete() workspace_invitations.delete()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
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): class WorkSpaceMemberViewSet(BaseViewSet):
@ -546,7 +469,7 @@ class WorkSpaceMemberViewSet(BaseViewSet):
model = WorkspaceMember model = WorkspaceMember
permission_classes = [ permission_classes = [
WorkSpaceAdminPermission, WorkspaceEntityPermission,
] ]
search_fields = [ search_fields = [
@ -563,8 +486,26 @@ class WorkSpaceMemberViewSet(BaseViewSet):
.select_related("member") .select_related("member")
) )
def list(self, request, slug):
workspace_member = WorkspaceMember.objects.get(
member=request.user, workspace__slug=slug
)
workspace_members = WorkspaceMember.objects.filter(
workspace__slug=slug,
member__is_bot=False,
).select_related("workspace", "member")
if workspace_member.role > 10:
serializer = WorkspaceMemberAdminSerializer(workspace_members, many=True)
else:
serializer = WorkSpaceMemberSerializer(
workspace_members,
many=True,
)
return Response(serializer.data, status=status.HTTP_200_OK)
def partial_update(self, request, slug, pk): def partial_update(self, request, slug, pk):
try:
workspace_member = WorkspaceMember.objects.get(pk=pk, workspace__slug=slug) workspace_member = WorkspaceMember.objects.get(pk=pk, workspace__slug=slug)
if request.user.id == workspace_member.member_id: if request.user.id == workspace_member.member_id:
return Response( return Response(
@ -584,9 +525,7 @@ class WorkSpaceMemberViewSet(BaseViewSet):
> requested_workspace_member.role > requested_workspace_member.role
): ):
return Response( return Response(
{ {"error": "You cannot update a role that is higher than your own role"},
"error": "You cannot update a role that is higher than your own role"
},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
@ -598,20 +537,8 @@ class WorkSpaceMemberViewSet(BaseViewSet):
serializer.save() serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except WorkspaceMember.DoesNotExist:
return Response(
{"error": "Workspace Member 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,
)
def destroy(self, request, slug, pk): def destroy(self, request, slug, pk):
try:
# Check the user role who is deleting the user # Check the user role who is deleting the user
workspace_member = WorkspaceMember.objects.get(workspace__slug=slug, pk=pk) workspace_member = WorkspaceMember.objects.get(workspace__slug=slug, pk=pk)
@ -676,17 +603,6 @@ class WorkSpaceMemberViewSet(BaseViewSet):
workspace_member.delete() workspace_member.delete()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
except WorkspaceMember.DoesNotExist:
return Response(
{"error": "Workspace Member 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 TeamMemberViewSet(BaseViewSet): class TeamMemberViewSet(BaseViewSet):
@ -711,7 +627,6 @@ class TeamMemberViewSet(BaseViewSet):
) )
def create(self, request, slug): def create(self, request, slug):
try:
members = list( members = list(
WorkspaceMember.objects.filter( WorkspaceMember.objects.filter(
workspace__slug=slug, member__id__in=request.data.get("members", []) workspace__slug=slug, member__id__in=request.data.get("members", [])
@ -736,25 +651,11 @@ class TeamMemberViewSet(BaseViewSet):
workspace = Workspace.objects.get(slug=slug) workspace = Workspace.objects.get(slug=slug)
serializer = TeamSerializer( serializer = TeamSerializer(data=request.data, context={"workspace": workspace})
data=request.data, context={"workspace": workspace}
)
if serializer.is_valid(): if serializer.is_valid():
serializer.save() serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) 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): class UserWorkspaceInvitationEndpoint(BaseViewSet):
@ -776,7 +677,6 @@ class UserWorkspaceInvitationEndpoint(BaseViewSet):
class UserLastProjectWithWorkspaceEndpoint(BaseAPIView): class UserLastProjectWithWorkspaceEndpoint(BaseAPIView):
def get(self, request): def get(self, request):
try:
user = User.objects.get(pk=request.user.id) user = User.objects.get(pk=request.user.id)
last_workspace_id = user.last_workspace_id last_workspace_id = user.last_workspace_id
@ -797,9 +697,7 @@ class UserLastProjectWithWorkspaceEndpoint(BaseAPIView):
workspace_id=last_workspace_id, member=request.user workspace_id=last_workspace_id, member=request.user
).select_related("workspace", "project", "member", "workspace__owner") ).select_related("workspace", "project", "member", "workspace__owner")
project_member_serializer = ProjectMemberSerializer( project_member_serializer = ProjectMemberSerializer(project_member, many=True)
project_member, many=True
)
return Response( return Response(
{ {
@ -809,37 +707,18 @@ class UserLastProjectWithWorkspaceEndpoint(BaseAPIView):
status=status.HTTP_200_OK, 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): class WorkspaceMemberUserEndpoint(BaseAPIView):
def get(self, request, slug): def get(self, request, slug):
try:
workspace_member = WorkspaceMember.objects.get( workspace_member = WorkspaceMember.objects.get(
member=request.user, workspace__slug=slug member=request.user, workspace__slug=slug
) )
serializer = WorkSpaceMemberSerializer(workspace_member) serializer = WorkspaceMemberMeSerializer(workspace_member)
return Response(serializer.data, status=status.HTTP_200_OK) 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): class WorkspaceMemberUserViewsEndpoint(BaseAPIView):
def post(self, request, slug): def post(self, request, slug):
try:
workspace_member = WorkspaceMember.objects.get( workspace_member = WorkspaceMember.objects.get(
workspace__slug=slug, member=request.user workspace__slug=slug, member=request.user
) )
@ -847,22 +726,10 @@ class WorkspaceMemberUserViewsEndpoint(BaseAPIView):
workspace_member.save() workspace_member.save()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
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): class UserActivityGraphEndpoint(BaseAPIView):
def get(self, request, slug): def get(self, request, slug):
try:
issue_activities = ( issue_activities = (
IssueActivity.objects.filter( IssueActivity.objects.filter(
actor=request.user, actor=request.user,
@ -876,17 +743,10 @@ class UserActivityGraphEndpoint(BaseAPIView):
) )
return Response(issue_activities, status=status.HTTP_200_OK) 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): class UserIssueCompletedGraphEndpoint(BaseAPIView):
def get(self, request, slug): def get(self, request, slug):
try:
month = request.GET.get("month", 1) month = request.GET.get("month", 1)
issues = ( issues = (
@ -904,12 +764,6 @@ class UserIssueCompletedGraphEndpoint(BaseAPIView):
) )
return Response(issues, status=status.HTTP_200_OK) 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): class WeekInMonth(Func):
@ -919,7 +773,6 @@ class WeekInMonth(Func):
class UserWorkspaceDashboardEndpoint(BaseAPIView): class UserWorkspaceDashboardEndpoint(BaseAPIView):
def get(self, request, slug): def get(self, request, slug):
try:
issue_activities = ( issue_activities = (
IssueActivity.objects.filter( IssueActivity.objects.filter(
actor=request.user, actor=request.user,
@ -1015,13 +868,6 @@ class UserWorkspaceDashboardEndpoint(BaseAPIView):
status=status.HTTP_200_OK, 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): class WorkspaceThemeViewSet(BaseViewSet):
permission_classes = [ permission_classes = [
@ -1034,29 +880,16 @@ class WorkspaceThemeViewSet(BaseViewSet):
return super().get_queryset().filter(workspace__slug=self.kwargs.get("slug")) return super().get_queryset().filter(workspace__slug=self.kwargs.get("slug"))
def create(self, request, slug): def create(self, request, slug):
try:
workspace = Workspace.objects.get(slug=slug) workspace = Workspace.objects.get(slug=slug)
serializer = WorkspaceThemeSerializer(data=request.data) serializer = WorkspaceThemeSerializer(data=request.data)
if serializer.is_valid(): if serializer.is_valid():
serializer.save(workspace=workspace, actor=request.user) serializer.save(workspace=workspace, actor=request.user)
return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) 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,
)
class WorkspaceUserProfileStatsEndpoint(BaseAPIView): class WorkspaceUserProfileStatsEndpoint(BaseAPIView):
def get(self, request, slug, user_id): def get(self, request, slug, user_id):
try:
filters = issue_filters(request.query_params, "GET") filters = issue_filters(request.query_params, "GET")
state_distribution = ( state_distribution = (
@ -1179,12 +1012,6 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView):
"upcoming_cycles": upcoming_cycles, "upcoming_cycles": upcoming_cycles,
} }
) )
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class WorkspaceUserActivityEndpoint(BaseAPIView): class WorkspaceUserActivityEndpoint(BaseAPIView):
@ -1193,11 +1020,10 @@ class WorkspaceUserActivityEndpoint(BaseAPIView):
] ]
def get(self, request, slug, user_id): def get(self, request, slug, user_id):
try:
projects = request.query_params.getlist("project", []) projects = request.query_params.getlist("project", [])
queryset = IssueActivity.objects.filter( queryset = IssueActivity.objects.filter(
~Q(field__in=["comment", "vote", "reaction"]), ~Q(field__in=["comment", "vote", "reaction", "draft"]),
workspace__slug=slug, workspace__slug=slug,
project__project_projectmember__member=request.user, project__project_projectmember__member=request.user,
actor=user_id, actor=user_id,
@ -1213,17 +1039,10 @@ class WorkspaceUserActivityEndpoint(BaseAPIView):
issue_activities, many=True issue_activities, many=True
).data, ).data,
) )
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class WorkspaceUserProfileEndpoint(BaseAPIView): class WorkspaceUserProfileEndpoint(BaseAPIView):
def get(self, request, slug, user_id): def get(self, request, slug, user_id):
try:
user_data = User.objects.get(pk=user_id) user_data = User.objects.get(pk=user_id)
requesting_workspace_member = WorkspaceMember.objects.get( requesting_workspace_member = WorkspaceMember.objects.get(
@ -1239,13 +1058,21 @@ class WorkspaceUserProfileEndpoint(BaseAPIView):
.annotate( .annotate(
created_issues=Count( created_issues=Count(
"project_issue", "project_issue",
filter=Q(project_issue__created_by_id=user_id), filter=Q(
project_issue__created_by_id=user_id,
project_issue__archived_at__isnull=True,
project_issue__is_draft=False,
),
) )
) )
.annotate( .annotate(
assigned_issues=Count( assigned_issues=Count(
"project_issue", "project_issue",
filter=Q(project_issue__assignees__in=[user_id]), filter=Q(
project_issue__assignees__in=[user_id],
project_issue__archived_at__isnull=True,
project_issue__is_draft=False,
),
) )
) )
.annotate( .annotate(
@ -1254,6 +1081,8 @@ class WorkspaceUserProfileEndpoint(BaseAPIView):
filter=Q( filter=Q(
project_issue__completed_at__isnull=False, project_issue__completed_at__isnull=False,
project_issue__assignees__in=[user_id], project_issue__assignees__in=[user_id],
project_issue__archived_at__isnull=True,
project_issue__is_draft=False,
), ),
) )
) )
@ -1267,6 +1096,8 @@ class WorkspaceUserProfileEndpoint(BaseAPIView):
"started", "started",
], ],
project_issue__assignees__in=[user_id], project_issue__assignees__in=[user_id],
project_issue__archived_at__isnull=True,
project_issue__is_draft=False,
), ),
) )
) )
@ -1299,14 +1130,6 @@ class WorkspaceUserProfileEndpoint(BaseAPIView):
}, },
status=status.HTTP_200_OK, status=status.HTTP_200_OK,
) )
except 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 WorkspaceUserProfileIssuesEndpoint(BaseAPIView): class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
@ -1315,8 +1138,12 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
] ]
def get(self, request, slug, user_id): def get(self, request, slug, user_id):
try:
filters = issue_filters(request.query_params, "GET") filters = issue_filters(request.query_params, "GET")
# Custom ordering for priority and state
priority_order = ["urgent", "high", "medium", "low", "none"]
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
order_by_param = request.GET.get("order_by", "-created_at") order_by_param = request.GET.get("order_by", "-created_at")
issue_queryset = ( issue_queryset = (
Issue.issue_objects.filter( Issue.issue_objects.filter(
@ -1349,9 +1176,7 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
.values("count") .values("count")
) )
.annotate( .annotate(
attachment_count=IssueAttachment.objects.filter( attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id"))
issue=OuterRef("id")
)
.order_by() .order_by()
.annotate(count=Func(F("id"), function="Count")) .annotate(count=Func(F("id"), function="Count"))
.values("count") .values("count")
@ -1361,9 +1186,7 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
# Priority Ordering # Priority Ordering
if order_by_param == "priority" or order_by_param == "-priority": if order_by_param == "priority" or order_by_param == "-priority":
priority_order = ( priority_order = (
priority_order priority_order if order_by_param == "priority" else priority_order[::-1]
if order_by_param == "priority"
else priority_order[::-1]
) )
issue_queryset = issue_queryset.annotate( issue_queryset = issue_queryset.annotate(
priority_order=Case( priority_order=Case(
@ -1421,16 +1244,14 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
## Grouping the results ## Grouping the results
group_by = request.GET.get("group_by", False) group_by = request.GET.get("group_by", False)
if group_by: if group_by:
grouped_results = group_results(issues, group_by)
return Response( return Response(
group_results(issues, group_by), status=status.HTTP_200_OK grouped_results,
status=status.HTTP_200_OK,
) )
return Response(issues, status=status.HTTP_200_OK)
except Exception as e:
capture_exception(e)
return Response( return Response(
{"error": "Something went wrong please try again later"}, issues, status=status.HTTP_200_OK
status=status.HTTP_400_BAD_REQUEST,
) )
@ -1440,39 +1261,11 @@ class WorkspaceLabelsEndpoint(BaseAPIView):
] ]
def get(self, request, slug): def get(self, request, slug):
try:
labels = Label.objects.filter( labels = Label.objects.filter(
workspace__slug=slug, workspace__slug=slug,
project__project_projectmember__member=request.user, project__project_projectmember__member=request.user,
).values("parent", "name", "color", "id", "project_id", "workspace__slug") ).values("parent", "name", "color", "id", "project_id", "workspace__slug")
return Response(labels, status=status.HTTP_200_OK) return Response(labels, 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 WorkspaceMembersEndpoint(BaseAPIView):
permission_classes = [
WorkspaceEntityPermission,
]
def get(self, request, slug):
try:
workspace_members = WorkspaceMember.objects.filter(
workspace__slug=slug,
member__is_bot=False,
).select_related("workspace", "member")
serialzier = WorkSpaceMemberSerializer(workspace_members, many=True)
return Response(serialzier.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 LeaveWorkspaceEndpoint(BaseAPIView): class LeaveWorkspaceEndpoint(BaseAPIView):
@ -1481,7 +1274,6 @@ class LeaveWorkspaceEndpoint(BaseAPIView):
] ]
def delete(self, request, slug): def delete(self, request, slug):
try:
workspace_member = WorkspaceMember.objects.get( workspace_member = WorkspaceMember.objects.get(
workspace__slug=slug, member=request.user workspace__slug=slug, member=request.user
) )
@ -1489,9 +1281,7 @@ class LeaveWorkspaceEndpoint(BaseAPIView):
# Only Admin case # Only Admin case
if ( if (
workspace_member.role == 20 workspace_member.role == 20
and WorkspaceMember.objects.filter( and WorkspaceMember.objects.filter(workspace__slug=slug, role=20).count()
workspace__slug=slug, role=20
).count()
== 1 == 1
): ):
return Response( return Response(
@ -1503,14 +1293,3 @@ class LeaveWorkspaceEndpoint(BaseAPIView):
# Delete the member from workspace # Delete the member from workspace
workspace_member.delete() workspace_member.delete()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
except WorkspaceMember.DoesNotExist:
return Response(
{"error": "Workspace member 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,
)

View File

@ -20,8 +20,8 @@ from plane.utils.issue_filters import issue_filters
row_mapping = { row_mapping = {
"state__name": "State", "state__name": "State",
"state__group": "State Group", "state__group": "State Group",
"labels__name": "Label", "labels__id": "Label",
"assignees__display_name": "Assignee Name", "assignees__id": "Assignee Name",
"start_date": "Start Date", "start_date": "Start Date",
"target_date": "Due Date", "target_date": "Due Date",
"completed_at": "Completed At", "completed_at": "Completed At",
@ -29,8 +29,321 @@ row_mapping = {
"issue_count": "Issue Count", "issue_count": "Issue Count",
"priority": "Priority", "priority": "Priority",
"estimate": "Estimate", "estimate": "Estimate",
"issue_cycle__cycle_id": "Cycle",
"issue_module__module_id": "Module"
} }
ASSIGNEE_ID = "assignees__id"
LABEL_ID = "labels__id"
STATE_ID = "state_id"
CYCLE_ID = "issue_cycle__cycle_id"
MODULE_ID = "issue_module__module_id"
def send_export_email(email, slug, csv_buffer):
"""Helper function to send export email."""
subject = "Your Export is ready"
html_content = render_to_string("emails/exports/analytics.html", {})
text_content = strip_tags(html_content)
csv_buffer.seek(0)
msg = EmailMultiAlternatives(subject, text_content, settings.EMAIL_FROM, [email])
msg.attach(f"{slug}-analytics.csv", csv_buffer.getvalue())
msg.send(fail_silently=False)
def get_assignee_details(slug, filters):
"""Fetch assignee details if required."""
return (
Issue.issue_objects.filter(
workspace__slug=slug, **filters, assignees__avatar__isnull=False
)
.distinct("assignees__id")
.order_by("assignees__id")
.values(
"assignees__avatar",
"assignees__display_name",
"assignees__first_name",
"assignees__last_name",
"assignees__id",
)
)
def get_label_details(slug, filters):
"""Fetch label details if required"""
return (
Issue.objects.filter(workspace__slug=slug, **filters, labels__id__isnull=False)
.distinct("labels__id")
.order_by("labels__id")
.values("labels__id", "labels__color", "labels__name")
)
def get_state_details(slug, filters):
return (
Issue.issue_objects.filter(
workspace__slug=slug,
**filters,
)
.distinct("state_id")
.order_by("state_id")
.values("state_id", "state__name", "state__color")
)
def get_module_details(slug, filters):
return (
Issue.issue_objects.filter(
workspace__slug=slug,
**filters,
issue_module__module_id__isnull=False,
)
.distinct("issue_module__module_id")
.order_by("issue_module__module_id")
.values(
"issue_module__module_id",
"issue_module__module__name",
)
)
def get_cycle_details(slug, filters):
return (
Issue.issue_objects.filter(
workspace__slug=slug,
**filters,
issue_cycle__cycle_id__isnull=False,
)
.distinct("issue_cycle__cycle_id")
.order_by("issue_cycle__cycle_id")
.values(
"issue_cycle__cycle_id",
"issue_cycle__cycle__name",
)
)
def generate_csv_from_rows(rows):
"""Generate CSV buffer from rows."""
csv_buffer = io.StringIO()
writer = csv.writer(csv_buffer, delimiter=",", quoting=csv.QUOTE_ALL)
[writer.writerow(row) for row in rows]
return csv_buffer
def generate_segmented_rows(
distribution,
x_axis,
y_axis,
segment,
key,
assignee_details,
label_details,
state_details,
cycle_details,
module_details,
):
segment_zero = list(
set(
item.get("segment") for sublist in distribution.values() for item in sublist
)
)
segmented = segment
row_zero = [
row_mapping.get(x_axis, "X-Axis"),
row_mapping.get(y_axis, "Y-Axis"),
] + segment_zero
rows = []
for item, data in distribution.items():
generated_row = [
item,
sum(obj.get(key) for obj in data if obj.get(key) is not None),
]
for segment in segment_zero:
value = next((x.get(key) for x in data if x.get("segment") == segment), "0")
generated_row.append(value)
if x_axis == ASSIGNEE_ID:
assignee = next(
(
user
for user in assignee_details
if str(user[ASSIGNEE_ID]) == str(item)
),
None,
)
if assignee:
generated_row[
0
] = f"{assignee['assignees__first_name']} {assignee['assignees__last_name']}"
if x_axis == LABEL_ID:
label = next(
(lab for lab in label_details if str(lab[LABEL_ID]) == str(item)),
None,
)
if label:
generated_row[0] = f"{label['labels__name']}"
if x_axis == STATE_ID:
state = next(
(sta for sta in state_details if str(sta[STATE_ID]) == str(item)),
None,
)
if state:
generated_row[0] = f"{state['state__name']}"
if x_axis == CYCLE_ID:
cycle = next(
(cyc for cyc in cycle_details if str(cyc[CYCLE_ID]) == str(item)),
None,
)
if cycle:
generated_row[0] = f"{cycle['issue_cycle__cycle__name']}"
if x_axis == MODULE_ID:
module = next(
(mod for mod in module_details if str(mod[MODULE_ID]) == str(item)),
None,
)
if module:
generated_row[0] = f"{module['issue_module__module__name']}"
rows.append(tuple(generated_row))
if segmented == ASSIGNEE_ID:
for index, segm in enumerate(row_zero[2:]):
assignee = next(
(
user
for user in assignee_details
if str(user[ASSIGNEE_ID]) == str(segm)
),
None,
)
if assignee:
row_zero[
index + 2
] = f"{assignee['assignees__first_name']} {assignee['assignees__last_name']}"
if segmented == LABEL_ID:
for index, segm in enumerate(row_zero[2:]):
label = next(
(lab for lab in label_details if str(lab[LABEL_ID]) == str(segm)),
None,
)
if label:
row_zero[index + 2] = label["labels__name"]
if segmented == STATE_ID:
for index, segm in enumerate(row_zero[2:]):
state = next(
(sta for sta in state_details if str(sta[STATE_ID]) == str(segm)),
None,
)
if state:
row_zero[index + 2] = state["state__name"]
if segmented == MODULE_ID:
for index, segm in enumerate(row_zero[2:]):
module = next(
(mod for mod in label_details if str(mod[MODULE_ID]) == str(segm)),
None,
)
if module:
row_zero[index + 2] = module["issue_module__module__name"]
if segmented == CYCLE_ID:
for index, segm in enumerate(row_zero[2:]):
cycle = next(
(cyc for cyc in cycle_details if str(cyc[CYCLE_ID]) == str(segm)),
None,
)
if cycle:
row_zero[index + 2] = cycle["issue_cycle__cycle__name"]
return [tuple(row_zero)] + rows
def generate_non_segmented_rows(
distribution,
x_axis,
y_axis,
key,
assignee_details,
label_details,
state_details,
cycle_details,
module_details,
):
rows = []
for item, data in distribution.items():
row = [item, data[0].get("count" if y_axis == "issue_count" else "estimate")]
if x_axis == ASSIGNEE_ID:
assignee = next(
(
user
for user in assignee_details
if str(user[ASSIGNEE_ID]) == str(item)
),
None,
)
if assignee:
row[
0
] = f"{assignee['assignees__first_name']} {assignee['assignees__last_name']}"
if x_axis == LABEL_ID:
label = next(
(lab for lab in label_details if str(lab[LABEL_ID]) == str(item)),
None,
)
if label:
row[0] = f"{label['labels__name']}"
if x_axis == STATE_ID:
state = next(
(sta for sta in state_details if str(sta[STATE_ID]) == str(item)),
None,
)
if state:
row[0] = f"{state['state__name']}"
if x_axis == CYCLE_ID:
cycle = next(
(cyc for cyc in cycle_details if str(cyc[CYCLE_ID]) == str(item)),
None,
)
if cycle:
row[0] = f"{cycle['issue_cycle__cycle__name']}"
if x_axis == MODULE_ID:
module = next(
(mod for mod in module_details if str(mod[MODULE_ID]) == str(item)),
None,
)
if module:
row[0] = f"{module['issue_module__module__name']}"
rows.append(tuple(row))
row_zero = [row_mapping.get(x_axis, "X-Axis"), row_mapping.get(y_axis, "Y-Axis")]
return [tuple(row_zero)] + rows
@shared_task @shared_task
def analytic_export_task(email, data, slug): def analytic_export_task(email, data, slug):
@ -43,134 +356,70 @@ def analytic_export_task(email, data, slug):
segment = data.get("segment", False) segment = data.get("segment", False)
distribution = build_graph_plot( distribution = build_graph_plot(
queryset=queryset, x_axis=x_axis, y_axis=y_axis, segment=segment queryset, x_axis=x_axis, y_axis=y_axis, segment=segment
) )
key = "count" if y_axis == "issue_count" else "estimate" key = "count" if y_axis == "issue_count" else "estimate"
segmented = segment
assignee_details = {}
if x_axis in ["assignees__id"] or segment in ["assignees__id"]:
assignee_details = ( assignee_details = (
Issue.issue_objects.filter(workspace__slug=slug, **filters, assignees__avatar__isnull=False) get_assignee_details(slug, filters)
.order_by("assignees__id") if x_axis == ASSIGNEE_ID or segment == ASSIGNEE_ID
.distinct("assignees__id") else {}
.values("assignees__avatar", "assignees__display_name", "assignees__first_name", "assignees__last_name", "assignees__id") )
label_details = (
get_label_details(slug, filters)
if x_axis == LABEL_ID or segment == LABEL_ID
else {}
)
state_details = (
get_state_details(slug, filters)
if x_axis == STATE_ID or segment == STATE_ID
else {}
)
cycle_details = (
get_cycle_details(slug, filters)
if x_axis == CYCLE_ID or segment == CYCLE_ID
else {}
)
module_details = (
get_module_details(slug, filters)
if x_axis == MODULE_ID or segment == MODULE_ID
else {}
) )
if segment: if segment:
segment_zero = [] rows = generate_segmented_rows(
for item in distribution: distribution,
current_dict = distribution.get(item) x_axis,
for current in current_dict: y_axis,
segment_zero.append(current.get("segment")) segment,
key,
segment_zero = list(set(segment_zero)) assignee_details,
row_zero = ( label_details,
[ state_details,
row_mapping.get(x_axis, "X-Axis"), cycle_details,
] module_details,
+ [
row_mapping.get(y_axis, "Y-Axis"),
]
+ segment_zero
) )
rows = []
for item in distribution:
generated_row = [
item,
]
data = distribution.get(item)
# Add y axis values
generated_row.append(sum(obj.get(key) for obj in data if obj.get(key, None) is not None))
for segment in segment_zero:
value = [x for x in data if x.get("segment") == segment]
if len(value):
generated_row.append(value[0].get(key))
else: else:
generated_row.append("0") rows = generate_non_segmented_rows(
# x-axis replacement for names distribution,
if x_axis in ["assignees__id"]: x_axis,
assignee = [user for user in assignee_details if str(user.get("assignees__id")) == str(item)] y_axis,
if len(assignee): segment,
generated_row[0] = str(assignee[0].get("assignees__first_name")) + " " + str(assignee[0].get("assignees__last_name")) key,
rows.append(tuple(generated_row)) assignee_details,
label_details,
# If segment is ["assignees__display_name"] then replace segment_zero rows with first and last names state_details,
if segmented in ["assignees__id"]: cycle_details,
for index, segm in enumerate(row_zero[2:]): module_details,
# find the name of the user
assignee = [user for user in assignee_details if str(user.get("assignees__id")) == str(segm)]
if len(assignee):
row_zero[index + 2] = str(assignee[0].get("assignees__first_name")) + " " + str(assignee[0].get("assignees__last_name"))
rows = [tuple(row_zero)] + rows
csv_buffer = io.StringIO()
writer = csv.writer(csv_buffer, delimiter=",", quoting=csv.QUOTE_ALL)
# Write CSV data to the buffer
for row in rows:
writer.writerow(row)
subject = "Your Export is ready"
html_content = render_to_string("emails/exports/analytics.html", {})
text_content = strip_tags(html_content)
csv_buffer.seek(0)
msg = EmailMultiAlternatives(
subject, text_content, settings.EMAIL_FROM, [email]
) )
msg.attach(f"{slug}-analytics.csv", csv_buffer.read())
msg.send(fail_silently=False)
else:
row_zero = [
row_mapping.get(x_axis, "X-Axis"),
row_mapping.get(y_axis, "Y-Axis"),
]
rows = []
for item in distribution:
row = [
item,
distribution.get(item)[0].get("count")
if y_axis == "issue_count"
else distribution.get(item)[0].get("estimate "),
]
# x-axis replacement to names
if x_axis in ["assignees__id"]:
assignee = [user for user in assignee_details if str(user.get("assignees__id")) == str(item)]
if len(assignee):
row[0] = str(assignee[0].get("assignees__first_name")) + " " + str(assignee[0].get("assignees__last_name"))
rows.append(tuple(row))
rows = [tuple(row_zero)] + rows
csv_buffer = io.StringIO()
writer = csv.writer(csv_buffer, delimiter=",", quoting=csv.QUOTE_ALL)
# Write CSV data to the buffer
for row in rows:
writer.writerow(row)
subject = "Your Export is ready"
html_content = render_to_string("emails/exports/analytics.html", {})
text_content = strip_tags(html_content)
csv_buffer.seek(0)
msg = EmailMultiAlternatives(
subject, text_content, settings.EMAIL_FROM, [email]
)
msg.attach(f"{slug}-analytics.csv", csv_buffer.read())
msg.send(fail_silently=False)
csv_buffer = generate_csv_from_rows(rows)
send_export_email(email, slug, csv_buffer)
except Exception as e: except Exception as e:
# Print logs if in DEBUG mode
if settings.DEBUG: if settings.DEBUG:
print(e) print(e)
capture_exception(e) capture_exception(e)
return

View File

@ -23,7 +23,7 @@ def email_verification(first_name, email, token, current_site):
from_email_string = settings.EMAIL_FROM from_email_string = settings.EMAIL_FROM
subject = f"Verify your Email!" subject = "Verify your Email!"
context = { context = {
"first_name": first_name, "first_name": first_name,

View File

@ -4,7 +4,6 @@ import io
import json import json
import boto3 import boto3
import zipfile import zipfile
from urllib.parse import urlparse, urlunparse
# Django imports # Django imports
from django.conf import settings from django.conf import settings

View File

@ -32,7 +32,7 @@ def delete_old_s3_link():
else: else:
s3 = boto3.client( s3 = boto3.client(
"s3", "s3",
region_name="ap-south-1", region_name=settings.AWS_REGION,
aws_access_key_id=settings.AWS_ACCESS_KEY_ID, aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY, aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
config=Config(signature_version="s3v4"), config=Config(signature_version="s3v4"),

View File

@ -8,20 +8,18 @@ from django.conf import settings
from celery import shared_task from celery import shared_task
from sentry_sdk import capture_exception from sentry_sdk import capture_exception
# Module imports
from plane.db.models import User
@shared_task @shared_task
def forgot_password(first_name, email, uidb64, token, current_site): def forgot_password(first_name, email, uidb64, token, current_site):
try: try:
realtivelink = f"/reset-password/?uidb64={uidb64}&token={token}" realtivelink = f"/accounts/reset-password/?uidb64={uidb64}&token={token}"
abs_url = current_site + realtivelink abs_url = current_site + realtivelink
from_email_string = settings.EMAIL_FROM from_email_string = settings.EMAIL_FROM
subject = f"Reset Your Password - Plane" subject = "Reset Your Password - Plane"
context = { context = {
"first_name": first_name, "first_name": first_name,

View File

@ -2,8 +2,6 @@
import json import json
import requests import requests
import uuid import uuid
import jwt
from datetime import datetime
# Django imports # Django imports
from django.conf import settings from django.conf import settings
@ -25,8 +23,8 @@ from plane.db.models import (
WorkspaceIntegration, WorkspaceIntegration,
Label, Label,
User, User,
IssueProperty,
) )
from .workspace_invitation_task import workspace_invitation
from plane.bgtasks.user_welcome_task import send_welcome_slack from plane.bgtasks.user_welcome_task import send_welcome_slack
@ -57,7 +55,7 @@ def service_importer(service, importer_id):
ignore_conflicts=True, ignore_conflicts=True,
) )
[ _ = [
send_welcome_slack.delay( send_welcome_slack.delay(
str(user.id), str(user.id),
True, True,
@ -103,6 +101,20 @@ def service_importer(service, importer_id):
ignore_conflicts=True, ignore_conflicts=True,
) )
IssueProperty.objects.bulk_create(
[
IssueProperty(
project_id=importer.project_id,
workspace_id=importer.workspace_id,
user=user,
created_by=importer.created_by,
)
for user in workspace_users
],
batch_size=100,
ignore_conflicts=True,
)
# Check if sync config is on for github importers # Check if sync config is on for github importers
if service == "github" and importer.config.get("sync", False): if service == "github" and importer.config.get("sync", False):
name = importer.metadata.get("name", False) name = importer.metadata.get("name", False)
@ -142,7 +154,7 @@ def service_importer(service, importer_id):
) )
# Create repo sync # Create repo sync
repo_sync = GithubRepositorySync.objects.create( _ = GithubRepositorySync.objects.create(
repository=repo, repository=repo,
workspace_integration=workspace_integration, workspace_integration=workspace_integration,
actor=workspace_integration.actor, actor=workspace_integration.actor,
@ -164,7 +176,7 @@ def service_importer(service, importer_id):
ImporterSerializer(importer).data, ImporterSerializer(importer).data,
cls=DjangoJSONEncoder, cls=DjangoJSONEncoder,
) )
res = requests.post( _ = requests.post(
f"{settings.PROXY_BASE_URL}/hooks/workspaces/{str(importer.workspace_id)}/projects/{str(importer.project_id)}/importers/{str(service)}/", f"{settings.PROXY_BASE_URL}/hooks/workspaces/{str(importer.workspace_id)}/projects/{str(importer.project_id)}/importers/{str(service)}/",
json=import_data_json, json=import_data_json,
headers=headers, headers=headers,

File diff suppressed because it is too large Load Diff

View File

@ -58,28 +58,31 @@ def archive_old_issues():
# Check if Issues # Check if Issues
if issues: if issues:
# Set the archive time to current time
archive_at = timezone.now().date()
issues_to_update = [] issues_to_update = []
for issue in issues: for issue in issues:
issue.archived_at = timezone.now() issue.archived_at = archive_at
issues_to_update.append(issue) issues_to_update.append(issue)
# Bulk Update the issues and log the activity # Bulk Update the issues and log the activity
if issues_to_update: if issues_to_update:
updated_issues = Issue.objects.bulk_update( Issue.objects.bulk_update(
issues_to_update, ["archived_at"], batch_size=100 issues_to_update, ["archived_at"], batch_size=100
) )
[ _ = [
issue_activity.delay( issue_activity.delay(
type="issue.activity.updated", type="issue.activity.updated",
requested_data=json.dumps({"archived_at": str(issue.archived_at)}), requested_data=json.dumps({"archived_at": str(archive_at)}),
actor_id=str(project.created_by_id), actor_id=str(project.created_by_id),
issue_id=issue.id, issue_id=issue.id,
project_id=project_id, project_id=project_id,
current_instance=None, current_instance=json.dumps({"archived_at": None}),
subscriber=False, subscriber=False,
epoch=int(timezone.now().timestamp()) epoch=int(timezone.now().timestamp())
) )
for issue in updated_issues for issue in issues_to_update
] ]
return return
except Exception as e: except Exception as e:
@ -139,7 +142,7 @@ def close_old_issues():
# Bulk Update the issues and log the activity # Bulk Update the issues and log the activity
if issues_to_update: if issues_to_update:
updated_issues = Issue.objects.bulk_update(issues_to_update, ["state"], batch_size=100) Issue.objects.bulk_update(issues_to_update, ["state"], batch_size=100)
[ [
issue_activity.delay( issue_activity.delay(
type="issue.activity.updated", type="issue.activity.updated",
@ -151,7 +154,7 @@ def close_old_issues():
subscriber=False, subscriber=False,
epoch=int(timezone.now().timestamp()) epoch=int(timezone.now().timestamp())
) )
for issue in updated_issues for issue in issues_to_update
] ]
return return
except Exception as e: except Exception as e:

View File

@ -17,7 +17,7 @@ def magic_link(email, key, token, current_site):
from_email_string = settings.EMAIL_FROM from_email_string = settings.EMAIL_FROM
subject = f"Login for Plane" subject = "Login for Plane"
context = {"magic_url": abs_url, "code": token} context = {"magic_url": abs_url, "code": token}

View File

@ -0,0 +1,274 @@
# Python imports
import json
# Django imports
from django.utils import timezone
# Module imports
from plane.db.models import (
IssueMention,
IssueSubscriber,
Project,
User,
IssueAssignee,
Issue,
Notification,
IssueComment,
)
# Third Party imports
from celery import shared_task
from bs4 import BeautifulSoup
def get_new_mentions(requested_instance, current_instance):
# requested_data is the newer instance of the current issue
# current_instance is the older instance of the current issue, saved in the database
# extract mentions from both the instance of data
mentions_older = extract_mentions(current_instance)
mentions_newer = extract_mentions(requested_instance)
# Getting Set Difference from mentions_newer
new_mentions = [
mention for mention in mentions_newer if mention not in mentions_older]
return new_mentions
# Get Removed Mention
def get_removed_mentions(requested_instance, current_instance):
# requested_data is the newer instance of the current issue
# current_instance is the older instance of the current issue, saved in the database
# extract mentions from both the instance of data
mentions_older = extract_mentions(current_instance)
mentions_newer = extract_mentions(requested_instance)
# Getting Set Difference from mentions_newer
removed_mentions = [
mention for mention in mentions_older if mention not in mentions_newer]
return removed_mentions
# Adds mentions as subscribers
def extract_mentions_as_subscribers(project_id, issue_id, mentions):
# mentions is an array of User IDs representing the FILTERED set of mentioned users
bulk_mention_subscribers = []
for mention_id in mentions:
# If the particular mention has not already been subscribed to the issue, he must be sent the mentioned notification
if not IssueSubscriber.objects.filter(
issue_id=issue_id,
subscriber=mention_id,
project=project_id,
).exists():
mentioned_user = User.objects.get(pk=mention_id)
project = Project.objects.get(pk=project_id)
issue = Issue.objects.get(pk=issue_id)
bulk_mention_subscribers.append(IssueSubscriber(
workspace=project.workspace,
project=project,
issue=issue,
subscriber=mentioned_user,
))
return bulk_mention_subscribers
# Parse Issue Description & extracts mentions
def extract_mentions(issue_instance):
try:
# issue_instance has to be a dictionary passed, containing the description_html and other set of activity data.
mentions = []
# Convert string to dictionary
data = json.loads(issue_instance)
html = data.get("description_html")
soup = BeautifulSoup(html, 'html.parser')
mention_tags = soup.find_all(
'mention-component', attrs={'target': 'users'})
mentions = [mention_tag['id'] for mention_tag in mention_tags]
return list(set(mentions))
except Exception as e:
return []
@shared_task
def notifications(type, issue_id, project_id, actor_id, subscriber, issue_activities_created, requested_data, current_instance):
issue_activities_created = (
json.loads(
issue_activities_created) if issue_activities_created is not None else None
)
if type not in [
"cycle.activity.created",
"cycle.activity.deleted",
"module.activity.created",
"module.activity.deleted",
"issue_reaction.activity.created",
"issue_reaction.activity.deleted",
"comment_reaction.activity.created",
"comment_reaction.activity.deleted",
"issue_vote.activity.created",
"issue_vote.activity.deleted",
"issue_draft.activity.created",
"issue_draft.activity.updated",
"issue_draft.activity.deleted",
]:
# Create Notifications
bulk_notifications = []
"""
Mention Tasks
1. Perform Diffing and Extract the mentions, that mention notification needs to be sent
2. From the latest set of mentions, extract the users which are not a subscribers & make them subscribers
"""
# Get new mentions from the newer instance
new_mentions = get_new_mentions(
requested_instance=requested_data, current_instance=current_instance)
removed_mention = get_removed_mentions(
requested_instance=requested_data, current_instance=current_instance)
# Get New Subscribers from the mentions of the newer instance
requested_mentions = extract_mentions(
issue_instance=requested_data)
mention_subscribers = extract_mentions_as_subscribers(
project_id=project_id, issue_id=issue_id, mentions=requested_mentions)
issue_subscribers = list(
IssueSubscriber.objects.filter(
project_id=project_id, issue_id=issue_id)
.exclude(subscriber_id__in=list(new_mentions + [actor_id]))
.values_list("subscriber", flat=True)
)
issue_assignees = list(
IssueAssignee.objects.filter(
project_id=project_id, issue_id=issue_id)
.exclude(assignee_id=actor_id)
.values_list("assignee", flat=True)
)
issue_subscribers = issue_subscribers + issue_assignees
issue = Issue.objects.filter(pk=issue_id).first()
if subscriber:
# add the user to issue subscriber
try:
_ = IssueSubscriber.objects.get_or_create(
issue_id=issue_id, subscriber_id=actor_id
)
except Exception as e:
pass
project = Project.objects.get(pk=project_id)
for subscriber in list(set(issue_subscribers)):
for issue_activity in issue_activities_created:
issue_comment = issue_activity.get("issue_comment")
if issue_comment is not None:
issue_comment = IssueComment.objects.get(id=issue_comment, issue_id=issue_id, project_id=project_id, workspace_id=project.workspace_id)
bulk_notifications.append(
Notification(
workspace=project.workspace,
sender="in_app:issue_activities",
triggered_by_id=actor_id,
receiver_id=subscriber,
entity_identifier=issue_id,
entity_name="issue",
project=project,
title=issue_activity.get("comment"),
data={
"issue": {
"id": str(issue_id),
"name": str(issue.name),
"identifier": str(issue.project.identifier),
"sequence_id": issue.sequence_id,
"state_name": issue.state.name,
"state_group": issue.state.group,
},
"issue_activity": {
"id": str(issue_activity.get("id")),
"verb": str(issue_activity.get("verb")),
"field": str(issue_activity.get("field")),
"actor": str(issue_activity.get("actor_id")),
"new_value": str(issue_activity.get("new_value")),
"old_value": str(issue_activity.get("old_value")),
"issue_comment": str(
issue_comment.comment_stripped
if issue_activity.get("issue_comment") is not None
else ""
),
},
},
)
)
# Add Mentioned as Issue Subscribers
IssueSubscriber.objects.bulk_create(
mention_subscribers, batch_size=100)
for mention_id in new_mentions:
if (mention_id != actor_id):
for issue_activity in issue_activities_created:
bulk_notifications.append(
Notification(
workspace=project.workspace,
sender="in_app:issue_activities:mention",
triggered_by_id=actor_id,
receiver_id=mention_id,
entity_identifier=issue_id,
entity_name="issue",
project=project,
message=f"You have been mentioned in the issue {issue.name}",
data={
"issue": {
"id": str(issue_id),
"name": str(issue.name),
"identifier": str(issue.project.identifier),
"sequence_id": issue.sequence_id,
"state_name": issue.state.name,
"state_group": issue.state.group,
},
"issue_activity": {
"id": str(issue_activity.get("id")),
"verb": str(issue_activity.get("verb")),
"field": str(issue_activity.get("field")),
"actor": str(issue_activity.get("actor_id")),
"new_value": str(issue_activity.get("new_value")),
"old_value": str(issue_activity.get("old_value")),
},
},
)
)
# Create New Mentions Here
aggregated_issue_mentions = []
for mention_id in new_mentions:
mentioned_user = User.objects.get(pk=mention_id)
aggregated_issue_mentions.append(
IssueMention(
mention=mentioned_user,
issue=issue,
project=project,
workspace=project.workspace
)
)
IssueMention.objects.bulk_create(
aggregated_issue_mentions, batch_size=100)
IssueMention.objects.filter(
issue=issue, mention__in=removed_mention).delete()
# Bulk create notifications
Notification.objects.bulk_create(bulk_notifications, batch_size=100)

View File

@ -11,7 +11,7 @@ from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError from slack_sdk.errors import SlackApiError
# Module imports # Module imports
from plane.db.models import Workspace, User, WorkspaceMemberInvite from plane.db.models import Workspace, WorkspaceMemberInvite
@shared_task @shared_task

View File

@ -33,8 +33,7 @@ def create_issue_relation(apps, schema_editor):
def update_issue_priority_choice(apps, schema_editor): def update_issue_priority_choice(apps, schema_editor):
IssueModel = apps.get_model("db", "Issue") IssueModel = apps.get_model("db", "Issue")
updated_issues = [] updated_issues = []
for obj in IssueModel.objects.all(): for obj in IssueModel.objects.filter(priority=None):
if obj.priority is None:
obj.priority = "none" obj.priority = "none"
updated_issues.append(obj) updated_issues.append(obj)
IssueModel.objects.bulk_update(updated_issues, ["priority"], batch_size=100) IssueModel.objects.bulk_update(updated_issues, ["priority"], batch_size=100)

View File

@ -26,19 +26,19 @@ def workspace_member_props(old_props):
"calendar_date_range": old_props.get("calendarDateRange", ""), "calendar_date_range": old_props.get("calendarDateRange", ""),
}, },
"display_properties": { "display_properties": {
"assignee": old_props.get("properties", {}).get("assignee",None), "assignee": old_props.get("properties", {}).get("assignee", True),
"attachment_count": old_props.get("properties", {}).get("attachment_count", None), "attachment_count": old_props.get("properties", {}).get("attachment_count", True),
"created_on": old_props.get("properties", {}).get("created_on", None), "created_on": old_props.get("properties", {}).get("created_on", True),
"due_date": old_props.get("properties", {}).get("due_date", None), "due_date": old_props.get("properties", {}).get("due_date", True),
"estimate": old_props.get("properties", {}).get("estimate", None), "estimate": old_props.get("properties", {}).get("estimate", True),
"key": old_props.get("properties", {}).get("key", None), "key": old_props.get("properties", {}).get("key", True),
"labels": old_props.get("properties", {}).get("labels", None), "labels": old_props.get("properties", {}).get("labels", True),
"link": old_props.get("properties", {}).get("link", None), "link": old_props.get("properties", {}).get("link", True),
"priority": old_props.get("properties", {}).get("priority", None), "priority": old_props.get("properties", {}).get("priority", True),
"start_date": old_props.get("properties", {}).get("start_date", None), "start_date": old_props.get("properties", {}).get("start_date", True),
"state": old_props.get("properties", {}).get("state", None), "state": old_props.get("properties", {}).get("state", True),
"sub_issue_count": old_props.get("properties", {}).get("sub_issue_count", None), "sub_issue_count": old_props.get("properties", {}).get("sub_issue_count", True),
"updated_on": old_props.get("properties", {}).get("updated_on", None), "updated_on": old_props.get("properties", {}).get("updated_on", True),
}, },
} }
return new_props return new_props

View File

@ -1,24 +0,0 @@
# Generated by Django 4.2.3 on 2023-09-15 06:55
from django.db import migrations
def update_issue_activity(apps, schema_editor):
IssueActivityModel = apps.get_model("db", "IssueActivity")
updated_issue_activity = []
for obj in IssueActivityModel.objects.all():
if obj.field == "blocks":
obj.field = "blocked_by"
updated_issue_activity.append(obj)
IssueActivityModel.objects.bulk_update(updated_issue_activity, ["field"], batch_size=100)
class Migration(migrations.Migration):
dependencies = [
('db', '0044_auto_20230913_0709'),
]
operations = [
migrations.RunPython(update_issue_activity),
]

View File

@ -1,24 +1,43 @@
# Generated by Django 4.2.3 on 2023-09-19 14:21 # Generated by Django 4.2.5 on 2023-09-29 10:14
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
import plane.db.models.workspace
import uuid import uuid
def update_epoch(apps, schema_editor): def update_issue_activity_priority(apps, schema_editor):
IssueActivity = apps.get_model('db', 'IssueActivity') IssueActivity = apps.get_model("db", "IssueActivity")
updated_issue_activity = [] updated_issue_activity = []
for obj in IssueActivity.objects.all(): for obj in IssueActivity.objects.filter(field="priority"):
obj.epoch = int(obj.created_at.timestamp()) # Set the old and new value to none if it is empty for Priority
obj.new_value = obj.new_value or "none"
obj.old_value = obj.old_value or "none"
updated_issue_activity.append(obj) updated_issue_activity.append(obj)
IssueActivity.objects.bulk_update(updated_issue_activity, ["epoch"], batch_size=100) IssueActivity.objects.bulk_update(
updated_issue_activity,
["new_value", "old_value"],
batch_size=2000,
)
def update_issue_activity_blocked(apps, schema_editor):
IssueActivity = apps.get_model("db", "IssueActivity")
updated_issue_activity = []
for obj in IssueActivity.objects.filter(field="blocks"):
# Set the field to blocked_by
obj.field = "blocked_by"
updated_issue_activity.append(obj)
IssueActivity.objects.bulk_update(
updated_issue_activity,
["field"],
batch_size=1000,
)
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('db', '0045_auto_20230915_0655'), ('db', '0044_auto_20230913_0709'),
] ]
operations = [ operations = [
@ -33,6 +52,7 @@ class Migration(migrations.Migration):
('query', models.JSONField(verbose_name='View Query')), ('query', models.JSONField(verbose_name='View Query')),
('access', models.PositiveSmallIntegerField(choices=[(0, 'Private'), (1, 'Public')], default=1)), ('access', models.PositiveSmallIntegerField(choices=[(0, 'Private'), (1, 'Public')], default=1)),
('query_data', models.JSONField(default=dict)), ('query_data', models.JSONField(default=dict)),
('sort_order', models.FloatField(default=65535)),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')), ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='global_views', to='db.workspace')), ('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='global_views', to='db.workspace')),
@ -44,10 +64,16 @@ class Migration(migrations.Migration):
'ordering': ('-created_at',), 'ordering': ('-created_at',),
}, },
), ),
migrations.AddField(
model_name='workspacemember',
name='issue_props',
field=models.JSONField(default=plane.db.models.workspace.get_issue_props),
),
migrations.AddField( migrations.AddField(
model_name='issueactivity', model_name='issueactivity',
name='epoch', name='epoch',
field=models.FloatField(null=True), field=models.FloatField(null=True),
), ),
migrations.RunPython(update_epoch), migrations.RunPython(update_issue_activity_priority),
migrations.RunPython(update_issue_activity_blocked),
] ]

View File

@ -0,0 +1,41 @@
# Generated by Django 4.2.5 on 2023-10-18 12:04
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import plane.db.models.issue
import uuid
class Migration(migrations.Migration):
dependencies = [
('db', '0045_issueactivity_epoch_workspacemember_issue_props_and_more'),
]
operations = [
migrations.CreateModel(
name="issue_mentions",
fields=[
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
('id', models.UUIDField(db_index=True, default=uuid.uuid4,editable=False, primary_key=True, serialize=False, unique=True)),
('mention', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_mention', to=settings.AUTH_USER_MODEL)),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL,related_name='issuemention_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_mention', to='db.issue')),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_issuemention', to='db.project')),
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issuemention_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_issuemention', to='db.workspace')),
],
options={
'verbose_name': 'IssueMention',
'verbose_name_plural': 'IssueMentions',
'db_table': 'issue_mentions',
'ordering': ('-created_at',),
},
),
migrations.AlterField(
model_name='issueproperty',
name='properties',
field=models.JSONField(default=plane.db.models.issue.get_default_properties),
),
]

View File

@ -27,12 +27,12 @@ from .issue import (
IssueActivity, IssueActivity,
IssueProperty, IssueProperty,
IssueComment, IssueComment,
IssueBlocker,
IssueLabel, IssueLabel,
IssueAssignee, IssueAssignee,
Label, Label,
IssueBlocker, IssueBlocker,
IssueRelation, IssueRelation,
IssueMention,
IssueLink, IssueLink,
IssueSequence, IssueSequence,
IssueAttachment, IssueAttachment,

View File

@ -6,7 +6,6 @@ from django.db import models
# Module imports # Module imports
from plane.db.models import ProjectBaseModel from plane.db.models import ProjectBaseModel
from plane.db.mixins import AuditModel
class GithubRepository(ProjectBaseModel): class GithubRepository(ProjectBaseModel):

View File

@ -16,6 +16,24 @@ from . import ProjectBaseModel
from plane.utils.html_processor import strip_tags from plane.utils.html_processor import strip_tags
def get_default_properties():
return {
"assignee": True,
"start_date": True,
"due_date": True,
"labels": True,
"key": True,
"priority": True,
"state": True,
"sub_issue_count": True,
"link": True,
"attachment_count": True,
"estimate": True,
"created_on": True,
"updated_on": True,
}
# TODO: Handle identifiers for Bulk Inserts - nk # TODO: Handle identifiers for Bulk Inserts - nk
class IssueManager(models.Manager): class IssueManager(models.Manager):
def get_queryset(self): def get_queryset(self):
@ -39,7 +57,7 @@ class Issue(ProjectBaseModel):
("high", "High"), ("high", "High"),
("medium", "Medium"), ("medium", "Medium"),
("low", "Low"), ("low", "Low"),
("none", "None") ("none", "None"),
) )
parent = models.ForeignKey( parent = models.ForeignKey(
"self", "self",
@ -210,6 +228,25 @@ class IssueRelation(ProjectBaseModel):
def __str__(self): def __str__(self):
return f"{self.issue.name} {self.related_issue.name}" return f"{self.issue.name} {self.related_issue.name}"
class IssueMention(ProjectBaseModel):
issue = models.ForeignKey(
Issue, on_delete=models.CASCADE, related_name="issue_mention"
)
mention = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="issue_mention",
)
class Meta:
unique_together = ["issue", "mention"]
verbose_name = "Issue Mention"
verbose_name_plural = "Issue Mentions"
db_table = "issue_mentions"
ordering = ("-created_at",)
def __str__(self):
return f"{self.issue.name} {self.mention.email}"
class IssueAssignee(ProjectBaseModel): class IssueAssignee(ProjectBaseModel):
issue = models.ForeignKey( issue = models.ForeignKey(
@ -327,7 +364,9 @@ class IssueComment(ProjectBaseModel):
comment_json = models.JSONField(blank=True, default=dict) comment_json = models.JSONField(blank=True, default=dict)
comment_html = models.TextField(blank=True, default="<p></p>") comment_html = models.TextField(blank=True, default="<p></p>")
attachments = ArrayField(models.URLField(), size=10, blank=True, default=list) attachments = ArrayField(models.URLField(), size=10, blank=True, default=list)
issue = models.ForeignKey(Issue, on_delete=models.CASCADE, related_name="issue_comments") issue = models.ForeignKey(
Issue, on_delete=models.CASCADE, related_name="issue_comments"
)
# System can also create comment # System can also create comment
actor = models.ForeignKey( actor = models.ForeignKey(
settings.AUTH_USER_MODEL, settings.AUTH_USER_MODEL,
@ -367,7 +406,7 @@ class IssueProperty(ProjectBaseModel):
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name="issue_property_user", related_name="issue_property_user",
) )
properties = models.JSONField(default=dict) properties = models.JSONField(default=get_default_properties)
class Meta: class Meta:
verbose_name = "Issue Property" verbose_name = "Issue Property"
@ -516,7 +555,10 @@ class IssueVote(ProjectBaseModel):
) )
class Meta: class Meta:
unique_together = ["issue", "actor",] unique_together = [
"issue",
"actor",
]
verbose_name = "Issue Vote" verbose_name = "Issue Vote"
verbose_name_plural = "Issue Votes" verbose_name_plural = "Issue Votes"
db_table = "issue_votes" db_table = "issue_votes"

View File

@ -4,9 +4,6 @@ from uuid import uuid4
# Django imports # Django imports
from django.db import models from django.db import models
from django.conf import settings from django.conf import settings
from django.template.defaultfilters import slugify
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.core.validators import MinValueValidator, MaxValueValidator from django.core.validators import MinValueValidator, MaxValueValidator
# Modeule imports # Modeule imports

Some files were not shown because too many files have changed in this diff Show More