forked from github/plane
Compare commits
83 Commits
preview
...
feat/self_
Author | SHA1 | Date | |
---|---|---|---|
|
1bef55ca88 | ||
|
c6d6c29e51 | ||
|
0bb13148b7 | ||
|
6531e420fc | ||
|
93aeb3795d | ||
|
0a88db975a | ||
|
5afb71ada1 | ||
|
a65da91969 | ||
|
63d5951e36 | ||
|
cc158ae6da | ||
|
3f2b890a48 | ||
|
936535fef2 | ||
|
29798fa08a | ||
|
dd60dec887 | ||
|
0c1097592e | ||
|
bed66235f2 | ||
|
af5534ffe3 | ||
|
46e733a2c5 | ||
|
c4d5ccdae4 | ||
|
78d841d67e | ||
|
95c35cde4a | ||
|
6045e659bc | ||
|
ca79739ead | ||
|
44a3097ed1 | ||
|
55869ee994 | ||
|
6102de37de | ||
|
deefdac8b4 | ||
|
1e9f4dd938 | ||
|
d9a06a5fe3 | ||
|
89a5088f31 | ||
|
9789757d2d | ||
|
2aee627616 | ||
|
26b1e9d5f1 | ||
|
5b2964dc48 | ||
|
79347ec62b | ||
|
7b965179d8 | ||
|
fc51ffc589 | ||
|
96f6e37cc5 | ||
|
29774ce84a | ||
|
8cbe9c26fc | ||
|
7f42566207 | ||
|
b60237b676 | ||
|
095f64793f | ||
|
ccfd1c703e | ||
|
d05b63fc51 | ||
|
1fe09d369f | ||
|
b7757c6b1a | ||
|
1a25bacce1 | ||
|
6797df239d | ||
|
43e7c10eb7 | ||
|
bdc9c9c2a8 | ||
|
f0c72bf249 | ||
|
a8904bfc48 | ||
|
fea812f1b1 | ||
|
46f2663854 | ||
|
1eac8ead86 | ||
|
aca0f5eadb | ||
|
32a8133a9f | ||
|
efbf834e77 | ||
|
4fdd6a0e26 | ||
|
b31041726b | ||
|
e6f947ad90 | ||
|
7963993171 | ||
|
081e42ced0 | ||
|
216f07f9d8 | ||
|
589e29ee45 | ||
|
b90218580d | ||
|
f37bae9ad9 | ||
|
7eaf52bae5 | ||
|
3f78c1bbc0 | ||
|
66cb297587 | ||
|
a4f28b32f5 | ||
|
39f69b7ed2 | ||
|
485e3e6a0f | ||
|
ea54bbca49 | ||
|
f34d508a48 | ||
|
02a8caaabc | ||
|
97b7a27695 | ||
|
088615f9d6 | ||
|
b3868423f6 | ||
|
1fde71c633 | ||
|
5b2a1dda72 | ||
|
3bfbc3a132 |
10
.env.example
10
.env.example
@ -21,15 +21,15 @@ AWS_S3_BUCKET_NAME="uploads"
|
|||||||
FILE_SIZE_LIMIT=5242880
|
FILE_SIZE_LIMIT=5242880
|
||||||
|
|
||||||
# GPT settings
|
# GPT settings
|
||||||
OPENAI_API_BASE="https://api.openai.com/v1" # change if using a custom endpoint
|
OPENAI_API_BASE="https://api.openai.com/v1" # deprecated
|
||||||
OPENAI_API_KEY="sk-" # add your openai key here
|
OPENAI_API_KEY="sk-" # deprecated
|
||||||
GPT_ENGINE="gpt-3.5-turbo" # use "gpt-4" if you have access
|
GPT_ENGINE="gpt-3.5-turbo" # deprecated
|
||||||
|
|
||||||
# Settings related to Docker
|
# Settings related to Docker
|
||||||
DOCKERIZED=1
|
DOCKERIZED=1 # deprecated
|
||||||
|
|
||||||
# set to 1 If using the pre-configured minio setup
|
# set to 1 If using the pre-configured minio setup
|
||||||
USE_MINIO=1
|
USE_MINIO=1
|
||||||
|
|
||||||
# Nginx Configuration
|
# Nginx Configuration
|
||||||
NGINX_PORT=80
|
NGINX_PORT=80
|
||||||
|
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -79,3 +79,4 @@ pnpm-workspace.yaml
|
|||||||
tmp/
|
tmp/
|
||||||
## packages
|
## packages
|
||||||
dist
|
dist
|
||||||
|
.temp/
|
||||||
|
@ -43,8 +43,6 @@ FROM python:3.11.1-alpine3.17 AS backend
|
|||||||
ENV PYTHONDONTWRITEBYTECODE 1
|
ENV PYTHONDONTWRITEBYTECODE 1
|
||||||
ENV PYTHONUNBUFFERED 1
|
ENV PYTHONUNBUFFERED 1
|
||||||
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
|
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
|
||||||
ENV DJANGO_SETTINGS_MODULE plane.settings.production
|
|
||||||
ENV DOCKERIZED 1
|
|
||||||
|
|
||||||
WORKDIR /code
|
WORKDIR /code
|
||||||
|
|
||||||
|
24
ENV_SETUP.md
24
ENV_SETUP.md
@ -31,12 +31,10 @@ AWS_S3_BUCKET_NAME="uploads"
|
|||||||
FILE_SIZE_LIMIT=5242880
|
FILE_SIZE_LIMIT=5242880
|
||||||
|
|
||||||
# GPT settings
|
# GPT settings
|
||||||
OPENAI_API_BASE="https://api.openai.com/v1" # change if using a custom endpoint
|
OPENAI_API_BASE="https://api.openai.com/v1" # deprecated
|
||||||
OPENAI_API_KEY="sk-" # add your openai key here
|
OPENAI_API_KEY="sk-" # deprecated
|
||||||
GPT_ENGINE="gpt-3.5-turbo" # use "gpt-4" if you have access
|
GPT_ENGINE="gpt-3.5-turbo" # deprecated
|
||||||
|
|
||||||
# Settings related to Docker
|
|
||||||
DOCKERIZED=1
|
|
||||||
# set to 1 If using the pre-configured minio setup
|
# set to 1 If using the pre-configured minio setup
|
||||||
USE_MINIO=1
|
USE_MINIO=1
|
||||||
|
|
||||||
@ -78,7 +76,7 @@ NEXT_PUBLIC_ENABLE_OAUTH=0
|
|||||||
# 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.selfhosted" # deprecated
|
||||||
|
|
||||||
# Error logs
|
# Error logs
|
||||||
SENTRY_DSN=""
|
SENTRY_DSN=""
|
||||||
@ -115,24 +113,22 @@ AWS_S3_BUCKET_NAME="uploads"
|
|||||||
FILE_SIZE_LIMIT=5242880
|
FILE_SIZE_LIMIT=5242880
|
||||||
|
|
||||||
# GPT settings
|
# GPT settings
|
||||||
OPENAI_API_BASE="https://api.openai.com/v1" # change if using a custom endpoint
|
OPENAI_API_BASE="https://api.openai.com/v1" # deprecated
|
||||||
OPENAI_API_KEY="sk-" # add your openai key here
|
OPENAI_API_KEY="sk-" # deprecated
|
||||||
GPT_ENGINE="gpt-3.5-turbo" # use "gpt-4" if you have access
|
GPT_ENGINE="gpt-3.5-turbo" # deprecated
|
||||||
|
|
||||||
|
# Settings related to Docker
|
||||||
|
DOCKERIZED=1 # Deprecated
|
||||||
|
|
||||||
# Github
|
# Github
|
||||||
GITHUB_CLIENT_SECRET="" # For fetching release notes
|
GITHUB_CLIENT_SECRET="" # For fetching release notes
|
||||||
|
|
||||||
# Settings related to Docker
|
|
||||||
DOCKERIZED=1
|
|
||||||
# set to 1 If using the pre-configured minio setup
|
# set to 1 If using the pre-configured minio setup
|
||||||
USE_MINIO=1
|
USE_MINIO=1
|
||||||
|
|
||||||
# Nginx Configuration
|
# Nginx Configuration
|
||||||
NGINX_PORT=80
|
NGINX_PORT=80
|
||||||
|
|
||||||
# Default Creds
|
|
||||||
DEFAULT_EMAIL="captain@plane.so"
|
|
||||||
DEFAULT_PASSWORD="password123"
|
|
||||||
|
|
||||||
# SignUps
|
# SignUps
|
||||||
ENABLE_SIGNUP="1"
|
ENABLE_SIGNUP="1"
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
# 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.production"
|
CORS_ALLOWED_ORIGINS=""
|
||||||
|
|
||||||
# Error logs
|
# Error logs
|
||||||
SENTRY_DSN=""
|
SENTRY_DSN=""
|
||||||
|
SENTRY_ENVIRONMENT="development"
|
||||||
|
|
||||||
# Database Settings
|
# Database Settings
|
||||||
PGUSER="plane"
|
PGUSER="plane"
|
||||||
@ -18,15 +19,6 @@ REDIS_HOST="plane-redis"
|
|||||||
REDIS_PORT="6379"
|
REDIS_PORT="6379"
|
||||||
REDIS_URL="redis://${REDIS_HOST}: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 Settings
|
||||||
AWS_REGION=""
|
AWS_REGION=""
|
||||||
AWS_ACCESS_KEY_ID="access-key"
|
AWS_ACCESS_KEY_ID="access-key"
|
||||||
@ -38,9 +30,9 @@ AWS_S3_BUCKET_NAME="uploads"
|
|||||||
FILE_SIZE_LIMIT=5242880
|
FILE_SIZE_LIMIT=5242880
|
||||||
|
|
||||||
# GPT settings
|
# GPT settings
|
||||||
OPENAI_API_BASE="https://api.openai.com/v1" # change if using a custom endpoint
|
OPENAI_API_BASE="https://api.openai.com/v1" # deprecated
|
||||||
OPENAI_API_KEY="sk-" # add your openai key here
|
OPENAI_API_KEY="sk-" # deprecated
|
||||||
GPT_ENGINE="gpt-3.5-turbo" # use "gpt-4" if you have access
|
GPT_ENGINE="gpt-3.5-turbo" # deprecated
|
||||||
|
|
||||||
# Github
|
# Github
|
||||||
GITHUB_CLIENT_SECRET="" # For fetching release notes
|
GITHUB_CLIENT_SECRET="" # For fetching release notes
|
||||||
@ -53,9 +45,6 @@ USE_MINIO=1
|
|||||||
# Nginx Configuration
|
# Nginx Configuration
|
||||||
NGINX_PORT=80
|
NGINX_PORT=80
|
||||||
|
|
||||||
# Default Creds
|
|
||||||
DEFAULT_EMAIL="captain@plane.so"
|
|
||||||
DEFAULT_PASSWORD="password123"
|
|
||||||
|
|
||||||
# SignUps
|
# SignUps
|
||||||
ENABLE_SIGNUP="1"
|
ENABLE_SIGNUP="1"
|
||||||
@ -70,6 +59,6 @@ ENABLE_MAGIC_LINK_LOGIN="0"
|
|||||||
# Email redirections and minio domain settings
|
# Email redirections and minio domain settings
|
||||||
WEB_URL="http://localhost"
|
WEB_URL="http://localhost"
|
||||||
|
|
||||||
|
|
||||||
# Gunicorn Workers
|
# Gunicorn Workers
|
||||||
GUNICORN_WORKERS=2
|
GUNICORN_WORKERS=2
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@ USER captain
|
|||||||
COPY manage.py manage.py
|
COPY manage.py manage.py
|
||||||
COPY plane plane/
|
COPY plane plane/
|
||||||
COPY templates templates/
|
COPY templates templates/
|
||||||
|
COPY package.json package.json
|
||||||
COPY gunicorn.config.py ./
|
COPY gunicorn.config.py ./
|
||||||
USER root
|
USER root
|
||||||
RUN apk --no-cache add "bash~=5.2"
|
RUN apk --no-cache add "bash~=5.2"
|
||||||
|
@ -3,7 +3,11 @@ set -e
|
|||||||
python manage.py wait_for_db
|
python manage.py wait_for_db
|
||||||
python manage.py migrate
|
python manage.py migrate
|
||||||
|
|
||||||
# Create a Default User
|
# Register instance
|
||||||
python bin/user_script.py
|
python manage.py register_instance
|
||||||
|
# Load the configuration variable
|
||||||
|
python manage.py configure_instance
|
||||||
|
# Create the default bucket
|
||||||
|
python bin/bucket_script.py
|
||||||
|
|
||||||
exec gunicorn -w $GUNICORN_WORKERS -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:8000 --max-requests 1200 --max-requests-jitter 1000 --access-logfile -
|
exec gunicorn -w $GUNICORN_WORKERS -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:8000 --max-requests 1200 --max-requests-jitter 1000 --access-logfile -
|
||||||
|
@ -1,28 +0,0 @@
|
|||||||
import os, sys
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
sys.path.append("/code")
|
|
||||||
|
|
||||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "plane.settings.production")
|
|
||||||
import django
|
|
||||||
|
|
||||||
django.setup()
|
|
||||||
|
|
||||||
from plane.db.models import User
|
|
||||||
|
|
||||||
|
|
||||||
def populate():
|
|
||||||
default_email = os.environ.get("DEFAULT_EMAIL", "captain@plane.so")
|
|
||||||
default_password = os.environ.get("DEFAULT_PASSWORD", "password123")
|
|
||||||
|
|
||||||
if not User.objects.filter(email=default_email).exists():
|
|
||||||
user = User.objects.create(email=default_email, username=uuid.uuid4().hex)
|
|
||||||
user.set_password(default_password)
|
|
||||||
user.save()
|
|
||||||
print(f"User created with an email: {default_email}")
|
|
||||||
else:
|
|
||||||
print(f"User already exists with the default email: {default_email}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
populate()
|
|
4
apiserver/package.json
Normal file
4
apiserver/package.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"name": "plane-api",
|
||||||
|
"version": "0.13.2"
|
||||||
|
}
|
@ -1,2 +1,17 @@
|
|||||||
from .workspace import WorkSpaceBasePermission, WorkSpaceAdminPermission, WorkspaceEntityPermission, WorkspaceViewerPermission
|
|
||||||
from .project import ProjectBasePermission, ProjectEntityPermission, ProjectMemberPermission, ProjectLitePermission
|
from .workspace import (
|
||||||
|
WorkSpaceBasePermission,
|
||||||
|
WorkspaceOwnerPermission,
|
||||||
|
WorkSpaceAdminPermission,
|
||||||
|
WorkspaceEntityPermission,
|
||||||
|
WorkspaceViewerPermission,
|
||||||
|
WorkspaceUserPermission,
|
||||||
|
)
|
||||||
|
from .project import (
|
||||||
|
ProjectBasePermission,
|
||||||
|
ProjectEntityPermission,
|
||||||
|
ProjectMemberPermission,
|
||||||
|
ProjectLitePermission,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -13,14 +13,15 @@ Guest = 5
|
|||||||
|
|
||||||
class ProjectBasePermission(BasePermission):
|
class ProjectBasePermission(BasePermission):
|
||||||
def has_permission(self, request, view):
|
def has_permission(self, request, view):
|
||||||
|
|
||||||
if request.user.is_anonymous:
|
if request.user.is_anonymous:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
## Safe Methods -> Handle the filtering logic in queryset
|
## Safe Methods -> Handle the filtering logic in queryset
|
||||||
if request.method in SAFE_METHODS:
|
if request.method in SAFE_METHODS:
|
||||||
return WorkspaceMember.objects.filter(
|
return WorkspaceMember.objects.filter(
|
||||||
workspace__slug=view.workspace_slug, member=request.user
|
workspace__slug=view.workspace_slug,
|
||||||
|
member=request.user,
|
||||||
|
is_active=True,
|
||||||
).exists()
|
).exists()
|
||||||
|
|
||||||
## Only workspace owners or admins can create the projects
|
## Only workspace owners or admins can create the projects
|
||||||
@ -29,6 +30,7 @@ class ProjectBasePermission(BasePermission):
|
|||||||
workspace__slug=view.workspace_slug,
|
workspace__slug=view.workspace_slug,
|
||||||
member=request.user,
|
member=request.user,
|
||||||
role__in=[Admin, Member],
|
role__in=[Admin, Member],
|
||||||
|
is_active=True,
|
||||||
).exists()
|
).exists()
|
||||||
|
|
||||||
## Only Project Admins can update project attributes
|
## Only Project Admins can update project attributes
|
||||||
@ -37,19 +39,21 @@ class ProjectBasePermission(BasePermission):
|
|||||||
member=request.user,
|
member=request.user,
|
||||||
role=Admin,
|
role=Admin,
|
||||||
project_id=view.project_id,
|
project_id=view.project_id,
|
||||||
|
is_active=True,
|
||||||
).exists()
|
).exists()
|
||||||
|
|
||||||
|
|
||||||
class ProjectMemberPermission(BasePermission):
|
class ProjectMemberPermission(BasePermission):
|
||||||
def has_permission(self, request, view):
|
def has_permission(self, request, view):
|
||||||
|
|
||||||
if request.user.is_anonymous:
|
if request.user.is_anonymous:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
## Safe Methods -> Handle the filtering logic in queryset
|
## Safe Methods -> Handle the filtering logic in queryset
|
||||||
if request.method in SAFE_METHODS:
|
if request.method in SAFE_METHODS:
|
||||||
return ProjectMember.objects.filter(
|
return ProjectMember.objects.filter(
|
||||||
workspace__slug=view.workspace_slug, member=request.user
|
workspace__slug=view.workspace_slug,
|
||||||
|
member=request.user,
|
||||||
|
is_active=True,
|
||||||
).exists()
|
).exists()
|
||||||
## Only workspace owners or admins can create the projects
|
## Only workspace owners or admins can create the projects
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
@ -57,6 +61,7 @@ class ProjectMemberPermission(BasePermission):
|
|||||||
workspace__slug=view.workspace_slug,
|
workspace__slug=view.workspace_slug,
|
||||||
member=request.user,
|
member=request.user,
|
||||||
role__in=[Admin, Member],
|
role__in=[Admin, Member],
|
||||||
|
is_active=True,
|
||||||
).exists()
|
).exists()
|
||||||
|
|
||||||
## Only Project Admins can update project attributes
|
## Only Project Admins can update project attributes
|
||||||
@ -65,12 +70,12 @@ class ProjectMemberPermission(BasePermission):
|
|||||||
member=request.user,
|
member=request.user,
|
||||||
role__in=[Admin, Member],
|
role__in=[Admin, Member],
|
||||||
project_id=view.project_id,
|
project_id=view.project_id,
|
||||||
|
is_active=True,
|
||||||
).exists()
|
).exists()
|
||||||
|
|
||||||
|
|
||||||
class ProjectEntityPermission(BasePermission):
|
class ProjectEntityPermission(BasePermission):
|
||||||
def has_permission(self, request, view):
|
def has_permission(self, request, view):
|
||||||
|
|
||||||
if request.user.is_anonymous:
|
if request.user.is_anonymous:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -80,6 +85,7 @@ class ProjectEntityPermission(BasePermission):
|
|||||||
workspace__slug=view.workspace_slug,
|
workspace__slug=view.workspace_slug,
|
||||||
member=request.user,
|
member=request.user,
|
||||||
project_id=view.project_id,
|
project_id=view.project_id,
|
||||||
|
is_active=True,
|
||||||
).exists()
|
).exists()
|
||||||
|
|
||||||
## Only project members or admins can create and edit the project attributes
|
## Only project members or admins can create and edit the project attributes
|
||||||
@ -88,11 +94,11 @@ class ProjectEntityPermission(BasePermission):
|
|||||||
member=request.user,
|
member=request.user,
|
||||||
role__in=[Admin, Member],
|
role__in=[Admin, Member],
|
||||||
project_id=view.project_id,
|
project_id=view.project_id,
|
||||||
|
is_active=True,
|
||||||
).exists()
|
).exists()
|
||||||
|
|
||||||
|
|
||||||
class ProjectLitePermission(BasePermission):
|
class ProjectLitePermission(BasePermission):
|
||||||
|
|
||||||
def has_permission(self, request, view):
|
def has_permission(self, request, view):
|
||||||
if request.user.is_anonymous:
|
if request.user.is_anonymous:
|
||||||
return False
|
return False
|
||||||
@ -101,4 +107,5 @@ class ProjectLitePermission(BasePermission):
|
|||||||
workspace__slug=view.workspace_slug,
|
workspace__slug=view.workspace_slug,
|
||||||
member=request.user,
|
member=request.user,
|
||||||
project_id=view.project_id,
|
project_id=view.project_id,
|
||||||
|
is_active=True,
|
||||||
).exists()
|
).exists()
|
||||||
|
@ -32,12 +32,28 @@ class WorkSpaceBasePermission(BasePermission):
|
|||||||
member=request.user,
|
member=request.user,
|
||||||
workspace__slug=view.workspace_slug,
|
workspace__slug=view.workspace_slug,
|
||||||
role__in=[Owner, Admin],
|
role__in=[Owner, Admin],
|
||||||
|
is_active=True,
|
||||||
).exists()
|
).exists()
|
||||||
|
|
||||||
# allow only owner to delete the workspace
|
# allow only owner to delete the workspace
|
||||||
if request.method == "DELETE":
|
if request.method == "DELETE":
|
||||||
return WorkspaceMember.objects.filter(
|
return WorkspaceMember.objects.filter(
|
||||||
member=request.user, workspace__slug=view.workspace_slug, role=Owner
|
member=request.user,
|
||||||
|
workspace__slug=view.workspace_slug,
|
||||||
|
role=Owner,
|
||||||
|
is_active=True,
|
||||||
|
).exists()
|
||||||
|
|
||||||
|
|
||||||
|
class WorkspaceOwnerPermission(BasePermission):
|
||||||
|
def has_permission(self, request, view):
|
||||||
|
if request.user.is_anonymous:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return WorkspaceMember.objects.filter(
|
||||||
|
workspace__slug=view.workspace_slug,
|
||||||
|
member=request.user,
|
||||||
|
role=Owner,
|
||||||
).exists()
|
).exists()
|
||||||
|
|
||||||
|
|
||||||
@ -50,6 +66,7 @@ class WorkSpaceAdminPermission(BasePermission):
|
|||||||
member=request.user,
|
member=request.user,
|
||||||
workspace__slug=view.workspace_slug,
|
workspace__slug=view.workspace_slug,
|
||||||
role__in=[Owner, Admin],
|
role__in=[Owner, Admin],
|
||||||
|
is_active=True,
|
||||||
).exists()
|
).exists()
|
||||||
|
|
||||||
|
|
||||||
@ -63,12 +80,14 @@ class WorkspaceEntityPermission(BasePermission):
|
|||||||
return WorkspaceMember.objects.filter(
|
return WorkspaceMember.objects.filter(
|
||||||
workspace__slug=view.workspace_slug,
|
workspace__slug=view.workspace_slug,
|
||||||
member=request.user,
|
member=request.user,
|
||||||
|
is_active=True,
|
||||||
).exists()
|
).exists()
|
||||||
|
|
||||||
return WorkspaceMember.objects.filter(
|
return WorkspaceMember.objects.filter(
|
||||||
member=request.user,
|
member=request.user,
|
||||||
workspace__slug=view.workspace_slug,
|
workspace__slug=view.workspace_slug,
|
||||||
role__in=[Owner, Admin],
|
role__in=[Owner, Admin],
|
||||||
|
is_active=True,
|
||||||
).exists()
|
).exists()
|
||||||
|
|
||||||
|
|
||||||
@ -78,5 +97,20 @@ class WorkspaceViewerPermission(BasePermission):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
return WorkspaceMember.objects.filter(
|
return WorkspaceMember.objects.filter(
|
||||||
member=request.user, workspace__slug=view.workspace_slug, role__gte=10
|
member=request.user,
|
||||||
|
workspace__slug=view.workspace_slug,
|
||||||
|
role__gte=10,
|
||||||
|
is_active=True,
|
||||||
|
).exists()
|
||||||
|
|
||||||
|
|
||||||
|
class WorkspaceUserPermission(BasePermission):
|
||||||
|
def has_permission(self, request, view):
|
||||||
|
if request.user.is_anonymous:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return WorkspaceMember.objects.filter(
|
||||||
|
member=request.user,
|
||||||
|
workspace__slug=view.workspace_slug,
|
||||||
|
is_active=True,
|
||||||
).exists()
|
).exists()
|
||||||
|
@ -71,7 +71,7 @@ from .module import (
|
|||||||
ModuleFavoriteSerializer,
|
ModuleFavoriteSerializer,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .api_token import APITokenSerializer
|
from .api import APITokenSerializer, APITokenReadSerializer
|
||||||
|
|
||||||
from .integration import (
|
from .integration import (
|
||||||
IntegrationSerializer,
|
IntegrationSerializer,
|
||||||
@ -85,7 +85,7 @@ from .integration import (
|
|||||||
|
|
||||||
from .importer import ImporterSerializer
|
from .importer import ImporterSerializer
|
||||||
|
|
||||||
from .page import PageSerializer, PageBlockSerializer, PageFavoriteSerializer
|
from .page import PageSerializer, PageLogSerializer, SubPageSerializer, PageFavoriteSerializer
|
||||||
|
|
||||||
from .estimate import (
|
from .estimate import (
|
||||||
EstimateSerializer,
|
EstimateSerializer,
|
||||||
@ -100,3 +100,5 @@ from .analytic import AnalyticViewSerializer
|
|||||||
from .notification import NotificationSerializer
|
from .notification import NotificationSerializer
|
||||||
|
|
||||||
from .exporter import ExporterHistorySerializer
|
from .exporter import ExporterHistorySerializer
|
||||||
|
|
||||||
|
from .webhook import WebhookSerializer, WebhookLogSerializer
|
31
apiserver/plane/api/serializers/api.py
Normal file
31
apiserver/plane/api/serializers/api.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
from .base import BaseSerializer
|
||||||
|
from plane.db.models import APIToken, APIActivityLog
|
||||||
|
|
||||||
|
|
||||||
|
class APITokenSerializer(BaseSerializer):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = APIToken
|
||||||
|
fields = "__all__"
|
||||||
|
read_only_fields = [
|
||||||
|
"token",
|
||||||
|
"expired_at",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
"workspace",
|
||||||
|
"user",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class APITokenReadSerializer(BaseSerializer):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = APIToken
|
||||||
|
exclude = ('token',)
|
||||||
|
|
||||||
|
|
||||||
|
class APIActivityLogSerializer(BaseSerializer):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = APIActivityLog
|
||||||
|
fields = "__all__"
|
@ -1,14 +0,0 @@
|
|||||||
from .base import BaseSerializer
|
|
||||||
from plane.db.models import APIToken
|
|
||||||
|
|
||||||
|
|
||||||
class APITokenSerializer(BaseSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = APIToken
|
|
||||||
fields = [
|
|
||||||
"label",
|
|
||||||
"user",
|
|
||||||
"user_type",
|
|
||||||
"workspace",
|
|
||||||
"created_at",
|
|
||||||
]
|
|
@ -6,28 +6,7 @@ from .base import BaseSerializer
|
|||||||
from .issue import IssueFlatSerializer, LabelLiteSerializer
|
from .issue import IssueFlatSerializer, LabelLiteSerializer
|
||||||
from .workspace import WorkspaceLiteSerializer
|
from .workspace import WorkspaceLiteSerializer
|
||||||
from .project import ProjectLiteSerializer
|
from .project import ProjectLiteSerializer
|
||||||
from plane.db.models import Page, PageBlock, PageFavorite, PageLabel, Label
|
from plane.db.models import Page, PageLog, PageFavorite, PageLabel, Label, Issue, Module
|
||||||
|
|
||||||
|
|
||||||
class PageBlockSerializer(BaseSerializer):
|
|
||||||
issue_detail = IssueFlatSerializer(source="issue", read_only=True)
|
|
||||||
project_detail = ProjectLiteSerializer(source="project", read_only=True)
|
|
||||||
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = PageBlock
|
|
||||||
fields = "__all__"
|
|
||||||
read_only_fields = [
|
|
||||||
"workspace",
|
|
||||||
"project",
|
|
||||||
"page",
|
|
||||||
]
|
|
||||||
|
|
||||||
class PageBlockLiteSerializer(BaseSerializer):
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = PageBlock
|
|
||||||
fields = "__all__"
|
|
||||||
|
|
||||||
|
|
||||||
class PageSerializer(BaseSerializer):
|
class PageSerializer(BaseSerializer):
|
||||||
@ -38,7 +17,6 @@ class PageSerializer(BaseSerializer):
|
|||||||
write_only=True,
|
write_only=True,
|
||||||
required=False,
|
required=False,
|
||||||
)
|
)
|
||||||
blocks = PageBlockLiteSerializer(read_only=True, many=True)
|
|
||||||
project_detail = ProjectLiteSerializer(source="project", read_only=True)
|
project_detail = ProjectLiteSerializer(source="project", read_only=True)
|
||||||
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
|
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
|
||||||
|
|
||||||
@ -102,6 +80,41 @@ class PageSerializer(BaseSerializer):
|
|||||||
return super().update(instance, validated_data)
|
return super().update(instance, validated_data)
|
||||||
|
|
||||||
|
|
||||||
|
class SubPageSerializer(BaseSerializer):
|
||||||
|
entity_details = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = PageLog
|
||||||
|
fields = "__all__"
|
||||||
|
read_only_fields = [
|
||||||
|
"workspace",
|
||||||
|
"project",
|
||||||
|
"page",
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_entity_details(self, obj):
|
||||||
|
entity_name = obj.entity_name
|
||||||
|
if entity_name == 'forward_link' or entity_name == 'back_link':
|
||||||
|
try:
|
||||||
|
page = Page.objects.get(pk=obj.entity_identifier)
|
||||||
|
return PageSerializer(page).data
|
||||||
|
except Page.DoesNotExist:
|
||||||
|
return None
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class PageLogSerializer(BaseSerializer):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = PageLog
|
||||||
|
fields = "__all__"
|
||||||
|
read_only_fields = [
|
||||||
|
"workspace",
|
||||||
|
"project",
|
||||||
|
"page",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class PageFavoriteSerializer(BaseSerializer):
|
class PageFavoriteSerializer(BaseSerializer):
|
||||||
page_detail = PageSerializer(source="page", read_only=True)
|
page_detail = PageSerializer(source="page", read_only=True)
|
||||||
|
|
||||||
|
@ -103,13 +103,16 @@ class ProjectListSerializer(DynamicBaseSerializer):
|
|||||||
members = serializers.SerializerMethodField()
|
members = serializers.SerializerMethodField()
|
||||||
|
|
||||||
def get_members(self, obj):
|
def get_members(self, obj):
|
||||||
project_members = ProjectMember.objects.filter(project_id=obj.id).values(
|
project_members = ProjectMember.objects.filter(
|
||||||
|
project_id=obj.id,
|
||||||
|
is_active=True,
|
||||||
|
).values(
|
||||||
"id",
|
"id",
|
||||||
"member_id",
|
"member_id",
|
||||||
"member__display_name",
|
"member__display_name",
|
||||||
"member__avatar",
|
"member__avatar",
|
||||||
)
|
)
|
||||||
return project_members
|
return list(project_members)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Project
|
model = Project
|
||||||
|
@ -4,6 +4,7 @@ from rest_framework import serializers
|
|||||||
# Module import
|
# Module import
|
||||||
from .base import BaseSerializer
|
from .base import BaseSerializer
|
||||||
from plane.db.models import User, Workspace, WorkspaceMemberInvite
|
from plane.db.models import User, Workspace, WorkspaceMemberInvite
|
||||||
|
from plane.license.models import InstanceAdmin, Instance
|
||||||
|
|
||||||
|
|
||||||
class UserSerializer(BaseSerializer):
|
class UserSerializer(BaseSerializer):
|
||||||
@ -86,7 +87,9 @@ class UserMeSettingsSerializer(BaseSerializer):
|
|||||||
"last_workspace_id": obj.last_workspace_id,
|
"last_workspace_id": obj.last_workspace_id,
|
||||||
"last_workspace_slug": workspace.slug if workspace is not None else "",
|
"last_workspace_slug": workspace.slug if workspace is not None else "",
|
||||||
"fallback_workspace_id": obj.last_workspace_id,
|
"fallback_workspace_id": obj.last_workspace_id,
|
||||||
"fallback_workspace_slug": workspace.slug if workspace is not None else "",
|
"fallback_workspace_slug": workspace.slug
|
||||||
|
if workspace is not None
|
||||||
|
else "",
|
||||||
"invites": workspace_invites,
|
"invites": workspace_invites,
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
|
30
apiserver/plane/api/serializers/webhook.py
Normal file
30
apiserver/plane/api/serializers/webhook.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# Third party imports
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
# Module imports
|
||||||
|
from .base import DynamicBaseSerializer
|
||||||
|
from plane.db.models import Webhook, WebhookLog
|
||||||
|
from plane.db.models.webhook import validate_domain, validate_schema
|
||||||
|
|
||||||
|
class WebhookSerializer(DynamicBaseSerializer):
|
||||||
|
url = serializers.URLField(validators=[validate_schema, validate_domain])
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Webhook
|
||||||
|
fields = "__all__"
|
||||||
|
read_only_fields = [
|
||||||
|
"workspace",
|
||||||
|
"secret_key",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class WebhookLogSerializer(DynamicBaseSerializer):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = WebhookLog
|
||||||
|
fields = "__all__"
|
||||||
|
read_only_fields = [
|
||||||
|
"workspace",
|
||||||
|
"webhook"
|
||||||
|
]
|
||||||
|
|
@ -19,6 +19,12 @@ from .state import urlpatterns as state_urls
|
|||||||
from .user import urlpatterns as user_urls
|
from .user import urlpatterns as user_urls
|
||||||
from .views import urlpatterns as view_urls
|
from .views import urlpatterns as view_urls
|
||||||
from .workspace import urlpatterns as workspace_urls
|
from .workspace import urlpatterns as workspace_urls
|
||||||
|
from .api import urlpatterns as api_urls
|
||||||
|
from .webhook import urlpatterns as webhook_urls
|
||||||
|
|
||||||
|
|
||||||
|
# Django imports
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
@ -43,4 +49,6 @@ urlpatterns = [
|
|||||||
*user_urls,
|
*user_urls,
|
||||||
*view_urls,
|
*view_urls,
|
||||||
*workspace_urls,
|
*workspace_urls,
|
||||||
|
*api_urls,
|
||||||
|
*webhook_urls,
|
||||||
]
|
]
|
||||||
|
17
apiserver/plane/api/urls/api.py
Normal file
17
apiserver/plane/api/urls/api.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
from django.urls import path
|
||||||
|
from plane.api.views import ApiTokenEndpoint
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
# API Tokens
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/api-tokens/",
|
||||||
|
ApiTokenEndpoint.as_view(),
|
||||||
|
name="api-tokens",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/api-tokens/<uuid:pk>/",
|
||||||
|
ApiTokenEndpoint.as_view(),
|
||||||
|
name="api-tokens",
|
||||||
|
),
|
||||||
|
## End API Tokens
|
||||||
|
]
|
@ -3,9 +3,9 @@ from django.urls import path
|
|||||||
|
|
||||||
from plane.api.views import (
|
from plane.api.views import (
|
||||||
PageViewSet,
|
PageViewSet,
|
||||||
PageBlockViewSet,
|
|
||||||
PageFavoriteViewSet,
|
PageFavoriteViewSet,
|
||||||
CreateIssueFromPageBlockEndpoint,
|
PageLogEndpoint,
|
||||||
|
SubPagesEndpoint,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -31,27 +31,6 @@ urlpatterns = [
|
|||||||
),
|
),
|
||||||
name="project-pages",
|
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(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-pages/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-pages/",
|
||||||
PageFavoriteViewSet.as_view(
|
PageFavoriteViewSet.as_view(
|
||||||
@ -72,8 +51,83 @@ urlpatterns = [
|
|||||||
name="user-favorite-pages",
|
name="user-favorite-pages",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/page-blocks/<uuid:page_block_id>/issues/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/",
|
||||||
CreateIssueFromPageBlockEndpoint.as_view(),
|
PageViewSet.as_view(
|
||||||
name="page-block-issues",
|
{
|
||||||
|
"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>/archive/",
|
||||||
|
PageViewSet.as_view(
|
||||||
|
{
|
||||||
|
"post": "archive",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-page-archive",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/unarchive/",
|
||||||
|
PageViewSet.as_view(
|
||||||
|
{
|
||||||
|
"post": "unarchive",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-page-unarchive",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-pages/",
|
||||||
|
PageViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "archive_list",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-pages",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/lock/",
|
||||||
|
PageViewSet.as_view(
|
||||||
|
{
|
||||||
|
"post": "lock",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-pages",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/unlock/",
|
||||||
|
PageViewSet.as_view(
|
||||||
|
{
|
||||||
|
"post": "unlock",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/transactions/",
|
||||||
|
PageLogEndpoint.as_view(),
|
||||||
|
name="page-transactions",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/transactions/<uuid:transaction>/",
|
||||||
|
PageLogEndpoint.as_view(),
|
||||||
|
name="page-transactions",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/sub-pages/",
|
||||||
|
SubPagesEndpoint.as_view(),
|
||||||
|
name="sub-page",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -2,17 +2,16 @@ from django.urls import path
|
|||||||
|
|
||||||
from plane.api.views import (
|
from plane.api.views import (
|
||||||
ProjectViewSet,
|
ProjectViewSet,
|
||||||
InviteProjectEndpoint,
|
ProjectInvitationsViewset,
|
||||||
ProjectMemberViewSet,
|
ProjectMemberViewSet,
|
||||||
ProjectMemberInvitationsViewset,
|
|
||||||
ProjectMemberUserEndpoint,
|
ProjectMemberUserEndpoint,
|
||||||
ProjectJoinEndpoint,
|
ProjectJoinEndpoint,
|
||||||
AddTeamToProjectEndpoint,
|
AddTeamToProjectEndpoint,
|
||||||
ProjectUserViewsEndpoint,
|
ProjectUserViewsEndpoint,
|
||||||
ProjectIdentifierEndpoint,
|
ProjectIdentifierEndpoint,
|
||||||
ProjectFavoritesViewSet,
|
ProjectFavoritesViewSet,
|
||||||
LeaveProjectEndpoint,
|
|
||||||
ProjectPublicCoverImagesEndpoint,
|
ProjectPublicCoverImagesEndpoint,
|
||||||
|
UserProjectInvitationsViewset,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -45,13 +44,48 @@ urlpatterns = [
|
|||||||
name="project-identifiers",
|
name="project-identifiers",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/invite/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/invitations/",
|
||||||
InviteProjectEndpoint.as_view(),
|
ProjectInvitationsViewset.as_view(
|
||||||
name="invite-project",
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
name="project-member-invite",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/invitations/<uuid:pk>/",
|
||||||
|
ProjectInvitationsViewset.as_view(
|
||||||
|
{
|
||||||
|
"get": "retrieve",
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-member-invite",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"users/me/invitations/projects/",
|
||||||
|
UserProjectInvitationsViewset.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
name="user-project-invitations",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/join/",
|
||||||
|
ProjectJoinEndpoint.as_view(),
|
||||||
|
name="project-join",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/members/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/members/",
|
||||||
ProjectMemberViewSet.as_view({"get": "list", "post": "create"}),
|
ProjectMemberViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
}
|
||||||
|
),
|
||||||
name="project-member",
|
name="project-member",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
@ -66,30 +100,19 @@ urlpatterns = [
|
|||||||
name="project-member",
|
name="project-member",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/join/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/members/leave/",
|
||||||
ProjectJoinEndpoint.as_view(),
|
ProjectMemberViewSet.as_view(
|
||||||
name="project-join",
|
{
|
||||||
|
"post": "leave",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-member",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/team-invite/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/team-invite/",
|
||||||
AddTeamToProjectEndpoint.as_view(),
|
AddTeamToProjectEndpoint.as_view(),
|
||||||
name="projects",
|
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(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/project-views/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/project-views/",
|
||||||
ProjectUserViewsEndpoint.as_view(),
|
ProjectUserViewsEndpoint.as_view(),
|
||||||
@ -119,11 +142,6 @@ urlpatterns = [
|
|||||||
),
|
),
|
||||||
name="project-favorite",
|
name="project-favorite",
|
||||||
),
|
),
|
||||||
path(
|
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/members/leave/",
|
|
||||||
LeaveProjectEndpoint.as_view(),
|
|
||||||
name="leave-project",
|
|
||||||
),
|
|
||||||
path(
|
path(
|
||||||
"project-covers/",
|
"project-covers/",
|
||||||
ProjectPublicCoverImagesEndpoint.as_view(),
|
ProjectPublicCoverImagesEndpoint.as_view(),
|
||||||
|
@ -9,15 +9,10 @@ from plane.api.views import (
|
|||||||
ChangePasswordEndpoint,
|
ChangePasswordEndpoint,
|
||||||
## End User
|
## End User
|
||||||
## Workspaces
|
## Workspaces
|
||||||
UserWorkspaceInvitationsEndpoint,
|
|
||||||
UserWorkSpacesEndpoint,
|
UserWorkSpacesEndpoint,
|
||||||
JoinWorkspaceEndpoint,
|
|
||||||
UserWorkspaceInvitationsEndpoint,
|
|
||||||
UserWorkspaceInvitationEndpoint,
|
|
||||||
UserActivityGraphEndpoint,
|
UserActivityGraphEndpoint,
|
||||||
UserIssueCompletedGraphEndpoint,
|
UserIssueCompletedGraphEndpoint,
|
||||||
UserWorkspaceDashboardEndpoint,
|
UserWorkspaceDashboardEndpoint,
|
||||||
UserProjectInvitationsViewset,
|
|
||||||
## End Workspaces
|
## End Workspaces
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -26,7 +21,11 @@ urlpatterns = [
|
|||||||
path(
|
path(
|
||||||
"users/me/",
|
"users/me/",
|
||||||
UserEndpoint.as_view(
|
UserEndpoint.as_view(
|
||||||
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
|
{
|
||||||
|
"get": "retrieve",
|
||||||
|
"patch": "partial_update",
|
||||||
|
"delete": "deactivate",
|
||||||
|
}
|
||||||
),
|
),
|
||||||
name="users",
|
name="users",
|
||||||
),
|
),
|
||||||
@ -39,6 +38,15 @@ urlpatterns = [
|
|||||||
),
|
),
|
||||||
name="users",
|
name="users",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"users/me/instance-admin/",
|
||||||
|
UserEndpoint.as_view(
|
||||||
|
{
|
||||||
|
"get": "retrieve_instance_admin",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="users",
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"users/me/change-password/",
|
"users/me/change-password/",
|
||||||
ChangePasswordEndpoint.as_view(),
|
ChangePasswordEndpoint.as_view(),
|
||||||
@ -65,23 +73,6 @@ urlpatterns = [
|
|||||||
UserWorkSpacesEndpoint.as_view(),
|
UserWorkSpacesEndpoint.as_view(),
|
||||||
name="user-workspace",
|
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
|
# User Graphs
|
||||||
path(
|
path(
|
||||||
"users/me/workspaces/<str:slug>/activity-graph/",
|
"users/me/workspaces/<str:slug>/activity-graph/",
|
||||||
@ -99,15 +90,4 @@ urlpatterns = [
|
|||||||
name="user-workspace-dashboard",
|
name="user-workspace-dashboard",
|
||||||
),
|
),
|
||||||
## End User Graph
|
## 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",
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
31
apiserver/plane/api/urls/webhook.py
Normal file
31
apiserver/plane/api/urls/webhook.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from plane.api.views import (
|
||||||
|
WebhookEndpoint,
|
||||||
|
WebhookLogsEndpoint,
|
||||||
|
WebhookSecretRegenerateEndpoint,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/webhooks/",
|
||||||
|
WebhookEndpoint.as_view(),
|
||||||
|
name="webhooks",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/webhooks/<uuid:pk>/",
|
||||||
|
WebhookEndpoint.as_view(),
|
||||||
|
name="webhooks",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/webhooks/<uuid:pk>/regenerate/",
|
||||||
|
WebhookSecretRegenerateEndpoint.as_view(),
|
||||||
|
name="webhooks",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/webhook-logs/<uuid:webhook_id>/",
|
||||||
|
WebhookLogsEndpoint.as_view(),
|
||||||
|
name="webhooks",
|
||||||
|
),
|
||||||
|
]
|
@ -2,8 +2,9 @@ from django.urls import path
|
|||||||
|
|
||||||
|
|
||||||
from plane.api.views import (
|
from plane.api.views import (
|
||||||
|
UserWorkspaceInvitationsViewSet,
|
||||||
WorkSpaceViewSet,
|
WorkSpaceViewSet,
|
||||||
InviteWorkspaceEndpoint,
|
WorkspaceJoinEndpoint,
|
||||||
WorkSpaceMemberViewSet,
|
WorkSpaceMemberViewSet,
|
||||||
WorkspaceInvitationsViewset,
|
WorkspaceInvitationsViewset,
|
||||||
WorkspaceMemberUserEndpoint,
|
WorkspaceMemberUserEndpoint,
|
||||||
@ -17,7 +18,6 @@ from plane.api.views import (
|
|||||||
WorkspaceUserProfileEndpoint,
|
WorkspaceUserProfileEndpoint,
|
||||||
WorkspaceUserProfileIssuesEndpoint,
|
WorkspaceUserProfileIssuesEndpoint,
|
||||||
WorkspaceLabelsEndpoint,
|
WorkspaceLabelsEndpoint,
|
||||||
LeaveWorkspaceEndpoint,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -49,14 +49,14 @@ urlpatterns = [
|
|||||||
),
|
),
|
||||||
name="workspace",
|
name="workspace",
|
||||||
),
|
),
|
||||||
path(
|
|
||||||
"workspaces/<str:slug>/invite/",
|
|
||||||
InviteWorkspaceEndpoint.as_view(),
|
|
||||||
name="invite-workspace",
|
|
||||||
),
|
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/invitations/",
|
"workspaces/<str:slug>/invitations/",
|
||||||
WorkspaceInvitationsViewset.as_view({"get": "list"}),
|
WorkspaceInvitationsViewset.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
},
|
||||||
|
),
|
||||||
name="workspace-invitations",
|
name="workspace-invitations",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
@ -69,6 +69,23 @@ urlpatterns = [
|
|||||||
),
|
),
|
||||||
name="workspace-invitations",
|
name="workspace-invitations",
|
||||||
),
|
),
|
||||||
|
# user workspace invitations
|
||||||
|
path(
|
||||||
|
"users/me/workspaces/invitations/",
|
||||||
|
UserWorkspaceInvitationsViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
name="user-workspace-invitations",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/invitations/<uuid:pk>/join/",
|
||||||
|
WorkspaceJoinEndpoint.as_view(),
|
||||||
|
name="workspace-join",
|
||||||
|
),
|
||||||
|
# user join workspace
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/members/",
|
"workspaces/<str:slug>/members/",
|
||||||
WorkSpaceMemberViewSet.as_view({"get": "list"}),
|
WorkSpaceMemberViewSet.as_view({"get": "list"}),
|
||||||
@ -85,6 +102,15 @@ urlpatterns = [
|
|||||||
),
|
),
|
||||||
name="workspace-member",
|
name="workspace-member",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/members/leave/",
|
||||||
|
WorkSpaceMemberViewSet.as_view(
|
||||||
|
{
|
||||||
|
"post": "leave",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
name="leave-workspace-members",
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/teams/",
|
"workspaces/<str:slug>/teams/",
|
||||||
TeamMemberViewSet.as_view(
|
TeamMemberViewSet.as_view(
|
||||||
@ -168,9 +194,4 @@ urlpatterns = [
|
|||||||
WorkspaceLabelsEndpoint.as_view(),
|
WorkspaceLabelsEndpoint.as_view(),
|
||||||
name="workspace-labels",
|
name="workspace-labels",
|
||||||
),
|
),
|
||||||
path(
|
|
||||||
"workspaces/<str:slug>/members/leave/",
|
|
||||||
LeaveWorkspaceEndpoint.as_view(),
|
|
||||||
name="leave-workspace-members",
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
@ -124,9 +124,10 @@ from plane.api.views import (
|
|||||||
## End Modules
|
## End Modules
|
||||||
# Pages
|
# Pages
|
||||||
PageViewSet,
|
PageViewSet,
|
||||||
PageBlockViewSet,
|
PageLogEndpoint,
|
||||||
|
SubPagesEndpoint,
|
||||||
PageFavoriteViewSet,
|
PageFavoriteViewSet,
|
||||||
CreateIssueFromPageBlockEndpoint,
|
CreateIssueFromBlockEndpoint,
|
||||||
## End Pages
|
## End Pages
|
||||||
# Api Tokens
|
# Api Tokens
|
||||||
ApiTokenEndpoint,
|
ApiTokenEndpoint,
|
||||||
@ -1222,25 +1223,81 @@ urlpatterns = [
|
|||||||
name="project-pages",
|
name="project-pages",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/page-blocks/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/archive/",
|
||||||
PageBlockViewSet.as_view(
|
PageViewSet.as_view(
|
||||||
|
{
|
||||||
|
"post": "archive",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-page-archive",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/unarchive/",
|
||||||
|
PageViewSet.as_view(
|
||||||
|
{
|
||||||
|
"post": "unarchive",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-page-unarchive"
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-pages/",
|
||||||
|
PageViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "archive_list",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-pages",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/lock/",
|
||||||
|
PageViewSet.as_view(
|
||||||
|
{
|
||||||
|
"post": "lock",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-pages",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/unlock/",
|
||||||
|
PageViewSet.as_view(
|
||||||
|
{
|
||||||
|
"post": "unlock",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/transactions/",
|
||||||
|
PageLogEndpoint.as_view(), name="page-transactions"
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/transactions/<uuid:transaction>/",
|
||||||
|
PageLogEndpoint.as_view(), name="page-transactions"
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/sub-pages/",
|
||||||
|
SubPagesEndpoint.as_view(), name="sub-page"
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/",
|
||||||
|
BulkEstimatePointEndpoint.as_view(
|
||||||
{
|
{
|
||||||
"get": "list",
|
"get": "list",
|
||||||
"post": "create",
|
"post": "create",
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
name="project-page-blocks",
|
name="bulk-create-estimate-points",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/page-blocks/<uuid:pk>/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/<uuid:estimate_id>/",
|
||||||
PageBlockViewSet.as_view(
|
BulkEstimatePointEndpoint.as_view(
|
||||||
{
|
{
|
||||||
"get": "retrieve",
|
"get": "retrieve",
|
||||||
"patch": "partial_update",
|
"patch": "partial_update",
|
||||||
"delete": "destroy",
|
"delete": "destroy",
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
name="project-page-blocks",
|
name="bulk-create-estimate-points",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-pages/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-pages/",
|
||||||
@ -1263,7 +1320,7 @@ urlpatterns = [
|
|||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/page-blocks/<uuid:page_block_id>/issues/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/page-blocks/<uuid:page_block_id>/issues/",
|
||||||
CreateIssueFromPageBlockEndpoint.as_view(),
|
CreateIssueFromBlockEndpoint.as_view(),
|
||||||
name="page-block-issues",
|
name="page-block-issues",
|
||||||
),
|
),
|
||||||
## End Pages
|
## End Pages
|
||||||
|
@ -2,10 +2,8 @@ from .project import (
|
|||||||
ProjectViewSet,
|
ProjectViewSet,
|
||||||
ProjectMemberViewSet,
|
ProjectMemberViewSet,
|
||||||
UserProjectInvitationsViewset,
|
UserProjectInvitationsViewset,
|
||||||
InviteProjectEndpoint,
|
ProjectInvitationsViewset,
|
||||||
AddTeamToProjectEndpoint,
|
AddTeamToProjectEndpoint,
|
||||||
ProjectMemberInvitationsViewset,
|
|
||||||
ProjectMemberInviteDetailViewSet,
|
|
||||||
ProjectIdentifierEndpoint,
|
ProjectIdentifierEndpoint,
|
||||||
ProjectJoinEndpoint,
|
ProjectJoinEndpoint,
|
||||||
ProjectUserViewsEndpoint,
|
ProjectUserViewsEndpoint,
|
||||||
@ -14,7 +12,6 @@ from .project import (
|
|||||||
ProjectDeployBoardViewSet,
|
ProjectDeployBoardViewSet,
|
||||||
ProjectDeployBoardPublicSettingsEndpoint,
|
ProjectDeployBoardPublicSettingsEndpoint,
|
||||||
WorkspaceProjectDeployBoardEndpoint,
|
WorkspaceProjectDeployBoardEndpoint,
|
||||||
LeaveProjectEndpoint,
|
|
||||||
ProjectPublicCoverImagesEndpoint,
|
ProjectPublicCoverImagesEndpoint,
|
||||||
)
|
)
|
||||||
from .user import (
|
from .user import (
|
||||||
@ -26,19 +23,17 @@ from .user import (
|
|||||||
|
|
||||||
from .oauth import OauthEndpoint
|
from .oauth import OauthEndpoint
|
||||||
|
|
||||||
from .base import BaseAPIView, BaseViewSet
|
from .base import BaseAPIView, BaseViewSet, WebhookMixin
|
||||||
|
|
||||||
from .workspace import (
|
from .workspace import (
|
||||||
WorkSpaceViewSet,
|
WorkSpaceViewSet,
|
||||||
UserWorkSpacesEndpoint,
|
UserWorkSpacesEndpoint,
|
||||||
WorkSpaceAvailabilityCheckEndpoint,
|
WorkSpaceAvailabilityCheckEndpoint,
|
||||||
InviteWorkspaceEndpoint,
|
WorkspaceJoinEndpoint,
|
||||||
JoinWorkspaceEndpoint,
|
|
||||||
WorkSpaceMemberViewSet,
|
WorkSpaceMemberViewSet,
|
||||||
TeamMemberViewSet,
|
TeamMemberViewSet,
|
||||||
WorkspaceInvitationsViewset,
|
WorkspaceInvitationsViewset,
|
||||||
UserWorkspaceInvitationsEndpoint,
|
UserWorkspaceInvitationsViewSet,
|
||||||
UserWorkspaceInvitationEndpoint,
|
|
||||||
UserLastProjectWithWorkspaceEndpoint,
|
UserLastProjectWithWorkspaceEndpoint,
|
||||||
WorkspaceMemberUserEndpoint,
|
WorkspaceMemberUserEndpoint,
|
||||||
WorkspaceMemberUserViewsEndpoint,
|
WorkspaceMemberUserViewsEndpoint,
|
||||||
@ -51,7 +46,6 @@ from .workspace import (
|
|||||||
WorkspaceUserProfileEndpoint,
|
WorkspaceUserProfileEndpoint,
|
||||||
WorkspaceUserProfileIssuesEndpoint,
|
WorkspaceUserProfileIssuesEndpoint,
|
||||||
WorkspaceLabelsEndpoint,
|
WorkspaceLabelsEndpoint,
|
||||||
LeaveWorkspaceEndpoint,
|
|
||||||
)
|
)
|
||||||
from .state import StateViewSet
|
from .state import StateViewSet
|
||||||
from .view import (
|
from .view import (
|
||||||
@ -121,7 +115,7 @@ from .module import (
|
|||||||
ModuleFavoriteViewSet,
|
ModuleFavoriteViewSet,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .api_token import ApiTokenEndpoint
|
from .api import ApiTokenEndpoint
|
||||||
|
|
||||||
from .integration import (
|
from .integration import (
|
||||||
WorkspaceIntegrationViewSet,
|
WorkspaceIntegrationViewSet,
|
||||||
@ -144,9 +138,10 @@ from .importer import (
|
|||||||
|
|
||||||
from .page import (
|
from .page import (
|
||||||
PageViewSet,
|
PageViewSet,
|
||||||
PageBlockViewSet,
|
|
||||||
PageFavoriteViewSet,
|
PageFavoriteViewSet,
|
||||||
CreateIssueFromPageBlockEndpoint,
|
PageLogEndpoint,
|
||||||
|
SubPagesEndpoint,
|
||||||
|
CreateIssueFromBlockEndpoint,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .search import GlobalSearchEndpoint, IssueSearchEndpoint
|
from .search import GlobalSearchEndpoint, IssueSearchEndpoint
|
||||||
@ -178,3 +173,5 @@ from .notification import (
|
|||||||
from .exporter import ExportIssuesEndpoint
|
from .exporter import ExportIssuesEndpoint
|
||||||
|
|
||||||
from .config import ConfigurationEndpoint
|
from .config import ConfigurationEndpoint
|
||||||
|
|
||||||
|
from .webhook import WebhookEndpoint, WebhookLogsEndpoint, WebhookSecretRegenerateEndpoint
|
||||||
|
78
apiserver/plane/api/views/api.py
Normal file
78
apiserver/plane/api/views/api.py
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
# Python import
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
# Third party
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import status
|
||||||
|
|
||||||
|
# Module import
|
||||||
|
from .base import BaseAPIView
|
||||||
|
from plane.db.models import APIToken, Workspace
|
||||||
|
from plane.api.serializers import APITokenSerializer, APITokenReadSerializer
|
||||||
|
from plane.api.permissions import WorkspaceOwnerPermission
|
||||||
|
|
||||||
|
|
||||||
|
class ApiTokenEndpoint(BaseAPIView):
|
||||||
|
permission_classes = [
|
||||||
|
WorkspaceOwnerPermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
def post(self, request, slug):
|
||||||
|
label = request.data.get("label", str(uuid4().hex))
|
||||||
|
description = request.data.get("description", "")
|
||||||
|
workspace = Workspace.objects.get(slug=slug)
|
||||||
|
expired_at = request.data.get("expired_at", None)
|
||||||
|
|
||||||
|
# Check the user type
|
||||||
|
user_type = 1 if request.user.is_bot else 0
|
||||||
|
|
||||||
|
api_token = APIToken.objects.create(
|
||||||
|
label=label,
|
||||||
|
description=description,
|
||||||
|
user=request.user,
|
||||||
|
workspace=workspace,
|
||||||
|
user_type=user_type,
|
||||||
|
expired_at=expired_at,
|
||||||
|
)
|
||||||
|
|
||||||
|
serializer = APITokenSerializer(api_token)
|
||||||
|
# Token will be only visible while creating
|
||||||
|
return Response(
|
||||||
|
serializer.data,
|
||||||
|
status=status.HTTP_201_CREATED,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get(self, request, slug, pk=None):
|
||||||
|
if pk == None:
|
||||||
|
api_tokens = APIToken.objects.filter(
|
||||||
|
user=request.user, workspace__slug=slug
|
||||||
|
)
|
||||||
|
serializer = APITokenReadSerializer(api_tokens, many=True)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
else:
|
||||||
|
api_tokens = APIToken.objects.get(
|
||||||
|
user=request.user, workspace__slug=slug, pk=pk
|
||||||
|
)
|
||||||
|
serializer = APITokenReadSerializer(api_tokens)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
def delete(self, request, slug, pk):
|
||||||
|
api_token = APIToken.objects.get(
|
||||||
|
workspace__slug=slug,
|
||||||
|
user=request.user,
|
||||||
|
pk=pk,
|
||||||
|
)
|
||||||
|
api_token.delete()
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
def patch(self, request, slug, pk):
|
||||||
|
api_token = APIToken.objects.get(
|
||||||
|
workspace__slug=slug,
|
||||||
|
user=request.user,
|
||||||
|
pk=pk,
|
||||||
|
)
|
||||||
|
serializer = APITokenSerializer(api_token, data=request.data, partial=True)
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save()
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
@ -1,47 +0,0 @@
|
|||||||
# Python import
|
|
||||||
from uuid import uuid4
|
|
||||||
|
|
||||||
# Third party
|
|
||||||
from rest_framework.response import Response
|
|
||||||
from rest_framework import status
|
|
||||||
from sentry_sdk import capture_exception
|
|
||||||
|
|
||||||
# Module import
|
|
||||||
from .base import BaseAPIView
|
|
||||||
from plane.db.models import APIToken
|
|
||||||
from plane.api.serializers import APITokenSerializer
|
|
||||||
|
|
||||||
|
|
||||||
class ApiTokenEndpoint(BaseAPIView):
|
|
||||||
def post(self, request):
|
|
||||||
label = request.data.get("label", str(uuid4().hex))
|
|
||||||
workspace = request.data.get("workspace", False)
|
|
||||||
|
|
||||||
if not workspace:
|
|
||||||
return Response(
|
|
||||||
{"error": "Workspace is required"}, status=status.HTTP_200_OK
|
|
||||||
)
|
|
||||||
|
|
||||||
api_token = APIToken.objects.create(
|
|
||||||
label=label, user=request.user, workspace_id=workspace
|
|
||||||
)
|
|
||||||
|
|
||||||
serializer = APITokenSerializer(api_token)
|
|
||||||
# Token will be only vissible while creating
|
|
||||||
return Response(
|
|
||||||
{"api_token": serializer.data, "token": api_token.token},
|
|
||||||
status=status.HTTP_201_CREATED,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get(self, request):
|
|
||||||
api_tokens = APIToken.objects.filter(user=request.user)
|
|
||||||
serializer = APITokenSerializer(api_tokens, many=True)
|
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
|
||||||
|
|
||||||
|
|
||||||
def delete(self, request, pk):
|
|
||||||
api_token = APIToken.objects.get(pk=pk)
|
|
||||||
api_token.delete()
|
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
|
||||||
|
|
@ -33,7 +33,7 @@ from plane.bgtasks.forgot_password_task import forgot_password
|
|||||||
class RequestEmailVerificationEndpoint(BaseAPIView):
|
class RequestEmailVerificationEndpoint(BaseAPIView):
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
token = RefreshToken.for_user(request.user).access_token
|
token = RefreshToken.for_user(request.user).access_token
|
||||||
current_site = settings.WEB_URL
|
current_site = request.META.get('HTTP_ORIGIN')
|
||||||
email_verification.delay(
|
email_verification.delay(
|
||||||
request.user.first_name, request.user.email, token, current_site
|
request.user.first_name, request.user.email, token, current_site
|
||||||
)
|
)
|
||||||
@ -76,7 +76,7 @@ class ForgotPasswordEndpoint(BaseAPIView):
|
|||||||
uidb64 = urlsafe_base64_encode(smart_bytes(user.id))
|
uidb64 = urlsafe_base64_encode(smart_bytes(user.id))
|
||||||
token = PasswordResetTokenGenerator().make_token(user)
|
token = PasswordResetTokenGenerator().make_token(user)
|
||||||
|
|
||||||
current_site = settings.WEB_URL
|
current_site = request.META.get('HTTP_ORIGIN')
|
||||||
|
|
||||||
forgot_password.delay(
|
forgot_password.delay(
|
||||||
user.first_name, user.email, uidb64, token, current_site
|
user.first_name, user.email, uidb64, token, current_site
|
||||||
|
@ -4,7 +4,7 @@ import random
|
|||||||
import string
|
import string
|
||||||
import json
|
import json
|
||||||
import requests
|
import requests
|
||||||
|
from requests.exceptions import RequestException
|
||||||
# Django imports
|
# Django imports
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
@ -22,8 +22,13 @@ from sentry_sdk import capture_exception, capture_message
|
|||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from . import BaseAPIView
|
from . import BaseAPIView
|
||||||
from plane.db.models import User
|
from plane.db.models import (
|
||||||
from plane.api.serializers import UserSerializer
|
User,
|
||||||
|
WorkspaceMemberInvite,
|
||||||
|
WorkspaceMember,
|
||||||
|
ProjectMemberInvite,
|
||||||
|
ProjectMember,
|
||||||
|
)
|
||||||
from plane.settings.redis import redis_instance
|
from plane.settings.redis import redis_instance
|
||||||
from plane.bgtasks.magic_link_code_task import magic_link
|
from plane.bgtasks.magic_link_code_task import magic_link
|
||||||
|
|
||||||
@ -86,13 +91,63 @@ class SignUpEndpoint(BaseAPIView):
|
|||||||
user.token_updated_at = timezone.now()
|
user.token_updated_at = timezone.now()
|
||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
access_token, refresh_token = get_tokens_for_user(user)
|
# Check if user has any accepted invites for workspace and add them to workspace
|
||||||
|
workspace_member_invites = WorkspaceMemberInvite.objects.filter(
|
||||||
|
email=user.email, accepted=True
|
||||||
|
)
|
||||||
|
|
||||||
data = {
|
WorkspaceMember.objects.bulk_create(
|
||||||
"access_token": access_token,
|
[
|
||||||
"refresh_token": refresh_token,
|
WorkspaceMember(
|
||||||
}
|
workspace_id=workspace_member_invite.workspace_id,
|
||||||
|
member=user,
|
||||||
|
role=workspace_member_invite.role,
|
||||||
|
)
|
||||||
|
for workspace_member_invite in workspace_member_invites
|
||||||
|
],
|
||||||
|
ignore_conflicts=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if user has any project invites
|
||||||
|
project_member_invites = ProjectMemberInvite.objects.filter(
|
||||||
|
email=user.email, accepted=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add user to workspace
|
||||||
|
WorkspaceMember.objects.bulk_create(
|
||||||
|
[
|
||||||
|
WorkspaceMember(
|
||||||
|
workspace_id=project_member_invite.workspace_id,
|
||||||
|
role=project_member_invite.role
|
||||||
|
if project_member_invite.role in [5, 10, 15]
|
||||||
|
else 15,
|
||||||
|
member=user,
|
||||||
|
created_by_id=project_member_invite.created_by_id,
|
||||||
|
)
|
||||||
|
for project_member_invite in project_member_invites
|
||||||
|
],
|
||||||
|
ignore_conflicts=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Now add the users to project
|
||||||
|
ProjectMember.objects.bulk_create(
|
||||||
|
[
|
||||||
|
ProjectMember(
|
||||||
|
workspace_id=project_member_invite.workspace_id,
|
||||||
|
role=project_member_invite.role
|
||||||
|
if project_member_invite.role in [5, 10, 15]
|
||||||
|
else 15,
|
||||||
|
member=user,
|
||||||
|
created_by_id=project_member_invite.created_by_id,
|
||||||
|
) for project_member_invite in project_member_invites
|
||||||
|
],
|
||||||
|
ignore_conflicts=True,
|
||||||
|
)
|
||||||
|
# Delete all the invites
|
||||||
|
workspace_member_invites.delete()
|
||||||
|
project_member_invites.delete()
|
||||||
|
|
||||||
|
try:
|
||||||
# Send Analytics
|
# Send Analytics
|
||||||
if settings.ANALYTICS_BASE_API:
|
if settings.ANALYTICS_BASE_API:
|
||||||
_ = requests.post(
|
_ = requests.post(
|
||||||
@ -114,7 +169,15 @@ class SignUpEndpoint(BaseAPIView):
|
|||||||
"event_type": "SIGN_UP",
|
"event_type": "SIGN_UP",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
except RequestException as e:
|
||||||
|
capture_exception(e)
|
||||||
|
|
||||||
|
access_token, refresh_token = get_tokens_for_user(user)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"access_token": access_token,
|
||||||
|
"refresh_token": refresh_token,
|
||||||
|
}
|
||||||
return Response(data, status=status.HTTP_200_OK)
|
return Response(data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
@ -176,7 +239,63 @@ class SignInEndpoint(BaseAPIView):
|
|||||||
user.token_updated_at = timezone.now()
|
user.token_updated_at = timezone.now()
|
||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
access_token, refresh_token = get_tokens_for_user(user)
|
# Check if user has any accepted invites for workspace and add them to workspace
|
||||||
|
workspace_member_invites = WorkspaceMemberInvite.objects.filter(
|
||||||
|
email=user.email, accepted=True
|
||||||
|
)
|
||||||
|
|
||||||
|
WorkspaceMember.objects.bulk_create(
|
||||||
|
[
|
||||||
|
WorkspaceMember(
|
||||||
|
workspace_id=workspace_member_invite.workspace_id,
|
||||||
|
member=user,
|
||||||
|
role=workspace_member_invite.role,
|
||||||
|
)
|
||||||
|
for workspace_member_invite in workspace_member_invites
|
||||||
|
],
|
||||||
|
ignore_conflicts=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if user has any project invites
|
||||||
|
project_member_invites = ProjectMemberInvite.objects.filter(
|
||||||
|
email=user.email, accepted=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add user to workspace
|
||||||
|
WorkspaceMember.objects.bulk_create(
|
||||||
|
[
|
||||||
|
WorkspaceMember(
|
||||||
|
workspace_id=project_member_invite.workspace_id,
|
||||||
|
role=project_member_invite.role
|
||||||
|
if project_member_invite.role in [5, 10, 15]
|
||||||
|
else 15,
|
||||||
|
member=user,
|
||||||
|
created_by_id=project_member_invite.created_by_id,
|
||||||
|
)
|
||||||
|
for project_member_invite in project_member_invites
|
||||||
|
],
|
||||||
|
ignore_conflicts=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Now add the users to project
|
||||||
|
ProjectMember.objects.bulk_create(
|
||||||
|
[
|
||||||
|
ProjectMember(
|
||||||
|
workspace_id=project_member_invite.workspace_id,
|
||||||
|
role=project_member_invite.role
|
||||||
|
if project_member_invite.role in [5, 10, 15]
|
||||||
|
else 15,
|
||||||
|
member=user,
|
||||||
|
created_by_id=project_member_invite.created_by_id,
|
||||||
|
) for project_member_invite in project_member_invites
|
||||||
|
],
|
||||||
|
ignore_conflicts=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Delete all the invites
|
||||||
|
workspace_member_invites.delete()
|
||||||
|
project_member_invites.delete()
|
||||||
|
try:
|
||||||
# Send Analytics
|
# Send Analytics
|
||||||
if settings.ANALYTICS_BASE_API:
|
if settings.ANALYTICS_BASE_API:
|
||||||
_ = requests.post(
|
_ = requests.post(
|
||||||
@ -198,11 +317,14 @@ class SignInEndpoint(BaseAPIView):
|
|||||||
"event_type": "SIGN_IN",
|
"event_type": "SIGN_IN",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
except RequestException as e:
|
||||||
|
capture_exception(e)
|
||||||
|
|
||||||
|
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,
|
||||||
}
|
}
|
||||||
|
|
||||||
return Response(data, status=status.HTTP_200_OK)
|
return Response(data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
@ -287,7 +409,8 @@ class MagicSignInGenerateEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
ri.set(key, json.dumps(value), ex=expiry)
|
ri.set(key, json.dumps(value), ex=expiry)
|
||||||
|
|
||||||
current_site = settings.WEB_URL
|
|
||||||
|
current_site = request.META.get('HTTP_ORIGIN')
|
||||||
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)
|
||||||
@ -319,6 +442,14 @@ class MagicSignInEndpoint(BaseAPIView):
|
|||||||
if str(token) == str(user_token):
|
if str(token) == str(user_token):
|
||||||
if User.objects.filter(email=email).exists():
|
if User.objects.filter(email=email).exists():
|
||||||
user = User.objects.get(email=email)
|
user = User.objects.get(email=email)
|
||||||
|
if not user.is_active:
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"error": "Your account has been deactivated. Please contact your site administrator."
|
||||||
|
},
|
||||||
|
status=status.HTTP_403_FORBIDDEN,
|
||||||
|
)
|
||||||
|
try:
|
||||||
# Send event to Jitsu for tracking
|
# Send event to Jitsu for tracking
|
||||||
if settings.ANALYTICS_BASE_API:
|
if settings.ANALYTICS_BASE_API:
|
||||||
_ = requests.post(
|
_ = requests.post(
|
||||||
@ -340,6 +471,8 @@ class MagicSignInEndpoint(BaseAPIView):
|
|||||||
"event_type": "SIGN_IN",
|
"event_type": "SIGN_IN",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
except RequestException as e:
|
||||||
|
capture_exception(e)
|
||||||
else:
|
else:
|
||||||
user = User.objects.create(
|
user = User.objects.create(
|
||||||
email=email,
|
email=email,
|
||||||
@ -347,6 +480,7 @@ class MagicSignInEndpoint(BaseAPIView):
|
|||||||
password=make_password(uuid.uuid4().hex),
|
password=make_password(uuid.uuid4().hex),
|
||||||
is_password_autoset=True,
|
is_password_autoset=True,
|
||||||
)
|
)
|
||||||
|
try:
|
||||||
# Send event to Jitsu for tracking
|
# Send event to Jitsu for tracking
|
||||||
if settings.ANALYTICS_BASE_API:
|
if settings.ANALYTICS_BASE_API:
|
||||||
_ = requests.post(
|
_ = requests.post(
|
||||||
@ -368,6 +502,8 @@ class MagicSignInEndpoint(BaseAPIView):
|
|||||||
"event_type": "SIGN_UP",
|
"event_type": "SIGN_UP",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
except RequestException as e:
|
||||||
|
capture_exception(e)
|
||||||
|
|
||||||
user.last_active = timezone.now()
|
user.last_active = timezone.now()
|
||||||
user.last_login_time = timezone.now()
|
user.last_login_time = timezone.now()
|
||||||
@ -376,6 +512,63 @@ class MagicSignInEndpoint(BaseAPIView):
|
|||||||
user.token_updated_at = timezone.now()
|
user.token_updated_at = timezone.now()
|
||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
|
# Check if user has any accepted invites for workspace and add them to workspace
|
||||||
|
workspace_member_invites = WorkspaceMemberInvite.objects.filter(
|
||||||
|
email=user.email, accepted=True
|
||||||
|
)
|
||||||
|
|
||||||
|
WorkspaceMember.objects.bulk_create(
|
||||||
|
[
|
||||||
|
WorkspaceMember(
|
||||||
|
workspace_id=workspace_member_invite.workspace_id,
|
||||||
|
member=user,
|
||||||
|
role=workspace_member_invite.role,
|
||||||
|
)
|
||||||
|
for workspace_member_invite in workspace_member_invites
|
||||||
|
],
|
||||||
|
ignore_conflicts=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if user has any project invites
|
||||||
|
project_member_invites = ProjectMemberInvite.objects.filter(
|
||||||
|
email=user.email, accepted=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add user to workspace
|
||||||
|
WorkspaceMember.objects.bulk_create(
|
||||||
|
[
|
||||||
|
WorkspaceMember(
|
||||||
|
workspace_id=project_member_invite.workspace_id,
|
||||||
|
role=project_member_invite.role
|
||||||
|
if project_member_invite.role in [5, 10, 15]
|
||||||
|
else 15,
|
||||||
|
member=user,
|
||||||
|
created_by_id=project_member_invite.created_by_id,
|
||||||
|
)
|
||||||
|
for project_member_invite in project_member_invites
|
||||||
|
],
|
||||||
|
ignore_conflicts=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Now add the users to project
|
||||||
|
ProjectMember.objects.bulk_create(
|
||||||
|
[
|
||||||
|
ProjectMember(
|
||||||
|
workspace_id=project_member_invite.workspace_id,
|
||||||
|
role=project_member_invite.role
|
||||||
|
if project_member_invite.role in [5, 10, 15]
|
||||||
|
else 15,
|
||||||
|
member=user,
|
||||||
|
created_by_id=project_member_invite.created_by_id,
|
||||||
|
) for project_member_invite in project_member_invites
|
||||||
|
],
|
||||||
|
ignore_conflicts=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Delete all the invites
|
||||||
|
workspace_member_invites.delete()
|
||||||
|
project_member_invites.delete()
|
||||||
|
|
||||||
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,
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
# Python imports
|
# Python imports
|
||||||
import zoneinfo
|
import zoneinfo
|
||||||
|
import json
|
||||||
|
|
||||||
# Django imports
|
# Django imports
|
||||||
from django.urls import resolve
|
from django.urls import resolve
|
||||||
@ -7,6 +8,7 @@ from django.conf import settings
|
|||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.db import IntegrityError
|
from django.db import IntegrityError
|
||||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||||
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
|
|
||||||
# Third part imports
|
# Third part imports
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
@ -22,6 +24,7 @@ from django_filters.rest_framework import DjangoFilterBackend
|
|||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from plane.utils.paginator import BasePaginator
|
from plane.utils.paginator import BasePaginator
|
||||||
|
from plane.bgtasks.webhook_task import send_webhook
|
||||||
|
|
||||||
|
|
||||||
class TimezoneMixin:
|
class TimezoneMixin:
|
||||||
@ -29,6 +32,7 @@ class TimezoneMixin:
|
|||||||
This enables timezone conversion according
|
This enables timezone conversion according
|
||||||
to the user set timezone
|
to the user set timezone
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def initial(self, request, *args, **kwargs):
|
def initial(self, request, *args, **kwargs):
|
||||||
super().initial(request, *args, **kwargs)
|
super().initial(request, *args, **kwargs)
|
||||||
if request.user.is_authenticated:
|
if request.user.is_authenticated:
|
||||||
@ -37,8 +41,28 @@ class TimezoneMixin:
|
|||||||
timezone.deactivate()
|
timezone.deactivate()
|
||||||
|
|
||||||
|
|
||||||
class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
|
class WebhookMixin:
|
||||||
|
webhook_event = None
|
||||||
|
|
||||||
|
def finalize_response(self, request, response, *args, **kwargs):
|
||||||
|
response = super().finalize_response(request, response, *args, **kwargs)
|
||||||
|
|
||||||
|
if (
|
||||||
|
self.webhook_event
|
||||||
|
and self.request.method in ["POST", "PATCH", "DELETE"]
|
||||||
|
and response.status_code in [200, 201, 204]
|
||||||
|
):
|
||||||
|
send_webhook.delay(
|
||||||
|
event=self.webhook_event,
|
||||||
|
event_data=json.dumps(response.data, cls=DjangoJSONEncoder),
|
||||||
|
action=self.request.method,
|
||||||
|
slug=self.workspace_slug,
|
||||||
|
)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
|
||||||
model = None
|
model = None
|
||||||
|
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
@ -71,18 +95,30 @@ class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
|
|||||||
return response
|
return response
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if isinstance(e, IntegrityError):
|
if isinstance(e, IntegrityError):
|
||||||
return Response({"error": "The payload is not valid"}, status=status.HTTP_400_BAD_REQUEST)
|
return Response(
|
||||||
|
{"error": "The payload is not valid"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
if isinstance(e, ValidationError):
|
if isinstance(e, ValidationError):
|
||||||
return Response({"error": "Please provide valid detail"}, status=status.HTTP_400_BAD_REQUEST)
|
return Response(
|
||||||
|
{"error": "Please provide valid detail"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
if isinstance(e, ObjectDoesNotExist):
|
if isinstance(e, ObjectDoesNotExist):
|
||||||
model_name = str(exc).split(" matching query does not exist.")[0]
|
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)
|
return Response(
|
||||||
|
{"error": f"{model_name} does not exist."},
|
||||||
|
status=status.HTTP_404_NOT_FOUND,
|
||||||
|
)
|
||||||
|
|
||||||
if isinstance(e, KeyError):
|
if isinstance(e, KeyError):
|
||||||
capture_exception(e)
|
capture_exception(e)
|
||||||
return Response({"error": f"key {e} does not exist"}, status=status.HTTP_400_BAD_REQUEST)
|
return Response(
|
||||||
|
{"error": f"key {e} does not exist"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
print(e) if settings.DEBUG else print("Server Error")
|
print(e) if settings.DEBUG else print("Server Error")
|
||||||
capture_exception(e)
|
capture_exception(e)
|
||||||
@ -99,8 +135,8 @@ class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
|
|||||||
print(
|
print(
|
||||||
f"{request.method} - {request.get_full_path()} of Queries: {len(connection.queries)}"
|
f"{request.method} - {request.get_full_path()} of Queries: {len(connection.queries)}"
|
||||||
)
|
)
|
||||||
return response
|
|
||||||
|
|
||||||
|
return response
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
response = self.handle_exception(exc)
|
response = self.handle_exception(exc)
|
||||||
return exc
|
return exc
|
||||||
@ -120,7 +156,6 @@ class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
|
|||||||
|
|
||||||
|
|
||||||
class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
|
class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
|
||||||
|
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
IsAuthenticated,
|
IsAuthenticated,
|
||||||
]
|
]
|
||||||
@ -139,7 +174,6 @@ 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):
|
def handle_exception(self, exc):
|
||||||
"""
|
"""
|
||||||
Handle any exception that occurs, by returning an appropriate response,
|
Handle any exception that occurs, by returning an appropriate response,
|
||||||
@ -150,19 +184,29 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
|
|||||||
return response
|
return response
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if isinstance(e, IntegrityError):
|
if isinstance(e, IntegrityError):
|
||||||
return Response({"error": "The payload is not valid"}, status=status.HTTP_400_BAD_REQUEST)
|
return Response(
|
||||||
|
{"error": "The payload is not valid"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
if isinstance(e, ValidationError):
|
if isinstance(e, ValidationError):
|
||||||
return Response({"error": "Please provide valid detail"}, status=status.HTTP_400_BAD_REQUEST)
|
return Response(
|
||||||
|
{"error": "Please provide valid detail"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
if isinstance(e, ObjectDoesNotExist):
|
if isinstance(e, ObjectDoesNotExist):
|
||||||
model_name = str(exc).split(" matching query does not exist.")[0]
|
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)
|
return Response(
|
||||||
|
{"error": f"{model_name} does not exist."},
|
||||||
|
status=status.HTTP_404_NOT_FOUND,
|
||||||
|
)
|
||||||
|
|
||||||
if isinstance(e, KeyError):
|
if isinstance(e, KeyError):
|
||||||
return Response({"error": f"key {e} does not exist"}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({"error": f"key {e} does not exist"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
print(e) if settings.DEBUG else print("Server Error")
|
if settings.DEBUG:
|
||||||
|
print(e)
|
||||||
capture_exception(e)
|
capture_exception(e)
|
||||||
return Response({"error": "Something went wrong please try again later"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
return Response({"error": "Something went wrong please try again later"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
@ -12,6 +12,8 @@ from sentry_sdk import capture_exception
|
|||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from .base import BaseAPIView
|
from .base import BaseAPIView
|
||||||
|
from plane.license.models import Instance, InstanceConfiguration
|
||||||
|
from plane.license.utils.instance_value import get_configuration_value
|
||||||
|
|
||||||
|
|
||||||
class ConfigurationEndpoint(BaseAPIView):
|
class ConfigurationEndpoint(BaseAPIView):
|
||||||
@ -20,18 +22,75 @@ class ConfigurationEndpoint(BaseAPIView):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
|
instance_configuration = InstanceConfiguration.objects.values("key", "value")
|
||||||
|
|
||||||
data = {}
|
data = {}
|
||||||
data["google_client_id"] = os.environ.get("GOOGLE_CLIENT_ID", None)
|
# Authentication
|
||||||
data["github_client_id"] = os.environ.get("GITHUB_CLIENT_ID", None)
|
data["google_client_id"] = get_configuration_value(
|
||||||
data["github_app_name"] = os.environ.get("GITHUB_APP_NAME", None)
|
instance_configuration,
|
||||||
data["magic_login"] = (
|
"GOOGLE_CLIENT_ID",
|
||||||
bool(settings.EMAIL_HOST_USER) and bool(settings.EMAIL_HOST_PASSWORD)
|
os.environ.get("GOOGLE_CLIENT_ID", None),
|
||||||
) and os.environ.get("ENABLE_MAGIC_LINK_LOGIN", "0") == "1"
|
|
||||||
data["email_password_login"] = (
|
|
||||||
os.environ.get("ENABLE_EMAIL_PASSWORD", "0") == "1"
|
|
||||||
)
|
)
|
||||||
data["slack_client_id"] = os.environ.get("SLACK_CLIENT_ID", None)
|
data["github_client_id"] = get_configuration_value(
|
||||||
data["posthog_api_key"] = os.environ.get("POSTHOG_API_KEY", None)
|
instance_configuration,
|
||||||
data["posthog_host"] = os.environ.get("POSTHOG_HOST", None)
|
"GITHUB_CLIENT_ID",
|
||||||
data["has_unsplash_configured"] = bool(settings.UNSPLASH_ACCESS_KEY)
|
os.environ.get("GITHUB_CLIENT_ID", None),
|
||||||
|
)
|
||||||
|
data["github_app_name"] = get_configuration_value(
|
||||||
|
instance_configuration,
|
||||||
|
"GITHUB_APP_NAME",
|
||||||
|
os.environ.get("GITHUB_APP_NAME", None),
|
||||||
|
)
|
||||||
|
data["magic_login"] = (
|
||||||
|
bool(
|
||||||
|
get_configuration_value(
|
||||||
|
instance_configuration,
|
||||||
|
"EMAIL_HOST_USER",
|
||||||
|
os.environ.get("GITHUB_APP_NAME", None),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
and bool(
|
||||||
|
get_configuration_value(
|
||||||
|
instance_configuration,
|
||||||
|
"EMAIL_HOST_PASSWORD",
|
||||||
|
os.environ.get("GITHUB_APP_NAME", None),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) and get_configuration_value(
|
||||||
|
instance_configuration, "ENABLE_MAGIC_LINK_LOGIN", "0"
|
||||||
|
) == "1"
|
||||||
|
data["email_password_login"] = (
|
||||||
|
get_configuration_value(
|
||||||
|
instance_configuration, "ENABLE_EMAIL_PASSWORD", "0"
|
||||||
|
)
|
||||||
|
== "1"
|
||||||
|
)
|
||||||
|
# Slack client
|
||||||
|
data["slack_client_id"] = get_configuration_value(
|
||||||
|
instance_configuration,
|
||||||
|
"SLACK_CLIENT_ID",
|
||||||
|
os.environ.get("SLACK_CLIENT_ID", None),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Posthog
|
||||||
|
data["posthog_api_key"] = get_configuration_value(
|
||||||
|
instance_configuration,
|
||||||
|
"POSTHOG_API_KEY",
|
||||||
|
os.environ.get("POSTHOG_API_KEY", None),
|
||||||
|
)
|
||||||
|
data["posthog_host"] = get_configuration_value(
|
||||||
|
instance_configuration,
|
||||||
|
"POSTHOG_HOST",
|
||||||
|
os.environ.get("POSTHOG_HOST", None),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Unsplash
|
||||||
|
data["has_unsplash_configured"] = bool(
|
||||||
|
get_configuration_value(
|
||||||
|
instance_configuration,
|
||||||
|
"UNSPLASH_ACCESS_KEY",
|
||||||
|
os.environ.get("UNSPLASH_ACCESS_KEY", None),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
return Response(data, status=status.HTTP_200_OK)
|
return Response(data, status=status.HTTP_200_OK)
|
||||||
|
@ -23,7 +23,7 @@ from rest_framework import status
|
|||||||
from sentry_sdk import capture_exception
|
from sentry_sdk import capture_exception
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from . import BaseViewSet, BaseAPIView
|
from . import BaseViewSet, BaseAPIView, WebhookMixin
|
||||||
from plane.api.serializers import (
|
from plane.api.serializers import (
|
||||||
CycleSerializer,
|
CycleSerializer,
|
||||||
CycleIssueSerializer,
|
CycleIssueSerializer,
|
||||||
@ -48,9 +48,10 @@ 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 CycleViewSet(BaseViewSet):
|
class CycleViewSet(WebhookMixin, BaseViewSet):
|
||||||
serializer_class = CycleSerializer
|
serializer_class = CycleSerializer
|
||||||
model = Cycle
|
model = Cycle
|
||||||
|
webhook_event = "cycle"
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
ProjectEntityPermission,
|
ProjectEntityPermission,
|
||||||
]
|
]
|
||||||
@ -499,10 +500,10 @@ class CycleViewSet(BaseViewSet):
|
|||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
class CycleIssueViewSet(BaseViewSet):
|
class CycleIssueViewSet(WebhookMixin, BaseViewSet):
|
||||||
serializer_class = CycleIssueSerializer
|
serializer_class = CycleIssueSerializer
|
||||||
model = CycleIssue
|
model = CycleIssue
|
||||||
|
webhook_event = "cycle"
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
ProjectEntityPermission,
|
ProjectEntityPermission,
|
||||||
]
|
]
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
import requests
|
import requests
|
||||||
|
|
||||||
# Third party imports
|
# Third party imports
|
||||||
import openai
|
from openai import OpenAI
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.permissions import AllowAny
|
from rest_framework.permissions import AllowAny
|
||||||
@ -17,7 +17,8 @@ from plane.api.permissions import ProjectEntityPermission
|
|||||||
from plane.db.models import Workspace, Project
|
from plane.db.models import Workspace, Project
|
||||||
from plane.api.serializers import ProjectLiteSerializer, WorkspaceLiteSerializer
|
from plane.api.serializers import ProjectLiteSerializer, WorkspaceLiteSerializer
|
||||||
from plane.utils.integrations.github import get_release_notes
|
from plane.utils.integrations.github import get_release_notes
|
||||||
|
from plane.license.models import InstanceConfiguration
|
||||||
|
from plane.license.utils.instance_value import get_configuration_value
|
||||||
|
|
||||||
class GPTIntegrationEndpoint(BaseAPIView):
|
class GPTIntegrationEndpoint(BaseAPIView):
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
@ -25,7 +26,14 @@ class GPTIntegrationEndpoint(BaseAPIView):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def post(self, request, slug, project_id):
|
def post(self, request, slug, project_id):
|
||||||
if not settings.OPENAI_API_KEY or not settings.GPT_ENGINE:
|
|
||||||
|
# Get the configuration value
|
||||||
|
instance_configuration = InstanceConfiguration.objects.values("key", "value")
|
||||||
|
api_key = get_configuration_value(instance_configuration, "OPENAI_API_KEY")
|
||||||
|
gpt_engine = get_configuration_value(instance_configuration, "GPT_ENGINE")
|
||||||
|
|
||||||
|
# Check the keys
|
||||||
|
if not api_key or not gpt_engine:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "OpenAI API key and engine is required"},
|
{"error": "OpenAI API key and engine is required"},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
@ -41,12 +49,17 @@ class GPTIntegrationEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
final_text = task + "\n" + prompt
|
final_text = task + "\n" + prompt
|
||||||
|
|
||||||
openai.api_key = settings.OPENAI_API_KEY
|
instance_configuration = InstanceConfiguration.objects.values("key", "value")
|
||||||
response = openai.ChatCompletion.create(
|
|
||||||
model=settings.GPT_ENGINE,
|
gpt_engine = get_configuration_value(instance_configuration, "GPT_ENGINE")
|
||||||
|
|
||||||
|
client = OpenAI(
|
||||||
|
api_key=api_key,
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.chat.completions.create(
|
||||||
|
model=gpt_engine,
|
||||||
messages=[{"role": "user", "content": final_text}],
|
messages=[{"role": "user", "content": final_text}],
|
||||||
temperature=0.7,
|
|
||||||
max_tokens=1024,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
workspace = Workspace.objects.get(slug=slug)
|
workspace = Workspace.objects.get(slug=slug)
|
||||||
|
@ -64,9 +64,7 @@ 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):
|
||||||
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
|
|
||||||
)
|
|
||||||
# Handle default inbox delete
|
# Handle default inbox delete
|
||||||
if inbox.is_default:
|
if inbox.is_default:
|
||||||
return Response(
|
return Response(
|
||||||
@ -128,9 +126,7 @@ class InboxIssueViewSet(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")
|
||||||
@ -150,7 +146,6 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
status=status.HTTP_200_OK,
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def create(self, request, slug, project_id, inbox_id):
|
def create(self, request, slug, project_id, inbox_id):
|
||||||
if not request.data.get("issue", {}).get("name", False):
|
if not request.data.get("issue", {}).get("name", False):
|
||||||
return Response(
|
return Response(
|
||||||
@ -198,7 +193,7 @@ class InboxIssueViewSet(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()),
|
||||||
)
|
)
|
||||||
# create an inbox issue
|
# create an inbox issue
|
||||||
InboxIssue.objects.create(
|
InboxIssue.objects.create(
|
||||||
@ -216,10 +211,20 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
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
|
||||||
)
|
)
|
||||||
# Get the project member
|
# Get the project member
|
||||||
project_member = ProjectMember.objects.get(workspace__slug=slug, project_id=project_id, member=request.user)
|
project_member = ProjectMember.objects.get(
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
member=request.user,
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
# Only project members admins and created_by users can access this endpoint
|
# Only project members admins and created_by users can access this endpoint
|
||||||
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(
|
||||||
return Response({"error": "You cannot edit inbox issues"}, status=status.HTTP_400_BAD_REQUEST)
|
request.user.id
|
||||||
|
):
|
||||||
|
return Response(
|
||||||
|
{"error": "You cannot edit inbox issues"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
# Get issue data
|
# Get issue data
|
||||||
issue_data = request.data.pop("issue", False)
|
issue_data = request.data.pop("issue", False)
|
||||||
@ -233,8 +238,10 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
# viewers and guests since only viewers and guests
|
# viewers and guests since only viewers and guests
|
||||||
issue_data = {
|
issue_data = {
|
||||||
"name": issue_data.get("name", issue.name),
|
"name": issue_data.get("name", issue.name),
|
||||||
"description_html": issue_data.get("description_html", issue.description_html),
|
"description_html": issue_data.get(
|
||||||
"description": issue_data.get("description", issue.description)
|
"description_html", issue.description_html
|
||||||
|
),
|
||||||
|
"description": issue_data.get("description", issue.description),
|
||||||
}
|
}
|
||||||
|
|
||||||
issue_serializer = IssueCreateSerializer(
|
issue_serializer = IssueCreateSerializer(
|
||||||
@ -256,7 +263,7 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
IssueSerializer(current_instance).data,
|
IssueSerializer(current_instance).data,
|
||||||
cls=DjangoJSONEncoder,
|
cls=DjangoJSONEncoder,
|
||||||
),
|
),
|
||||||
epoch=int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp()),
|
||||||
)
|
)
|
||||||
issue_serializer.save()
|
issue_serializer.save()
|
||||||
else:
|
else:
|
||||||
@ -307,7 +314,9 @@ class InboxIssueViewSet(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)
|
||||||
else:
|
else:
|
||||||
return Response(InboxIssueSerializer(inbox_issue).data, status=status.HTTP_200_OK)
|
return Response(
|
||||||
|
InboxIssueSerializer(inbox_issue).data, status=status.HTTP_200_OK
|
||||||
|
)
|
||||||
|
|
||||||
def retrieve(self, request, slug, project_id, inbox_id, pk):
|
def retrieve(self, request, slug, project_id, inbox_id, pk):
|
||||||
inbox_issue = InboxIssue.objects.get(
|
inbox_issue = InboxIssue.objects.get(
|
||||||
@ -324,15 +333,27 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
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
|
||||||
)
|
)
|
||||||
# Get the project member
|
# Get the project member
|
||||||
project_member = ProjectMember.objects.get(workspace__slug=slug, project_id=project_id, member=request.user)
|
project_member = ProjectMember.objects.get(
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
member=request.user,
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
|
||||||
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(
|
||||||
return Response({"error": "You cannot delete inbox issue"}, status=status.HTTP_400_BAD_REQUEST)
|
request.user.id
|
||||||
|
):
|
||||||
|
return Response(
|
||||||
|
{"error": "You cannot delete inbox issue"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
# Check the issue status
|
# Check the issue status
|
||||||
if inbox_issue.status in [-2, -1, 0, 2]:
|
if inbox_issue.status in [-2, -1, 0, 2]:
|
||||||
# Delete the issue also
|
# Delete the issue also
|
||||||
Issue.objects.filter(workspace__slug=slug, project_id=project_id, pk=inbox_issue.issue_id).delete()
|
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)
|
||||||
@ -347,7 +368,10 @@ class InboxIssuePublicViewSet(BaseViewSet):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=self.kwargs.get("slug"), project_id=self.kwargs.get("project_id"))
|
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||||
|
workspace__slug=self.kwargs.get("slug"),
|
||||||
|
project_id=self.kwargs.get("project_id"),
|
||||||
|
)
|
||||||
if project_deploy_board is not None:
|
if project_deploy_board is not None:
|
||||||
return self.filter_queryset(
|
return self.filter_queryset(
|
||||||
super()
|
super()
|
||||||
@ -363,9 +387,14 @@ class InboxIssuePublicViewSet(BaseViewSet):
|
|||||||
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):
|
||||||
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,
|
||||||
|
)
|
||||||
|
|
||||||
filters = issue_filters(request.query_params, "GET")
|
filters = issue_filters(request.query_params, "GET")
|
||||||
issues = (
|
issues = (
|
||||||
@ -392,9 +421,7 @@ class InboxIssuePublicViewSet(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")
|
||||||
@ -415,9 +442,14 @@ class InboxIssuePublicViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def create(self, request, slug, project_id, inbox_id):
|
def create(self, request, slug, project_id, inbox_id):
|
||||||
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,
|
||||||
|
)
|
||||||
|
|
||||||
if not request.data.get("issue", {}).get("name", False):
|
if not request.data.get("issue", {}).get("name", False):
|
||||||
return Response(
|
return Response(
|
||||||
@ -465,7 +497,7 @@ class InboxIssuePublicViewSet(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()),
|
||||||
)
|
)
|
||||||
# create an inbox issue
|
# create an inbox issue
|
||||||
InboxIssue.objects.create(
|
InboxIssue.objects.create(
|
||||||
@ -479,34 +511,41 @@ class InboxIssuePublicViewSet(BaseViewSet):
|
|||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def partial_update(self, request, slug, project_id, inbox_id, pk):
|
def partial_update(self, request, slug, project_id, inbox_id, pk):
|
||||||
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,
|
||||||
|
)
|
||||||
|
|
||||||
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
|
||||||
)
|
)
|
||||||
# Get the project member
|
# Get the project member
|
||||||
if str(inbox_issue.created_by_id) != str(request.user.id):
|
if str(inbox_issue.created_by_id) != str(request.user.id):
|
||||||
return Response({"error": "You cannot edit inbox issues"}, status=status.HTTP_400_BAD_REQUEST)
|
return Response(
|
||||||
|
{"error": "You cannot edit inbox issues"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
# Get issue data
|
# Get issue data
|
||||||
issue_data = request.data.pop("issue", False)
|
issue_data = request.data.pop("issue", False)
|
||||||
|
|
||||||
|
|
||||||
issue = Issue.objects.get(
|
issue = Issue.objects.get(
|
||||||
pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id
|
pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id
|
||||||
)
|
)
|
||||||
# viewers and guests since only viewers and guests
|
# viewers and guests since only viewers and guests
|
||||||
issue_data = {
|
issue_data = {
|
||||||
"name": issue_data.get("name", issue.name),
|
"name": issue_data.get("name", issue.name),
|
||||||
"description_html": issue_data.get("description_html", issue.description_html),
|
"description_html": issue_data.get(
|
||||||
"description": issue_data.get("description", issue.description)
|
"description_html", issue.description_html
|
||||||
|
),
|
||||||
|
"description": issue_data.get("description", issue.description),
|
||||||
}
|
}
|
||||||
|
|
||||||
issue_serializer = IssueCreateSerializer(
|
issue_serializer = IssueCreateSerializer(issue, data=issue_data, partial=True)
|
||||||
issue, data=issue_data, partial=True
|
|
||||||
)
|
|
||||||
|
|
||||||
if issue_serializer.is_valid():
|
if issue_serializer.is_valid():
|
||||||
current_instance = issue
|
current_instance = issue
|
||||||
@ -523,16 +562,21 @@ class InboxIssuePublicViewSet(BaseViewSet):
|
|||||||
IssueSerializer(current_instance).data,
|
IssueSerializer(current_instance).data,
|
||||||
cls=DjangoJSONEncoder,
|
cls=DjangoJSONEncoder,
|
||||||
),
|
),
|
||||||
epoch=int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp()),
|
||||||
)
|
)
|
||||||
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)
|
||||||
|
|
||||||
def retrieve(self, request, slug, project_id, inbox_id, pk):
|
def retrieve(self, request, slug, project_id, inbox_id, pk):
|
||||||
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,
|
||||||
|
)
|
||||||
|
|
||||||
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
|
||||||
@ -544,16 +588,24 @@ class InboxIssuePublicViewSet(BaseViewSet):
|
|||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def destroy(self, request, slug, project_id, inbox_id, pk):
|
def destroy(self, request, slug, project_id, inbox_id, pk):
|
||||||
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,
|
||||||
|
)
|
||||||
|
|
||||||
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
|
||||||
)
|
)
|
||||||
|
|
||||||
if str(inbox_issue.created_by_id) != str(request.user.id):
|
if 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,
|
||||||
|
)
|
||||||
|
|
||||||
inbox_issue.delete()
|
inbox_issue.delete()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
@ -33,7 +33,7 @@ from rest_framework.permissions import AllowAny, IsAuthenticated
|
|||||||
from sentry_sdk import capture_exception
|
from sentry_sdk import capture_exception
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from . import BaseViewSet, BaseAPIView
|
from . import BaseViewSet, BaseAPIView, WebhookMixin
|
||||||
from plane.api.serializers import (
|
from plane.api.serializers import (
|
||||||
IssueCreateSerializer,
|
IssueCreateSerializer,
|
||||||
IssueActivitySerializer,
|
IssueActivitySerializer,
|
||||||
@ -84,7 +84,7 @@ from plane.utils.grouper import group_results
|
|||||||
from plane.utils.issue_filters import issue_filters
|
from plane.utils.issue_filters import issue_filters
|
||||||
|
|
||||||
|
|
||||||
class IssueViewSet(BaseViewSet):
|
class IssueViewSet(WebhookMixin, BaseViewSet):
|
||||||
def get_serializer_class(self):
|
def get_serializer_class(self):
|
||||||
return (
|
return (
|
||||||
IssueCreateSerializer
|
IssueCreateSerializer
|
||||||
@ -93,6 +93,7 @@ class IssueViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
model = Issue
|
model = Issue
|
||||||
|
webhook_event = "issue"
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
ProjectEntityPermission,
|
ProjectEntityPermission,
|
||||||
]
|
]
|
||||||
@ -594,9 +595,10 @@ class IssueActivityEndpoint(BaseAPIView):
|
|||||||
return Response(result_list, status=status.HTTP_200_OK)
|
return Response(result_list, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
class IssueCommentViewSet(BaseViewSet):
|
class IssueCommentViewSet(WebhookMixin, BaseViewSet):
|
||||||
serializer_class = IssueCommentSerializer
|
serializer_class = IssueCommentSerializer
|
||||||
model = IssueComment
|
model = IssueComment
|
||||||
|
webhook_event = "issue-comment"
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
ProjectLitePermission,
|
ProjectLitePermission,
|
||||||
]
|
]
|
||||||
@ -623,6 +625,7 @@ class IssueCommentViewSet(BaseViewSet):
|
|||||||
workspace__slug=self.kwargs.get("slug"),
|
workspace__slug=self.kwargs.get("slug"),
|
||||||
project_id=self.kwargs.get("project_id"),
|
project_id=self.kwargs.get("project_id"),
|
||||||
member_id=self.request.user.id,
|
member_id=self.request.user.id,
|
||||||
|
is_active=True,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -753,8 +756,8 @@ class LabelViewSet(BaseViewSet):
|
|||||||
.select_related("project")
|
.select_related("project")
|
||||||
.select_related("workspace")
|
.select_related("workspace")
|
||||||
.select_related("parent")
|
.select_related("parent")
|
||||||
.order_by("name")
|
|
||||||
.distinct()
|
.distinct()
|
||||||
|
.order_by("sort_order")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -1254,7 +1257,11 @@ class IssueSubscriberViewSet(BaseViewSet):
|
|||||||
|
|
||||||
def list(self, request, slug, project_id, issue_id):
|
def list(self, request, slug, project_id, issue_id):
|
||||||
members = (
|
members = (
|
||||||
ProjectMember.objects.filter(workspace__slug=slug, project_id=project_id)
|
ProjectMember.objects.filter(
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
is_subscribed=Exists(
|
is_subscribed=Exists(
|
||||||
IssueSubscriber.objects.filter(
|
IssueSubscriber.objects.filter(
|
||||||
@ -1498,6 +1505,7 @@ class IssueCommentPublicViewSet(BaseViewSet):
|
|||||||
workspace__slug=self.kwargs.get("slug"),
|
workspace__slug=self.kwargs.get("slug"),
|
||||||
project_id=self.kwargs.get("project_id"),
|
project_id=self.kwargs.get("project_id"),
|
||||||
member_id=self.request.user.id,
|
member_id=self.request.user.id,
|
||||||
|
is_active=True,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -1538,6 +1546,7 @@ class IssueCommentPublicViewSet(BaseViewSet):
|
|||||||
if not ProjectMember.objects.filter(
|
if not ProjectMember.objects.filter(
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
member=request.user,
|
member=request.user,
|
||||||
|
is_active=True,
|
||||||
).exists():
|
).exists():
|
||||||
# Add the user for workspace tracking
|
# Add the user for workspace tracking
|
||||||
_ = ProjectPublicMember.objects.get_or_create(
|
_ = ProjectPublicMember.objects.get_or_create(
|
||||||
@ -1651,6 +1660,7 @@ class IssueReactionPublicViewSet(BaseViewSet):
|
|||||||
if not ProjectMember.objects.filter(
|
if not ProjectMember.objects.filter(
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
member=request.user,
|
member=request.user,
|
||||||
|
is_active=True,
|
||||||
).exists():
|
).exists():
|
||||||
# Add the user for workspace tracking
|
# Add the user for workspace tracking
|
||||||
_ = ProjectPublicMember.objects.get_or_create(
|
_ = ProjectPublicMember.objects.get_or_create(
|
||||||
@ -1744,7 +1754,9 @@ class CommentReactionPublicViewSet(BaseViewSet):
|
|||||||
project_id=project_id, comment_id=comment_id, actor=request.user
|
project_id=project_id, comment_id=comment_id, actor=request.user
|
||||||
)
|
)
|
||||||
if not ProjectMember.objects.filter(
|
if not ProjectMember.objects.filter(
|
||||||
project_id=project_id, member=request.user
|
project_id=project_id,
|
||||||
|
member=request.user,
|
||||||
|
is_active=True,
|
||||||
).exists():
|
).exists():
|
||||||
# Add the user for workspace tracking
|
# Add the user for workspace tracking
|
||||||
_ = ProjectPublicMember.objects.get_or_create(
|
_ = ProjectPublicMember.objects.get_or_create(
|
||||||
@ -1829,7 +1841,9 @@ class IssueVotePublicViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
# Add the user for workspace tracking
|
# Add the user for workspace tracking
|
||||||
if not ProjectMember.objects.filter(
|
if not ProjectMember.objects.filter(
|
||||||
project_id=project_id, member=request.user
|
project_id=project_id,
|
||||||
|
member=request.user,
|
||||||
|
is_active=True,
|
||||||
).exists():
|
).exists():
|
||||||
_ = ProjectPublicMember.objects.get_or_create(
|
_ = ProjectPublicMember.objects.get_or_create(
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
|
@ -15,7 +15,7 @@ from rest_framework import status
|
|||||||
from sentry_sdk import capture_exception
|
from sentry_sdk import capture_exception
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from . import BaseViewSet
|
from . import BaseViewSet, WebhookMixin
|
||||||
from plane.api.serializers import (
|
from plane.api.serializers import (
|
||||||
ModuleWriteSerializer,
|
ModuleWriteSerializer,
|
||||||
ModuleSerializer,
|
ModuleSerializer,
|
||||||
@ -41,11 +41,12 @@ 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(WebhookMixin, BaseViewSet):
|
||||||
model = Module
|
model = Module
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
ProjectEntityPermission,
|
ProjectEntityPermission,
|
||||||
]
|
]
|
||||||
|
webhook_event = "module"
|
||||||
|
|
||||||
def get_serializer_class(self):
|
def get_serializer_class(self):
|
||||||
return (
|
return (
|
||||||
|
@ -85,7 +85,10 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
|
|||||||
# Created issues
|
# Created issues
|
||||||
if type == "created":
|
if type == "created":
|
||||||
if WorkspaceMember.objects.filter(
|
if WorkspaceMember.objects.filter(
|
||||||
workspace__slug=slug, member=request.user, role__lt=15
|
workspace__slug=slug,
|
||||||
|
member=request.user,
|
||||||
|
role__lt=15,
|
||||||
|
is_active=True,
|
||||||
).exists():
|
).exists():
|
||||||
notifications = Notification.objects.none()
|
notifications = Notification.objects.none()
|
||||||
else:
|
else:
|
||||||
@ -255,7 +258,10 @@ class MarkAllReadNotificationViewSet(BaseViewSet):
|
|||||||
# Created issues
|
# Created issues
|
||||||
if type == "created":
|
if type == "created":
|
||||||
if WorkspaceMember.objects.filter(
|
if WorkspaceMember.objects.filter(
|
||||||
workspace__slug=slug, member=request.user, role__lt=15
|
workspace__slug=slug,
|
||||||
|
member=request.user,
|
||||||
|
role__lt=15,
|
||||||
|
is_active=True,
|
||||||
).exists():
|
).exists():
|
||||||
notifications = Notification.objects.none()
|
notifications = Notification.objects.none()
|
||||||
else:
|
else:
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
import uuid
|
import uuid
|
||||||
import requests
|
import requests
|
||||||
import os
|
import os
|
||||||
|
from requests.exceptions import RequestException
|
||||||
|
|
||||||
# Django imports
|
# Django imports
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
@ -20,7 +21,14 @@ 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
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from plane.db.models import SocialLoginConnection, User
|
from plane.db.models import (
|
||||||
|
SocialLoginConnection,
|
||||||
|
User,
|
||||||
|
WorkspaceMemberInvite,
|
||||||
|
WorkspaceMember,
|
||||||
|
ProjectMemberInvite,
|
||||||
|
ProjectMember,
|
||||||
|
)
|
||||||
from plane.api.serializers import UserSerializer
|
from plane.api.serializers import UserSerializer
|
||||||
from .base import BaseAPIView
|
from .base import BaseAPIView
|
||||||
|
|
||||||
@ -168,7 +176,6 @@ class OauthEndpoint(BaseAPIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
## Login Case
|
## Login Case
|
||||||
|
|
||||||
if not user.is_active:
|
if not user.is_active:
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
@ -185,12 +192,61 @@ class OauthEndpoint(BaseAPIView):
|
|||||||
user.is_email_verified = email_verified
|
user.is_email_verified = email_verified
|
||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
access_token, refresh_token = get_tokens_for_user(user)
|
# Check if user has any accepted invites for workspace and add them to workspace
|
||||||
|
workspace_member_invites = WorkspaceMemberInvite.objects.filter(
|
||||||
|
email=user.email, accepted=True
|
||||||
|
)
|
||||||
|
|
||||||
data = {
|
WorkspaceMember.objects.bulk_create(
|
||||||
"access_token": access_token,
|
[
|
||||||
"refresh_token": refresh_token,
|
WorkspaceMember(
|
||||||
}
|
workspace_id=workspace_member_invite.workspace_id,
|
||||||
|
member=user,
|
||||||
|
role=workspace_member_invite.role,
|
||||||
|
)
|
||||||
|
for workspace_member_invite in workspace_member_invites
|
||||||
|
],
|
||||||
|
ignore_conflicts=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if user has any project invites
|
||||||
|
project_member_invites = ProjectMemberInvite.objects.filter(
|
||||||
|
email=user.email, accepted=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add user to workspace
|
||||||
|
WorkspaceMember.objects.bulk_create(
|
||||||
|
[
|
||||||
|
WorkspaceMember(
|
||||||
|
workspace_id=project_member_invite.workspace_id,
|
||||||
|
role=project_member_invite.role
|
||||||
|
if project_member_invite.role in [5, 10, 15]
|
||||||
|
else 15,
|
||||||
|
member=user,
|
||||||
|
created_by_id=project_member_invite.created_by_id,
|
||||||
|
)
|
||||||
|
for project_member_invite in project_member_invites
|
||||||
|
],
|
||||||
|
ignore_conflicts=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Now add the users to project
|
||||||
|
ProjectMember.objects.bulk_create(
|
||||||
|
[
|
||||||
|
ProjectMember(
|
||||||
|
workspace_id=project_member_invite.workspace_id,
|
||||||
|
role=project_member_invite.role
|
||||||
|
if project_member_invite.role in [5, 10, 15]
|
||||||
|
else 15,
|
||||||
|
member=user,
|
||||||
|
created_by_id=project_member_invite.created_by_id,
|
||||||
|
) for project_member_invite in project_member_invites
|
||||||
|
],
|
||||||
|
ignore_conflicts=True,
|
||||||
|
)
|
||||||
|
# Delete all the invites
|
||||||
|
workspace_member_invites.delete()
|
||||||
|
project_member_invites.delete()
|
||||||
|
|
||||||
SocialLoginConnection.objects.update_or_create(
|
SocialLoginConnection.objects.update_or_create(
|
||||||
medium=medium,
|
medium=medium,
|
||||||
@ -201,6 +257,7 @@ class OauthEndpoint(BaseAPIView):
|
|||||||
"last_login_at": timezone.now(),
|
"last_login_at": timezone.now(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
try:
|
||||||
if settings.ANALYTICS_BASE_API:
|
if settings.ANALYTICS_BASE_API:
|
||||||
_ = requests.post(
|
_ = requests.post(
|
||||||
settings.ANALYTICS_BASE_API,
|
settings.ANALYTICS_BASE_API,
|
||||||
@ -221,6 +278,15 @@ class OauthEndpoint(BaseAPIView):
|
|||||||
"event_type": "SIGN_IN",
|
"event_type": "SIGN_IN",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
except RequestException as e:
|
||||||
|
capture_exception(e)
|
||||||
|
|
||||||
|
access_token, refresh_token = get_tokens_for_user(user)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"access_token": access_token,
|
||||||
|
"refresh_token": refresh_token,
|
||||||
|
}
|
||||||
return Response(data, status=status.HTTP_200_OK)
|
return Response(data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
except User.DoesNotExist:
|
except User.DoesNotExist:
|
||||||
@ -260,11 +326,63 @@ class OauthEndpoint(BaseAPIView):
|
|||||||
user.token_updated_at = timezone.now()
|
user.token_updated_at = timezone.now()
|
||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
access_token, refresh_token = get_tokens_for_user(user)
|
# Check if user has any accepted invites for workspace and add them to workspace
|
||||||
data = {
|
workspace_member_invites = WorkspaceMemberInvite.objects.filter(
|
||||||
"access_token": access_token,
|
email=user.email, accepted=True
|
||||||
"refresh_token": refresh_token,
|
)
|
||||||
}
|
|
||||||
|
WorkspaceMember.objects.bulk_create(
|
||||||
|
[
|
||||||
|
WorkspaceMember(
|
||||||
|
workspace_id=workspace_member_invite.workspace_id,
|
||||||
|
member=user,
|
||||||
|
role=workspace_member_invite.role,
|
||||||
|
)
|
||||||
|
for workspace_member_invite in workspace_member_invites
|
||||||
|
],
|
||||||
|
ignore_conflicts=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if user has any project invites
|
||||||
|
project_member_invites = ProjectMemberInvite.objects.filter(
|
||||||
|
email=user.email, accepted=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add user to workspace
|
||||||
|
WorkspaceMember.objects.bulk_create(
|
||||||
|
[
|
||||||
|
WorkspaceMember(
|
||||||
|
workspace_id=project_member_invite.workspace_id,
|
||||||
|
role=project_member_invite.role
|
||||||
|
if project_member_invite.role in [5, 10, 15]
|
||||||
|
else 15,
|
||||||
|
member=user,
|
||||||
|
created_by_id=project_member_invite.created_by_id,
|
||||||
|
)
|
||||||
|
for project_member_invite in project_member_invites
|
||||||
|
],
|
||||||
|
ignore_conflicts=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Now add the users to project
|
||||||
|
ProjectMember.objects.bulk_create(
|
||||||
|
[
|
||||||
|
ProjectMember(
|
||||||
|
workspace_id=project_member_invite.workspace_id,
|
||||||
|
role=project_member_invite.role
|
||||||
|
if project_member_invite.role in [5, 10, 15]
|
||||||
|
else 15,
|
||||||
|
member=user,
|
||||||
|
created_by_id=project_member_invite.created_by_id,
|
||||||
|
) for project_member_invite in project_member_invites
|
||||||
|
],
|
||||||
|
ignore_conflicts=True,
|
||||||
|
)
|
||||||
|
# Delete all the invites
|
||||||
|
workspace_member_invites.delete()
|
||||||
|
project_member_invites.delete()
|
||||||
|
|
||||||
|
try:
|
||||||
if settings.ANALYTICS_BASE_API:
|
if settings.ANALYTICS_BASE_API:
|
||||||
_ = requests.post(
|
_ = requests.post(
|
||||||
settings.ANALYTICS_BASE_API,
|
settings.ANALYTICS_BASE_API,
|
||||||
@ -285,6 +403,8 @@ class OauthEndpoint(BaseAPIView):
|
|||||||
"event_type": "SIGN_UP",
|
"event_type": "SIGN_UP",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
except RequestException as e:
|
||||||
|
capture_exception(e)
|
||||||
|
|
||||||
SocialLoginConnection.objects.update_or_create(
|
SocialLoginConnection.objects.update_or_create(
|
||||||
medium=medium,
|
medium=medium,
|
||||||
@ -295,4 +415,10 @@ class OauthEndpoint(BaseAPIView):
|
|||||||
"last_login_at": timezone.now(),
|
"last_login_at": timezone.now(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
access_token, refresh_token = get_tokens_for_user(user)
|
||||||
|
data = {
|
||||||
|
"access_token": access_token,
|
||||||
|
"refresh_token": refresh_token,
|
||||||
|
}
|
||||||
return Response(data, status=status.HTTP_201_CREATED)
|
return Response(data, status=status.HTTP_201_CREATED)
|
||||||
|
@ -1,9 +1,19 @@
|
|||||||
# Python imports
|
# Python imports
|
||||||
from datetime import timedelta, date
|
from datetime import timedelta, date, datetime
|
||||||
|
|
||||||
# Django imports
|
# Django imports
|
||||||
|
from django.db import connection
|
||||||
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
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.views.decorators.gzip import gzip_page
|
||||||
|
from django.db.models import (
|
||||||
|
OuterRef,
|
||||||
|
Func,
|
||||||
|
F,
|
||||||
|
Q,
|
||||||
|
Exists,
|
||||||
|
)
|
||||||
|
|
||||||
# Third party imports
|
# Third party imports
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
@ -15,20 +25,37 @@ from .base import BaseViewSet, BaseAPIView
|
|||||||
from plane.api.permissions import ProjectEntityPermission
|
from plane.api.permissions import ProjectEntityPermission
|
||||||
from plane.db.models import (
|
from plane.db.models import (
|
||||||
Page,
|
Page,
|
||||||
PageBlock,
|
|
||||||
PageFavorite,
|
PageFavorite,
|
||||||
Issue,
|
Issue,
|
||||||
IssueAssignee,
|
IssueAssignee,
|
||||||
IssueActivity,
|
IssueActivity,
|
||||||
|
PageLog,
|
||||||
)
|
)
|
||||||
from plane.api.serializers import (
|
from plane.api.serializers import (
|
||||||
PageSerializer,
|
PageSerializer,
|
||||||
PageBlockSerializer,
|
|
||||||
PageFavoriteSerializer,
|
PageFavoriteSerializer,
|
||||||
|
PageLogSerializer,
|
||||||
IssueLiteSerializer,
|
IssueLiteSerializer,
|
||||||
|
SubPageSerializer,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def unarchive_archive_page_and_descendants(page_id, archived_at):
|
||||||
|
# Your SQL query
|
||||||
|
sql = """
|
||||||
|
WITH RECURSIVE descendants AS (
|
||||||
|
SELECT id FROM pages WHERE id = %s
|
||||||
|
UNION ALL
|
||||||
|
SELECT pages.id FROM pages, descendants WHERE pages.parent_id = descendants.id
|
||||||
|
)
|
||||||
|
UPDATE pages SET archived_at = %s WHERE id IN (SELECT id FROM descendants);
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Execute the SQL query
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
cursor.execute(sql, [page_id, archived_at])
|
||||||
|
|
||||||
|
|
||||||
class PageViewSet(BaseViewSet):
|
class PageViewSet(BaseViewSet):
|
||||||
serializer_class = PageSerializer
|
serializer_class = PageSerializer
|
||||||
model = Page
|
model = Page
|
||||||
@ -52,6 +79,7 @@ class PageViewSet(BaseViewSet):
|
|||||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||||
.filter(project_id=self.kwargs.get("project_id"))
|
.filter(project_id=self.kwargs.get("project_id"))
|
||||||
.filter(project__project_projectmember__member=self.request.user)
|
.filter(project__project_projectmember__member=self.request.user)
|
||||||
|
.filter(parent__isnull=True)
|
||||||
.filter(Q(owned_by=self.request.user) | Q(access=0))
|
.filter(Q(owned_by=self.request.user) | Q(access=0))
|
||||||
.select_related("project")
|
.select_related("project")
|
||||||
.select_related("workspace")
|
.select_related("workspace")
|
||||||
@ -59,15 +87,7 @@ class PageViewSet(BaseViewSet):
|
|||||||
.annotate(is_favorite=Exists(subquery))
|
.annotate(is_favorite=Exists(subquery))
|
||||||
.order_by(self.request.GET.get("order_by", "-created_at"))
|
.order_by(self.request.GET.get("order_by", "-created_at"))
|
||||||
.prefetch_related("labels")
|
.prefetch_related("labels")
|
||||||
.order_by("name", "-is_favorite")
|
.order_by("-is_favorite","-created_at")
|
||||||
.prefetch_related(
|
|
||||||
Prefetch(
|
|
||||||
"blocks",
|
|
||||||
queryset=PageBlock.objects.select_related(
|
|
||||||
"page", "issue", "workspace", "project"
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.distinct()
|
.distinct()
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -88,7 +108,21 @@ class PageViewSet(BaseViewSet):
|
|||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, 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)
|
||||||
|
|
||||||
|
if page.is_locked:
|
||||||
|
return Response(
|
||||||
|
{"error": "Page is locked"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
parent = request.data.get("parent", None)
|
||||||
|
if parent:
|
||||||
|
_ = Page.objects.get(
|
||||||
|
pk=parent, 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 (
|
||||||
page.access != request.data.get("access", page.access)
|
page.access != request.data.get("access", page.access)
|
||||||
@ -100,22 +134,64 @@ class PageViewSet(BaseViewSet):
|
|||||||
},
|
},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
serializer = PageSerializer(page, data=request.data, partial=True)
|
serializer = PageSerializer(page, data=request.data, partial=True)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
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": "Access cannot be updated since this page is owned by someone else"
|
||||||
|
},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
def lock(self, request, slug, project_id, pk):
|
||||||
|
page = Page.objects.filter(
|
||||||
|
pk=pk, workspace__slug=slug, project_id=project_id
|
||||||
|
)
|
||||||
|
|
||||||
|
# only the owner can lock the page
|
||||||
|
if request.user.id != page.owned_by_id:
|
||||||
|
return Response(
|
||||||
|
{"error": "Only the page owner can lock the page"},
|
||||||
|
)
|
||||||
|
|
||||||
|
page.is_locked = True
|
||||||
|
page.save()
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
def unlock(self, request, slug, project_id, pk):
|
||||||
|
page = Page.objects.get(pk=pk, workspace__slug=slug, project_id=project_id)
|
||||||
|
|
||||||
|
# only the owner can unlock the page
|
||||||
|
if request.user.id != page.owned_by_id:
|
||||||
|
return Response(
|
||||||
|
{"error": "Only the page owner can unlock the page"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
page.is_locked = False
|
||||||
|
page.save()
|
||||||
|
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
def list(self, request, slug, project_id):
|
def list(self, request, slug, project_id):
|
||||||
queryset = self.get_queryset()
|
queryset = self.get_queryset().filter(archived_at__isnull=True)
|
||||||
page_view = request.GET.get("page_view", False)
|
page_view = request.GET.get("page_view", False)
|
||||||
|
|
||||||
if not page_view:
|
if not page_view:
|
||||||
return Response({"error": "Page View parameter is required"}, status=status.HTTP_400_BAD_REQUEST)
|
return Response(
|
||||||
|
{"error": "Page View parameter is required"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
# All Pages
|
# All Pages
|
||||||
if page_view == "all":
|
if page_view == "all":
|
||||||
return Response(PageSerializer(queryset, many=True).data, status=status.HTTP_200_OK)
|
return Response(
|
||||||
|
PageSerializer(queryset, many=True).data, status=status.HTTP_200_OK
|
||||||
|
)
|
||||||
|
|
||||||
# Recent pages
|
# Recent pages
|
||||||
if page_view == "recent":
|
if page_view == "recent":
|
||||||
@ -123,15 +199,19 @@ class PageViewSet(BaseViewSet):
|
|||||||
day_before = current_time - timedelta(days=1)
|
day_before = current_time - timedelta(days=1)
|
||||||
todays_pages = queryset.filter(updated_at__date=date.today())
|
todays_pages = queryset.filter(updated_at__date=date.today())
|
||||||
yesterdays_pages = queryset.filter(updated_at__date=day_before)
|
yesterdays_pages = queryset.filter(updated_at__date=day_before)
|
||||||
earlier_this_week = queryset.filter( updated_at__date__range=(
|
earlier_this_week = queryset.filter(
|
||||||
|
updated_at__date__range=(
|
||||||
(timezone.now() - timedelta(days=7)),
|
(timezone.now() - timedelta(days=7)),
|
||||||
(timezone.now() - timedelta(days=2)),
|
(timezone.now() - timedelta(days=2)),
|
||||||
))
|
)
|
||||||
|
)
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
"today": PageSerializer(todays_pages, many=True).data,
|
"today": PageSerializer(todays_pages, many=True).data,
|
||||||
"yesterday": PageSerializer(yesterdays_pages, many=True).data,
|
"yesterday": PageSerializer(yesterdays_pages, many=True).data,
|
||||||
"earlier_this_week": PageSerializer(earlier_this_week, many=True).data,
|
"earlier_this_week": PageSerializer(
|
||||||
|
earlier_this_week, many=True
|
||||||
|
).data,
|
||||||
},
|
},
|
||||||
status=status.HTTP_200_OK,
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
@ -139,50 +219,75 @@ class PageViewSet(BaseViewSet):
|
|||||||
# Favorite Pages
|
# Favorite Pages
|
||||||
if page_view == "favorite":
|
if page_view == "favorite":
|
||||||
queryset = queryset.filter(is_favorite=True)
|
queryset = queryset.filter(is_favorite=True)
|
||||||
return Response(PageSerializer(queryset, many=True).data, status=status.HTTP_200_OK)
|
return Response(
|
||||||
|
PageSerializer(queryset, many=True).data, status=status.HTTP_200_OK
|
||||||
|
)
|
||||||
|
|
||||||
# My pages
|
# My pages
|
||||||
if page_view == "created_by_me":
|
if page_view == "created_by_me":
|
||||||
queryset = queryset.filter(owned_by=request.user)
|
queryset = queryset.filter(owned_by=request.user)
|
||||||
return Response(PageSerializer(queryset, many=True).data, status=status.HTTP_200_OK)
|
return Response(
|
||||||
|
PageSerializer(queryset, many=True).data, status=status.HTTP_200_OK
|
||||||
|
)
|
||||||
|
|
||||||
# Created by other Pages
|
# Created by other Pages
|
||||||
if page_view == "created_by_other":
|
if page_view == "created_by_other":
|
||||||
queryset = queryset.filter(~Q(owned_by=request.user), access=0)
|
queryset = queryset.filter(~Q(owned_by=request.user), access=0)
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
class PageBlockViewSet(BaseViewSet):
|
|
||||||
serializer_class = PageBlockSerializer
|
|
||||||
model = PageBlock
|
|
||||||
permission_classes = [
|
|
||||||
ProjectEntityPermission,
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
return self.filter_queryset(
|
|
||||||
super()
|
|
||||||
.get_queryset()
|
|
||||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
|
||||||
.filter(project_id=self.kwargs.get("project_id"))
|
|
||||||
.filter(page_id=self.kwargs.get("page_id"))
|
|
||||||
.filter(project__project_projectmember__member=self.request.user)
|
|
||||||
.select_related("project")
|
|
||||||
.select_related("workspace")
|
|
||||||
.select_related("page")
|
|
||||||
.select_related("issue")
|
|
||||||
.order_by("sort_order")
|
|
||||||
.distinct()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
return Response(
|
||||||
serializer.save(
|
{"error": "No matching view found"}, status=status.HTTP_400_BAD_REQUEST
|
||||||
project_id=self.kwargs.get("project_id"),
|
|
||||||
page_id=self.kwargs.get("page_id"),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def archive(self, request, slug, project_id, page_id):
|
||||||
|
_ = Page.objects.get(
|
||||||
|
project_id=project_id,
|
||||||
|
owned_by_id=request.user.id,
|
||||||
|
workspace__slug=slug,
|
||||||
|
pk=page_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
unarchive_archive_page_and_descendants(page_id, datetime.now())
|
||||||
|
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
def unarchive(self, request, slug, project_id, page_id):
|
||||||
|
page = Page.objects.get(
|
||||||
|
project_id=project_id,
|
||||||
|
owned_by_id=request.user.id,
|
||||||
|
workspace__slug=slug,
|
||||||
|
pk=page_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
page.parent = None
|
||||||
|
page.save()
|
||||||
|
|
||||||
|
unarchive_archive_page_and_descendants(page_id, None)
|
||||||
|
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
def archive_list(self, request, slug, project_id):
|
||||||
|
pages = (
|
||||||
|
Page.objects.filter(
|
||||||
|
project_id=project_id,
|
||||||
|
workspace__slug=slug,
|
||||||
|
)
|
||||||
|
.filter(archived_at__isnull=False)
|
||||||
|
.filter(parent_id__isnull=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not pages:
|
||||||
|
return Response(
|
||||||
|
{"error": "No pages found"}, status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
PageSerializer(pages, many=True).data, status=status.HTTP_200_OK
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class PageFavoriteViewSet(BaseViewSet):
|
class PageFavoriteViewSet(BaseViewSet):
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
@ -196,6 +301,7 @@ class PageFavoriteViewSet(BaseViewSet):
|
|||||||
return self.filter_queryset(
|
return self.filter_queryset(
|
||||||
super()
|
super()
|
||||||
.get_queryset()
|
.get_queryset()
|
||||||
|
.filter(archived_at__isnull=True)
|
||||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||||
.filter(user=self.request.user)
|
.filter(user=self.request.user)
|
||||||
.select_related("page", "page__owned_by")
|
.select_related("page", "page__owned_by")
|
||||||
@ -218,24 +324,62 @@ 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)
|
||||||
|
|
||||||
class CreateIssueFromPageBlockEndpoint(BaseAPIView):
|
class PageLogEndpoint(BaseAPIView):
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
ProjectEntityPermission,
|
ProjectEntityPermission,
|
||||||
]
|
]
|
||||||
|
|
||||||
def post(self, request, slug, project_id, page_id, page_block_id):
|
serializer_class = PageLogSerializer
|
||||||
page_block = PageBlock.objects.get(
|
model = PageLog
|
||||||
pk=page_block_id,
|
|
||||||
|
def post(self, request, slug, project_id, page_id):
|
||||||
|
serializer = PageLogSerializer(data=request.data)
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save(project_id=project_id, page_id=page_id)
|
||||||
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
def patch(self, request, slug, project_id, page_id, transaction):
|
||||||
|
page_transaction = PageLog.objects.get(
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
page_id=page_id,
|
page_id=page_id,
|
||||||
|
transaction=transaction,
|
||||||
|
)
|
||||||
|
serializer = PageLogSerializer(
|
||||||
|
page_transaction, data=request.data, partial=True
|
||||||
|
)
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save()
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
def delete(self, request, slug, project_id, page_id, transaction):
|
||||||
|
transaction = PageLog.objects.get(
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
page_id=page_id,
|
||||||
|
transaction=transaction,
|
||||||
|
)
|
||||||
|
# Delete the transaction object
|
||||||
|
transaction.delete()
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
|
class CreateIssueFromBlockEndpoint(BaseAPIView):
|
||||||
|
permission_classes = [
|
||||||
|
ProjectEntityPermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
def post(self, request, slug, project_id, page_id):
|
||||||
|
page = Page.objects.get(
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
pk=page_id,
|
||||||
)
|
)
|
||||||
issue = Issue.objects.create(
|
issue = Issue.objects.create(
|
||||||
name=page_block.name,
|
name=request.data.get("name"),
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
description=page_block.description,
|
|
||||||
description_html=page_block.description_html,
|
|
||||||
description_stripped=page_block.description_stripped,
|
|
||||||
)
|
)
|
||||||
_ = IssueAssignee.objects.create(
|
_ = IssueAssignee.objects.create(
|
||||||
issue=issue, assignee=request.user, project_id=project_id
|
issue=issue, assignee=request.user, project_id=project_id
|
||||||
@ -245,11 +389,32 @@ class CreateIssueFromPageBlockEndpoint(BaseAPIView):
|
|||||||
issue=issue,
|
issue=issue,
|
||||||
actor=request.user,
|
actor=request.user,
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
comment=f"created the issue from {page_block.name} block",
|
comment=f"created the issue from {page.name} block",
|
||||||
verb="created",
|
verb="created",
|
||||||
)
|
)
|
||||||
|
|
||||||
page_block.issue = issue
|
|
||||||
page_block.save()
|
|
||||||
|
|
||||||
return Response(IssueLiteSerializer(issue).data, status=status.HTTP_200_OK)
|
return Response(IssueLiteSerializer(issue).data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
class SubPagesEndpoint(BaseAPIView):
|
||||||
|
permission_classes = [
|
||||||
|
ProjectEntityPermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
@method_decorator(gzip_page)
|
||||||
|
def get(self, request, slug, project_id, page_id):
|
||||||
|
pages = (
|
||||||
|
PageLog.objects.filter(
|
||||||
|
page_id=page_id,
|
||||||
|
project_id=project_id,
|
||||||
|
workspace__slug=slug,
|
||||||
|
entity_name__in=["forward_link", "back_link"],
|
||||||
|
)
|
||||||
|
.filter(archived_at__isnull=True)
|
||||||
|
.select_related("project")
|
||||||
|
.select_related("workspace")
|
||||||
|
)
|
||||||
|
return Response(
|
||||||
|
SubPageSerializer(pages, many=True).data, status=status.HTTP_200_OK
|
||||||
|
)
|
||||||
|
|
||||||
|
@ -17,16 +17,16 @@ from django.db.models import (
|
|||||||
)
|
)
|
||||||
from django.core.validators import validate_email
|
from django.core.validators import validate_email
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
# 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
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.permissions import AllowAny
|
from rest_framework.permissions import AllowAny
|
||||||
from sentry_sdk import capture_exception
|
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from .base import BaseViewSet, BaseAPIView
|
from .base import BaseViewSet, BaseAPIView, WebhookMixin
|
||||||
from plane.api.serializers import (
|
from plane.api.serializers import (
|
||||||
ProjectSerializer,
|
ProjectSerializer,
|
||||||
ProjectListSerializer,
|
ProjectListSerializer,
|
||||||
@ -39,6 +39,7 @@ from plane.api.serializers import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from plane.api.permissions import (
|
from plane.api.permissions import (
|
||||||
|
WorkspaceUserPermission,
|
||||||
ProjectBasePermission,
|
ProjectBasePermission,
|
||||||
ProjectEntityPermission,
|
ProjectEntityPermission,
|
||||||
ProjectMemberPermission,
|
ProjectMemberPermission,
|
||||||
@ -58,13 +59,6 @@ from plane.db.models import (
|
|||||||
ProjectIdentifier,
|
ProjectIdentifier,
|
||||||
Module,
|
Module,
|
||||||
Cycle,
|
Cycle,
|
||||||
CycleFavorite,
|
|
||||||
ModuleFavorite,
|
|
||||||
PageFavorite,
|
|
||||||
IssueViewFavorite,
|
|
||||||
Page,
|
|
||||||
IssueAssignee,
|
|
||||||
ModuleMember,
|
|
||||||
Inbox,
|
Inbox,
|
||||||
ProjectDeployBoard,
|
ProjectDeployBoard,
|
||||||
IssueProperty,
|
IssueProperty,
|
||||||
@ -73,9 +67,10 @@ from plane.db.models import (
|
|||||||
from plane.bgtasks.project_invitation_task import project_invitation
|
from plane.bgtasks.project_invitation_task import project_invitation
|
||||||
|
|
||||||
|
|
||||||
class ProjectViewSet(BaseViewSet):
|
class ProjectViewSet(WebhookMixin, BaseViewSet):
|
||||||
serializer_class = ProjectSerializer
|
serializer_class = ProjectSerializer
|
||||||
model = Project
|
model = Project
|
||||||
|
webhook_event = "project"
|
||||||
|
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
ProjectBasePermission,
|
ProjectBasePermission,
|
||||||
@ -110,12 +105,15 @@ class ProjectViewSet(BaseViewSet):
|
|||||||
member=self.request.user,
|
member=self.request.user,
|
||||||
project_id=OuterRef("pk"),
|
project_id=OuterRef("pk"),
|
||||||
workspace__slug=self.kwargs.get("slug"),
|
workspace__slug=self.kwargs.get("slug"),
|
||||||
|
is_active=True,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
total_members=ProjectMember.objects.filter(
|
total_members=ProjectMember.objects.filter(
|
||||||
project_id=OuterRef("id"), member__is_bot=False
|
project_id=OuterRef("id"),
|
||||||
|
member__is_bot=False,
|
||||||
|
is_active=True,
|
||||||
)
|
)
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
@ -137,6 +135,7 @@ class ProjectViewSet(BaseViewSet):
|
|||||||
member_role=ProjectMember.objects.filter(
|
member_role=ProjectMember.objects.filter(
|
||||||
project_id=OuterRef("pk"),
|
project_id=OuterRef("pk"),
|
||||||
member_id=self.request.user.id,
|
member_id=self.request.user.id,
|
||||||
|
is_active=True,
|
||||||
).values("role")
|
).values("role")
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
@ -157,6 +156,7 @@ class ProjectViewSet(BaseViewSet):
|
|||||||
member=request.user,
|
member=request.user,
|
||||||
project_id=OuterRef("pk"),
|
project_id=OuterRef("pk"),
|
||||||
workspace__slug=self.kwargs.get("slug"),
|
workspace__slug=self.kwargs.get("slug"),
|
||||||
|
is_active=True,
|
||||||
).values("sort_order")
|
).values("sort_order")
|
||||||
projects = (
|
projects = (
|
||||||
self.get_queryset()
|
self.get_queryset()
|
||||||
@ -166,6 +166,7 @@ class ProjectViewSet(BaseViewSet):
|
|||||||
"project_projectmember",
|
"project_projectmember",
|
||||||
queryset=ProjectMember.objects.filter(
|
queryset=ProjectMember.objects.filter(
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
|
is_active=True,
|
||||||
).select_related("member"),
|
).select_related("member"),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -345,68 +346,106 @@ class ProjectViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class InviteProjectEndpoint(BaseAPIView):
|
class ProjectInvitationsViewset(BaseViewSet):
|
||||||
|
serializer_class = ProjectMemberInviteSerializer
|
||||||
|
model = ProjectMemberInvite
|
||||||
|
|
||||||
|
search_fields = []
|
||||||
|
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
ProjectBasePermission,
|
ProjectBasePermission,
|
||||||
]
|
]
|
||||||
|
|
||||||
def post(self, request, slug, project_id):
|
def get_queryset(self):
|
||||||
email = request.data.get("email", False)
|
return self.filter_queryset(
|
||||||
role = request.data.get("role", False)
|
super()
|
||||||
|
.get_queryset()
|
||||||
# Check if email is provided
|
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||||
if not email:
|
.filter(project_id=self.kwargs.get("project_id"))
|
||||||
return Response(
|
.select_related("project")
|
||||||
{"error": "Email is required"}, status=status.HTTP_400_BAD_REQUEST
|
.select_related("workspace", "workspace__owner")
|
||||||
)
|
)
|
||||||
|
|
||||||
validate_email(email)
|
def create(self, request, slug, project_id):
|
||||||
# Check if user is already a member of workspace
|
emails = request.data.get("emails", [])
|
||||||
if ProjectMember.objects.filter(
|
|
||||||
project_id=project_id,
|
# Check if email is provided
|
||||||
member__email=email,
|
if not emails:
|
||||||
member__is_bot=False,
|
|
||||||
).exists():
|
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "User is already member of workspace"},
|
{"error": "Emails are required"}, status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
requesting_user = ProjectMember.objects.get(
|
||||||
|
workspace__slug=slug, project_id=project_id, member_id=request.user.id
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if any invited user has an higher role
|
||||||
|
if len(
|
||||||
|
[
|
||||||
|
email
|
||||||
|
for email in emails
|
||||||
|
if int(email.get("role", 10)) > requesting_user.role
|
||||||
|
]
|
||||||
|
):
|
||||||
|
return Response(
|
||||||
|
{"error": "You cannot invite a user with higher role"},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
user = User.objects.filter(email=email).first()
|
workspace = Workspace.objects.get(slug=slug)
|
||||||
|
|
||||||
if user is None:
|
project_invitations = []
|
||||||
token = jwt.encode(
|
for email in emails:
|
||||||
{"email": email, "timestamp": datetime.now().timestamp()},
|
try:
|
||||||
|
validate_email(email.get("email"))
|
||||||
|
project_invitations.append(
|
||||||
|
ProjectMemberInvite(
|
||||||
|
email=email.get("email").strip().lower(),
|
||||||
|
project_id=project_id,
|
||||||
|
workspace_id=workspace.id,
|
||||||
|
token=jwt.encode(
|
||||||
|
{
|
||||||
|
"email": email,
|
||||||
|
"timestamp": datetime.now().timestamp(),
|
||||||
|
},
|
||||||
settings.SECRET_KEY,
|
settings.SECRET_KEY,
|
||||||
algorithm="HS256",
|
algorithm="HS256",
|
||||||
|
),
|
||||||
|
role=email.get("role", 10),
|
||||||
|
created_by=request.user,
|
||||||
)
|
)
|
||||||
project_invitation_obj = ProjectMemberInvite.objects.create(
|
|
||||||
email=email.strip().lower(),
|
|
||||||
project_id=project_id,
|
|
||||||
token=token,
|
|
||||||
role=role,
|
|
||||||
)
|
)
|
||||||
domain = settings.WEB_URL
|
except ValidationError:
|
||||||
project_invitation.delay(email, project_id, token, domain)
|
return Response(
|
||||||
|
{
|
||||||
|
"error": f"Invalid email - {email} provided a valid email address is required to send the invite"
|
||||||
|
},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create workspace member invite
|
||||||
|
project_invitations = ProjectMemberInvite.objects.bulk_create(
|
||||||
|
project_invitations, batch_size=10, ignore_conflicts=True
|
||||||
|
)
|
||||||
|
current_site = request.META.get('HTTP_ORIGIN')
|
||||||
|
|
||||||
|
# Send invitations
|
||||||
|
for invitation in project_invitations:
|
||||||
|
project_invitations.delay(
|
||||||
|
invitation.email,
|
||||||
|
project_id,
|
||||||
|
invitation.token,
|
||||||
|
current_site,
|
||||||
|
request.user.email,
|
||||||
|
)
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
"message": "Email sent successfully",
|
"message": "Email sent successfully",
|
||||||
"id": project_invitation_obj.id,
|
|
||||||
},
|
},
|
||||||
status=status.HTTP_200_OK,
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
|
|
||||||
project_member = ProjectMember.objects.create(
|
|
||||||
member=user, project_id=project_id, role=role
|
|
||||||
)
|
|
||||||
|
|
||||||
_ = IssueProperty.objects.create(user=user, project_id=project_id)
|
|
||||||
|
|
||||||
return Response(
|
|
||||||
ProjectMemberSerializer(project_member).data, status=status.HTTP_200_OK
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class UserProjectInvitationsViewset(BaseViewSet):
|
class UserProjectInvitationsViewset(BaseViewSet):
|
||||||
serializer_class = ProjectMemberInviteSerializer
|
serializer_class = ProjectMemberInviteSerializer
|
||||||
@ -420,40 +459,134 @@ class UserProjectInvitationsViewset(BaseViewSet):
|
|||||||
.select_related("workspace", "workspace__owner", "project")
|
.select_related("workspace", "workspace__owner", "project")
|
||||||
)
|
)
|
||||||
|
|
||||||
def create(self, request):
|
def create(self, request, slug):
|
||||||
invitations = request.data.get("invitations")
|
project_ids = request.data.get("project_ids", [])
|
||||||
project_invitations = ProjectMemberInvite.objects.filter(
|
|
||||||
pk__in=invitations, accepted=True
|
# Get the workspace user role
|
||||||
|
workspace_member = WorkspaceMember.objects.get(
|
||||||
|
member=request.user,
|
||||||
|
workspace__slug=slug,
|
||||||
|
is_active=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
workspace_role = workspace_member.role
|
||||||
|
workspace = workspace_member.workspace
|
||||||
|
|
||||||
ProjectMember.objects.bulk_create(
|
ProjectMember.objects.bulk_create(
|
||||||
[
|
[
|
||||||
ProjectMember(
|
ProjectMember(
|
||||||
project=invitation.project,
|
project_id=project_id,
|
||||||
workspace=invitation.project.workspace,
|
|
||||||
member=request.user,
|
member=request.user,
|
||||||
role=invitation.role,
|
role=15 if workspace_role >= 15 else 10,
|
||||||
|
workspace=workspace,
|
||||||
created_by=request.user,
|
created_by=request.user,
|
||||||
)
|
)
|
||||||
for invitation in project_invitations
|
for project_id in project_ids
|
||||||
]
|
],
|
||||||
|
ignore_conflicts=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
IssueProperty.objects.bulk_create(
|
IssueProperty.objects.bulk_create(
|
||||||
[
|
[
|
||||||
ProjectMember(
|
IssueProperty(
|
||||||
project=invitation.project,
|
project_id=project_id,
|
||||||
workspace=invitation.project.workspace,
|
|
||||||
user=request.user,
|
user=request.user,
|
||||||
|
workspace=workspace,
|
||||||
created_by=request.user,
|
created_by=request.user,
|
||||||
)
|
)
|
||||||
for invitation in project_invitations
|
for project_id in project_ids
|
||||||
]
|
],
|
||||||
|
ignore_conflicts=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Delete joined project invites
|
return Response(
|
||||||
project_invitations.delete()
|
{"message": "Projects joined successfully"},
|
||||||
|
status=status.HTTP_201_CREATED,
|
||||||
|
)
|
||||||
|
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
|
||||||
|
class ProjectJoinEndpoint(BaseAPIView):
|
||||||
|
permission_classes = [
|
||||||
|
AllowAny,
|
||||||
|
]
|
||||||
|
|
||||||
|
def post(self, request, slug, project_id, pk):
|
||||||
|
project_invite = ProjectMemberInvite.objects.get(
|
||||||
|
pk=pk,
|
||||||
|
project_id=project_id,
|
||||||
|
workspace__slug=slug,
|
||||||
|
)
|
||||||
|
|
||||||
|
email = request.data.get("email", "")
|
||||||
|
|
||||||
|
if email == "" or project_invite.email != email:
|
||||||
|
return Response(
|
||||||
|
{"error": "You do not have permission to join the project"},
|
||||||
|
status=status.HTTP_403_FORBIDDEN,
|
||||||
|
)
|
||||||
|
|
||||||
|
if project_invite.responded_at is None:
|
||||||
|
project_invite.accepted = request.data.get("accepted", False)
|
||||||
|
project_invite.responded_at = timezone.now()
|
||||||
|
project_invite.save()
|
||||||
|
|
||||||
|
if project_invite.accepted:
|
||||||
|
# Check if the user account exists
|
||||||
|
user = User.objects.filter(email=email).first()
|
||||||
|
|
||||||
|
# Check if user is a part of workspace
|
||||||
|
workspace_member = WorkspaceMember.objects.filter(
|
||||||
|
workspace__slug=slug, member=user
|
||||||
|
).first()
|
||||||
|
# Add him to workspace
|
||||||
|
if workspace_member is None:
|
||||||
|
_ = WorkspaceMember.objects.create(
|
||||||
|
workspace_id=project_invite.workspace_id,
|
||||||
|
member=user,
|
||||||
|
role=15 if project_invite.role >= 15 else project_invite.role,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Else make him active
|
||||||
|
workspace_member.is_active = True
|
||||||
|
workspace_member.save()
|
||||||
|
|
||||||
|
# Check if the user was already a member of project then activate the user
|
||||||
|
project_member = ProjectMember.objects.filter(
|
||||||
|
workspace_id=project_invite.workspace_id, member=user
|
||||||
|
).first()
|
||||||
|
if project_member is None:
|
||||||
|
# Create a Project Member
|
||||||
|
_ = ProjectMember.objects.create(
|
||||||
|
workspace_id=project_invite.workspace_id,
|
||||||
|
member=user,
|
||||||
|
role=project_invite.role,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
project_member.is_active = True
|
||||||
|
project_member.role = project_member.role
|
||||||
|
project_member.save()
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
{"message": "Project Invitation Accepted"},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
{"message": "Project Invitation was not accepted"},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
{"error": "You have already responded to the invitation request"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get(self, request, slug, project_id, pk):
|
||||||
|
project_invitation = ProjectMemberInvite.objects.get(
|
||||||
|
workspace__slug=slug, project_id=project_id, pk=pk
|
||||||
|
)
|
||||||
|
serializer = ProjectMemberInviteSerializer(project_invitation)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
class ProjectMemberViewSet(BaseViewSet):
|
class ProjectMemberViewSet(BaseViewSet):
|
||||||
@ -475,6 +608,7 @@ class ProjectMemberViewSet(BaseViewSet):
|
|||||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||||
.filter(project_id=self.kwargs.get("project_id"))
|
.filter(project_id=self.kwargs.get("project_id"))
|
||||||
.filter(member__is_bot=False)
|
.filter(member__is_bot=False)
|
||||||
|
.filter()
|
||||||
.select_related("project")
|
.select_related("project")
|
||||||
.select_related("member")
|
.select_related("member")
|
||||||
.select_related("workspace", "workspace__owner")
|
.select_related("workspace", "workspace__owner")
|
||||||
@ -542,13 +676,17 @@ class ProjectMemberViewSet(BaseViewSet):
|
|||||||
|
|
||||||
def list(self, request, slug, project_id):
|
def list(self, request, slug, project_id):
|
||||||
project_member = ProjectMember.objects.get(
|
project_member = ProjectMember.objects.get(
|
||||||
member=request.user, workspace__slug=slug, project_id=project_id
|
member=request.user,
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
is_active=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
project_members = ProjectMember.objects.filter(
|
project_members = ProjectMember.objects.filter(
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
member__is_bot=False,
|
member__is_bot=False,
|
||||||
|
is_active=True,
|
||||||
).select_related("project", "member", "workspace")
|
).select_related("project", "member", "workspace")
|
||||||
|
|
||||||
if project_member.role > 10:
|
if project_member.role > 10:
|
||||||
@ -559,7 +697,10 @@ class ProjectMemberViewSet(BaseViewSet):
|
|||||||
|
|
||||||
def partial_update(self, request, slug, project_id, pk):
|
def partial_update(self, request, slug, project_id, pk):
|
||||||
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,
|
||||||
|
is_active=True,
|
||||||
)
|
)
|
||||||
if request.user.id == project_member.member_id:
|
if request.user.id == project_member.member_id:
|
||||||
return Response(
|
return Response(
|
||||||
@ -568,7 +709,10 @@ class ProjectMemberViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
# Check while updating user roles
|
# Check while updating user roles
|
||||||
requested_project_member = ProjectMember.objects.get(
|
requested_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,
|
||||||
|
is_active=True,
|
||||||
)
|
)
|
||||||
if (
|
if (
|
||||||
"role" in request.data
|
"role" in request.data
|
||||||
@ -591,54 +735,66 @@ class ProjectMemberViewSet(BaseViewSet):
|
|||||||
|
|
||||||
def destroy(self, request, slug, project_id, pk):
|
def destroy(self, request, slug, project_id, pk):
|
||||||
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,
|
||||||
|
member__is_bot=False,
|
||||||
|
is_active=True,
|
||||||
)
|
)
|
||||||
# check requesting user role
|
# check requesting user role
|
||||||
requesting_project_member = ProjectMember.objects.get(
|
requesting_project_member = ProjectMember.objects.get(
|
||||||
workspace__slug=slug, member=request.user, project_id=project_id
|
workspace__slug=slug,
|
||||||
|
member=request.user,
|
||||||
|
project_id=project_id,
|
||||||
|
is_active=True,
|
||||||
)
|
)
|
||||||
|
# User cannot remove himself
|
||||||
|
if str(project_member.id) == str(requesting_project_member.id):
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"error": "You cannot remove yourself from the workspace. Please use leave workspace"
|
||||||
|
},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
# User cannot deactivate higher role
|
||||||
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 you"},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Remove all favorites
|
project_member.is_active = False
|
||||||
ProjectFavorite.objects.filter(
|
project_member.save()
|
||||||
workspace__slug=slug, project_id=project_id, user=project_member.member
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
).delete()
|
|
||||||
CycleFavorite.objects.filter(
|
|
||||||
workspace__slug=slug, project_id=project_id, user=project_member.member
|
|
||||||
).delete()
|
|
||||||
ModuleFavorite.objects.filter(
|
|
||||||
workspace__slug=slug, project_id=project_id, user=project_member.member
|
|
||||||
).delete()
|
|
||||||
PageFavorite.objects.filter(
|
|
||||||
workspace__slug=slug, project_id=project_id, user=project_member.member
|
|
||||||
).delete()
|
|
||||||
IssueViewFavorite.objects.filter(
|
|
||||||
workspace__slug=slug, project_id=project_id, user=project_member.member
|
|
||||||
).delete()
|
|
||||||
# Also remove issue from issue assigned
|
|
||||||
IssueAssignee.objects.filter(
|
|
||||||
workspace__slug=slug,
|
|
||||||
project_id=project_id,
|
|
||||||
assignee=project_member.member,
|
|
||||||
).delete()
|
|
||||||
|
|
||||||
# Remove if module member
|
def leave(self, request, slug, project_id):
|
||||||
ModuleMember.objects.filter(
|
project_member = ProjectMember.objects.get(
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
member=project_member.member,
|
member=request.user,
|
||||||
).delete()
|
is_active=True,
|
||||||
# Delete owned Pages
|
)
|
||||||
Page.objects.filter(
|
|
||||||
|
# Check if the leaving user is the only admin of the project
|
||||||
|
if (
|
||||||
|
project_member.role == 20
|
||||||
|
and not ProjectMember.objects.filter(
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
owned_by=project_member.member,
|
role=20,
|
||||||
).delete()
|
is_active=True,
|
||||||
project_member.delete()
|
).count()
|
||||||
|
> 1
|
||||||
|
):
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"error": "You cannot leave the project as your the only admin of the project you will have to either delete the project or create an another admin",
|
||||||
|
},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
# Deactivate the user
|
||||||
|
project_member.is_active = False
|
||||||
|
project_member.save()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
@ -691,46 +847,6 @@ class AddTeamToProjectEndpoint(BaseAPIView):
|
|||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
|
||||||
class ProjectMemberInvitationsViewset(BaseViewSet):
|
|
||||||
serializer_class = ProjectMemberInviteSerializer
|
|
||||||
model = ProjectMemberInvite
|
|
||||||
|
|
||||||
search_fields = []
|
|
||||||
|
|
||||||
permission_classes = [
|
|
||||||
ProjectBasePermission,
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
return self.filter_queryset(
|
|
||||||
super()
|
|
||||||
.get_queryset()
|
|
||||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
|
||||||
.filter(project_id=self.kwargs.get("project_id"))
|
|
||||||
.select_related("project")
|
|
||||||
.select_related("workspace", "workspace__owner")
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ProjectMemberInviteDetailViewSet(BaseViewSet):
|
|
||||||
serializer_class = ProjectMemberInviteSerializer
|
|
||||||
model = ProjectMemberInvite
|
|
||||||
|
|
||||||
search_fields = []
|
|
||||||
|
|
||||||
permission_classes = [
|
|
||||||
ProjectBasePermission,
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
return self.filter_queryset(
|
|
||||||
super()
|
|
||||||
.get_queryset()
|
|
||||||
.select_related("project")
|
|
||||||
.select_related("workspace", "workspace__owner")
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ProjectIdentifierEndpoint(BaseAPIView):
|
class ProjectIdentifierEndpoint(BaseAPIView):
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
ProjectBasePermission,
|
ProjectBasePermission,
|
||||||
@ -774,59 +890,14 @@ class ProjectIdentifierEndpoint(BaseAPIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ProjectJoinEndpoint(BaseAPIView):
|
|
||||||
def post(self, request, slug):
|
|
||||||
project_ids = request.data.get("project_ids", [])
|
|
||||||
|
|
||||||
# Get the workspace user role
|
|
||||||
workspace_member = WorkspaceMember.objects.get(
|
|
||||||
member=request.user, workspace__slug=slug
|
|
||||||
)
|
|
||||||
|
|
||||||
workspace_role = workspace_member.role
|
|
||||||
workspace = workspace_member.workspace
|
|
||||||
|
|
||||||
ProjectMember.objects.bulk_create(
|
|
||||||
[
|
|
||||||
ProjectMember(
|
|
||||||
project_id=project_id,
|
|
||||||
member=request.user,
|
|
||||||
role=20
|
|
||||||
if workspace_role >= 15
|
|
||||||
else (15 if workspace_role == 10 else workspace_role),
|
|
||||||
workspace=workspace,
|
|
||||||
created_by=request.user,
|
|
||||||
)
|
|
||||||
for project_id in project_ids
|
|
||||||
],
|
|
||||||
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(
|
|
||||||
{"message": "Projects joined successfully"},
|
|
||||||
status=status.HTTP_201_CREATED,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ProjectUserViewsEndpoint(BaseAPIView):
|
class ProjectUserViewsEndpoint(BaseAPIView):
|
||||||
def post(self, request, slug, project_id):
|
def post(self, request, slug, project_id):
|
||||||
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(
|
||||||
member=request.user, project=project
|
member=request.user,
|
||||||
|
project=project,
|
||||||
|
is_active=True,
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
if project_member is None:
|
if project_member is None:
|
||||||
@ -850,7 +921,10 @@ class ProjectUserViewsEndpoint(BaseAPIView):
|
|||||||
class ProjectMemberUserEndpoint(BaseAPIView):
|
class ProjectMemberUserEndpoint(BaseAPIView):
|
||||||
def get(self, request, slug, project_id):
|
def get(self, request, slug, project_id):
|
||||||
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,
|
||||||
|
is_active=True,
|
||||||
)
|
)
|
||||||
serializer = ProjectMemberSerializer(project_member)
|
serializer = ProjectMemberSerializer(project_member)
|
||||||
|
|
||||||
@ -983,39 +1057,6 @@ class WorkspaceProjectDeployBoardEndpoint(BaseAPIView):
|
|||||||
return Response(projects, status=status.HTTP_200_OK)
|
return Response(projects, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
class LeaveProjectEndpoint(BaseAPIView):
|
|
||||||
permission_classes = [
|
|
||||||
ProjectLitePermission,
|
|
||||||
]
|
|
||||||
|
|
||||||
def delete(self, request, slug, project_id):
|
|
||||||
project_member = ProjectMember.objects.get(
|
|
||||||
workspace__slug=slug,
|
|
||||||
member=request.user,
|
|
||||||
project_id=project_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Only Admin case
|
|
||||||
if (
|
|
||||||
project_member.role == 20
|
|
||||||
and ProjectMember.objects.filter(
|
|
||||||
workspace__slug=slug,
|
|
||||||
role=20,
|
|
||||||
project_id=project_id,
|
|
||||||
).count()
|
|
||||||
== 1
|
|
||||||
):
|
|
||||||
return Response(
|
|
||||||
{
|
|
||||||
"error": "You cannot leave the project since you are the only admin of the project you should delete the project"
|
|
||||||
},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
# Delete the member from workspace
|
|
||||||
project_member.delete()
|
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
|
||||||
|
|
||||||
|
|
||||||
class ProjectPublicCoverImagesEndpoint(BaseAPIView):
|
class ProjectPublicCoverImagesEndpoint(BaseAPIView):
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
AllowAny,
|
AllowAny,
|
||||||
|
@ -13,13 +13,8 @@ from plane.api.serializers import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from plane.api.views.base import BaseViewSet, BaseAPIView
|
from plane.api.views.base import BaseViewSet, BaseAPIView
|
||||||
from plane.db.models import (
|
from plane.db.models import User, IssueActivity, WorkspaceMember
|
||||||
User,
|
from plane.license.models import Instance, InstanceAdmin
|
||||||
Workspace,
|
|
||||||
WorkspaceMemberInvite,
|
|
||||||
Issue,
|
|
||||||
IssueActivity,
|
|
||||||
)
|
|
||||||
from plane.utils.paginator import BasePaginator
|
from plane.utils.paginator import BasePaginator
|
||||||
|
|
||||||
|
|
||||||
@ -41,10 +36,33 @@ class UserEndpoint(BaseViewSet):
|
|||||||
serialized_data = UserMeSettingsSerializer(request.user).data
|
serialized_data = UserMeSettingsSerializer(request.user).data
|
||||||
return Response(serialized_data, status=status.HTTP_200_OK)
|
return Response(serialized_data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
def retrieve_instance_admin(self, request):
|
||||||
|
instance = Instance.objects.first()
|
||||||
|
is_admin = InstanceAdmin.objects.filter(
|
||||||
|
instance=instance, user=request.user
|
||||||
|
).exists()
|
||||||
|
return Response({"is_instance_admin": is_admin}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
def deactivate(self, request):
|
||||||
|
# Check all workspace user is active
|
||||||
|
user = self.get_object()
|
||||||
|
if WorkspaceMember.objects.filter(member=request.user, is_active=True).exists():
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"error": "User cannot deactivate account as user is active in some workspaces"
|
||||||
|
},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Deactivate the user
|
||||||
|
user.is_active = False
|
||||||
|
user.save()
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
class UpdateUserOnBoardedEndpoint(BaseAPIView):
|
class UpdateUserOnBoardedEndpoint(BaseAPIView):
|
||||||
def patch(self, request):
|
def patch(self, request):
|
||||||
user = User.objects.get(pk=request.user.id)
|
user = User.objects.get(pk=request.user.id, is_active=True)
|
||||||
user.is_onboarded = request.data.get("is_onboarded", False)
|
user.is_onboarded = request.data.get("is_onboarded", False)
|
||||||
user.save()
|
user.save()
|
||||||
return Response({"message": "Updated successfully"}, status=status.HTTP_200_OK)
|
return Response({"message": "Updated successfully"}, status=status.HTTP_200_OK)
|
||||||
@ -52,7 +70,7 @@ class UpdateUserOnBoardedEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
class UpdateUserTourCompletedEndpoint(BaseAPIView):
|
class UpdateUserTourCompletedEndpoint(BaseAPIView):
|
||||||
def patch(self, request):
|
def patch(self, request):
|
||||||
user = User.objects.get(pk=request.user.id)
|
user = User.objects.get(pk=request.user.id, is_active=True)
|
||||||
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({"message": "Updated successfully"}, status=status.HTTP_200_OK)
|
return Response({"message": "Updated successfully"}, status=status.HTTP_200_OK)
|
||||||
|
130
apiserver/plane/api/views/webhook.py
Normal file
130
apiserver/plane/api/views/webhook.py
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
# Django imports
|
||||||
|
from django.db import IntegrityError
|
||||||
|
|
||||||
|
# Third party imports
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
# Module imports
|
||||||
|
from plane.db.models import Webhook, WebhookLog, Workspace
|
||||||
|
from plane.db.models.webhook import generate_token
|
||||||
|
from .base import BaseAPIView
|
||||||
|
from plane.api.permissions import WorkspaceOwnerPermission
|
||||||
|
from plane.api.serializers import WebhookSerializer, WebhookLogSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class WebhookEndpoint(BaseAPIView):
|
||||||
|
permission_classes = [
|
||||||
|
WorkspaceOwnerPermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
def post(self, request, slug):
|
||||||
|
workspace = Workspace.objects.get(slug=slug)
|
||||||
|
|
||||||
|
try:
|
||||||
|
serializer = WebhookSerializer(data=request.data)
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save(workspace_id=workspace.id)
|
||||||
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
except IntegrityError as e:
|
||||||
|
if "already exists" in str(e):
|
||||||
|
return Response(
|
||||||
|
{"error": "URL already exists for the workspace"},
|
||||||
|
status=status.HTTP_410_GONE,
|
||||||
|
)
|
||||||
|
raise IntegrityError
|
||||||
|
|
||||||
|
def get(self, request, slug, pk=None):
|
||||||
|
if pk == None:
|
||||||
|
webhooks = Webhook.objects.filter(workspace__slug=slug)
|
||||||
|
serializer = WebhookSerializer(
|
||||||
|
webhooks,
|
||||||
|
fields=(
|
||||||
|
"id",
|
||||||
|
"url",
|
||||||
|
"is_active",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
"project",
|
||||||
|
"issue",
|
||||||
|
"cycle",
|
||||||
|
"module",
|
||||||
|
"issue_comment",
|
||||||
|
),
|
||||||
|
many=True,
|
||||||
|
)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
else:
|
||||||
|
webhook = Webhook.objects.get(workspace__slug=slug, pk=pk)
|
||||||
|
serializer = WebhookSerializer(
|
||||||
|
webhook,
|
||||||
|
fields=(
|
||||||
|
"id",
|
||||||
|
"url",
|
||||||
|
"is_active",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
"project",
|
||||||
|
"issue",
|
||||||
|
"cycle",
|
||||||
|
"module",
|
||||||
|
"issue_comment",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
def patch(self, request, slug, pk):
|
||||||
|
webhook = Webhook.objects.get(workspace__slug=slug, pk=pk)
|
||||||
|
serializer = WebhookSerializer(
|
||||||
|
webhook,
|
||||||
|
data=request.data,
|
||||||
|
partial=True,
|
||||||
|
fields=(
|
||||||
|
"id",
|
||||||
|
"url",
|
||||||
|
"is_active",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
"project",
|
||||||
|
"issue",
|
||||||
|
"cycle",
|
||||||
|
"module",
|
||||||
|
"issue_comment",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save()
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
def delete(self, request, slug, pk):
|
||||||
|
webhook = Webhook.objects.get(pk=pk, workspace__slug=slug)
|
||||||
|
webhook.delete()
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
|
class WebhookSecretRegenerateEndpoint(BaseAPIView):
|
||||||
|
permission_classes = [
|
||||||
|
WorkspaceOwnerPermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
def post(self, request, slug, pk):
|
||||||
|
webhook = Webhook.objects.get(workspace__slug=slug, pk=pk)
|
||||||
|
webhook.secret_key = generate_token()
|
||||||
|
webhook.save()
|
||||||
|
serializer = WebhookSerializer(webhook)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
class WebhookLogsEndpoint(BaseAPIView):
|
||||||
|
permission_classes = [
|
||||||
|
WorkspaceOwnerPermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
def get(self, request, slug, webhook_id):
|
||||||
|
webhook_logs = WebhookLog.objects.filter(
|
||||||
|
workspace__slug=slug, webhook_id=webhook_id
|
||||||
|
)
|
||||||
|
serializer = WebhookLogSerializer(webhook_logs, many=True)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
@ -2,7 +2,6 @@
|
|||||||
import jwt
|
import jwt
|
||||||
from datetime import date, datetime
|
from datetime import date, datetime
|
||||||
from dateutil.relativedelta import relativedelta
|
from dateutil.relativedelta import relativedelta
|
||||||
from uuid import uuid4
|
|
||||||
|
|
||||||
# Django imports
|
# Django imports
|
||||||
from django.db import IntegrityError
|
from django.db import IntegrityError
|
||||||
@ -26,13 +25,11 @@ from django.db.models import (
|
|||||||
)
|
)
|
||||||
from django.db.models.functions import ExtractWeek, Cast, ExtractDay
|
from django.db.models.functions import ExtractWeek, Cast, ExtractDay
|
||||||
from django.db.models.fields import DateField
|
from django.db.models.fields import DateField
|
||||||
from django.contrib.auth.hashers import make_password
|
|
||||||
|
|
||||||
# Third party modules
|
# Third party modules
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.permissions import AllowAny
|
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||||
from sentry_sdk import capture_exception
|
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from plane.api.serializers import (
|
from plane.api.serializers import (
|
||||||
@ -59,14 +56,6 @@ from plane.db.models import (
|
|||||||
IssueActivity,
|
IssueActivity,
|
||||||
Issue,
|
Issue,
|
||||||
WorkspaceTheme,
|
WorkspaceTheme,
|
||||||
IssueAssignee,
|
|
||||||
ProjectFavorite,
|
|
||||||
CycleFavorite,
|
|
||||||
ModuleMember,
|
|
||||||
ModuleFavorite,
|
|
||||||
PageFavorite,
|
|
||||||
Page,
|
|
||||||
IssueViewFavorite,
|
|
||||||
IssueLink,
|
IssueLink,
|
||||||
IssueAttachment,
|
IssueAttachment,
|
||||||
IssueSubscriber,
|
IssueSubscriber,
|
||||||
@ -106,7 +95,9 @@ class WorkSpaceViewSet(BaseViewSet):
|
|||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
member_count = (
|
member_count = (
|
||||||
WorkspaceMember.objects.filter(
|
WorkspaceMember.objects.filter(
|
||||||
workspace=OuterRef("id"), member__is_bot=False
|
workspace=OuterRef("id"),
|
||||||
|
member__is_bot=False,
|
||||||
|
is_active=True,
|
||||||
)
|
)
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
@ -181,7 +172,9 @@ class UserWorkSpacesEndpoint(BaseAPIView):
|
|||||||
def get(self, request):
|
def get(self, request):
|
||||||
member_count = (
|
member_count = (
|
||||||
WorkspaceMember.objects.filter(
|
WorkspaceMember.objects.filter(
|
||||||
workspace=OuterRef("id"), member__is_bot=False
|
workspace=OuterRef("id"),
|
||||||
|
member__is_bot=False,
|
||||||
|
is_active=True,
|
||||||
)
|
)
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
@ -227,23 +220,40 @@ class WorkSpaceAvailabilityCheckEndpoint(BaseAPIView):
|
|||||||
return Response({"status": not workspace}, status=status.HTTP_200_OK)
|
return Response({"status": not workspace}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
class InviteWorkspaceEndpoint(BaseAPIView):
|
class WorkspaceInvitationsViewset(BaseViewSet):
|
||||||
|
"""Endpoint for creating, listing and deleting workspaces"""
|
||||||
|
|
||||||
|
serializer_class = WorkSpaceMemberInviteSerializer
|
||||||
|
model = WorkspaceMemberInvite
|
||||||
|
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
WorkSpaceAdminPermission,
|
WorkSpaceAdminPermission,
|
||||||
]
|
]
|
||||||
|
|
||||||
def post(self, request, slug):
|
def get_queryset(self):
|
||||||
emails = request.data.get("emails", False)
|
return self.filter_queryset(
|
||||||
|
super()
|
||||||
|
.get_queryset()
|
||||||
|
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||||
|
.select_related("workspace", "workspace__owner", "created_by")
|
||||||
|
)
|
||||||
|
|
||||||
|
def create(self, request, slug):
|
||||||
|
emails = request.data.get("emails", [])
|
||||||
# Check if email is provided
|
# Check if email is provided
|
||||||
if not emails or not len(emails):
|
if not emails:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Emails are required"}, status=status.HTTP_400_BAD_REQUEST
|
{"error": "Emails are required"}, status=status.HTTP_400_BAD_REQUEST
|
||||||
)
|
)
|
||||||
|
|
||||||
# check for role level
|
# check for role level of the requesting user
|
||||||
requesting_user = WorkspaceMember.objects.get(
|
requesting_user = WorkspaceMember.objects.get(
|
||||||
workspace__slug=slug, member=request.user
|
workspace__slug=slug,
|
||||||
|
member=request.user,
|
||||||
|
is_active=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Check if any invited user has an higher role
|
||||||
if len(
|
if len(
|
||||||
[
|
[
|
||||||
email
|
email
|
||||||
@ -256,15 +266,17 @@ class InviteWorkspaceEndpoint(BaseAPIView):
|
|||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Get the workspace object
|
||||||
workspace = Workspace.objects.get(slug=slug)
|
workspace = Workspace.objects.get(slug=slug)
|
||||||
|
|
||||||
# Check if user is already a member of workspace
|
# Check if user is already a member of workspace
|
||||||
workspace_members = WorkspaceMember.objects.filter(
|
workspace_members = WorkspaceMember.objects.filter(
|
||||||
workspace_id=workspace.id,
|
workspace_id=workspace.id,
|
||||||
member__email__in=[email.get("email") for email in emails],
|
member__email__in=[email.get("email") for email in emails],
|
||||||
|
is_active=True,
|
||||||
).select_related("member", "workspace", "workspace__owner")
|
).select_related("member", "workspace", "workspace__owner")
|
||||||
|
|
||||||
if len(workspace_members):
|
if workspace_members:
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
"error": "Some users are already member of workspace",
|
"error": "Some users are already member of workspace",
|
||||||
@ -302,35 +314,20 @@ class InviteWorkspaceEndpoint(BaseAPIView):
|
|||||||
},
|
},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
WorkspaceMemberInvite.objects.bulk_create(
|
# Create workspace member invite
|
||||||
|
workspace_invitations = WorkspaceMemberInvite.objects.bulk_create(
|
||||||
workspace_invitations, batch_size=10, ignore_conflicts=True
|
workspace_invitations, batch_size=10, ignore_conflicts=True
|
||||||
)
|
)
|
||||||
|
|
||||||
workspace_invitations = WorkspaceMemberInvite.objects.filter(
|
current_site = request.META.get('HTTP_ORIGIN')
|
||||||
email__in=[email.get("email") for email in emails]
|
|
||||||
).select_related("workspace")
|
|
||||||
|
|
||||||
# create the user if signup is disabled
|
|
||||||
if settings.DOCKERIZED and not settings.ENABLE_SIGNUP:
|
|
||||||
_ = User.objects.bulk_create(
|
|
||||||
[
|
|
||||||
User(
|
|
||||||
username=str(uuid4().hex),
|
|
||||||
email=invitation.email,
|
|
||||||
password=make_password(uuid4().hex),
|
|
||||||
is_password_autoset=True,
|
|
||||||
)
|
|
||||||
for invitation in workspace_invitations
|
|
||||||
],
|
|
||||||
batch_size=100,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
# Send invitations
|
||||||
for invitation in workspace_invitations:
|
for invitation in workspace_invitations:
|
||||||
workspace_invitation.delay(
|
workspace_invitation.delay(
|
||||||
invitation.email,
|
invitation.email,
|
||||||
workspace.id,
|
workspace.id,
|
||||||
invitation.token,
|
invitation.token,
|
||||||
settings.WEB_URL,
|
current_site,
|
||||||
request.user.email,
|
request.user.email,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -341,11 +338,19 @@ class InviteWorkspaceEndpoint(BaseAPIView):
|
|||||||
status=status.HTTP_200_OK,
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def destroy(self, request, slug, pk):
|
||||||
|
workspace_member_invite = WorkspaceMemberInvite.objects.get(
|
||||||
|
pk=pk, workspace__slug=slug
|
||||||
|
)
|
||||||
|
workspace_member_invite.delete()
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
class JoinWorkspaceEndpoint(BaseAPIView):
|
|
||||||
|
class WorkspaceJoinEndpoint(BaseAPIView):
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
AllowAny,
|
AllowAny,
|
||||||
]
|
]
|
||||||
|
"""Invitation response endpoint the user can respond to the invitation"""
|
||||||
|
|
||||||
def post(self, request, slug, pk):
|
def post(self, request, slug, pk):
|
||||||
workspace_invite = WorkspaceMemberInvite.objects.get(
|
workspace_invite = WorkspaceMemberInvite.objects.get(
|
||||||
@ -354,12 +359,14 @@ class JoinWorkspaceEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
email = request.data.get("email", "")
|
email = request.data.get("email", "")
|
||||||
|
|
||||||
|
# Check the email
|
||||||
if email == "" or workspace_invite.email != email:
|
if email == "" or workspace_invite.email != email:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "You do not have permission to join the workspace"},
|
{"error": "You do not have permission to join the workspace"},
|
||||||
status=status.HTTP_403_FORBIDDEN,
|
status=status.HTTP_403_FORBIDDEN,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# If already responded then return error
|
||||||
if workspace_invite.responded_at is None:
|
if workspace_invite.responded_at is None:
|
||||||
workspace_invite.accepted = request.data.get("accepted", False)
|
workspace_invite.accepted = request.data.get("accepted", False)
|
||||||
workspace_invite.responded_at = timezone.now()
|
workspace_invite.responded_at = timezone.now()
|
||||||
@ -371,12 +378,23 @@ class JoinWorkspaceEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
# If the user is present then create the workspace member
|
# If the user is present then create the workspace member
|
||||||
if user is not None:
|
if user is not None:
|
||||||
WorkspaceMember.objects.create(
|
# Check if the user was already a member of workspace then activate the user
|
||||||
|
workspace_member = WorkspaceMember.objects.filter(
|
||||||
|
workspace=workspace_invite.workspace, member=user
|
||||||
|
).first()
|
||||||
|
if workspace_member is not None:
|
||||||
|
workspace_member.is_active = True
|
||||||
|
workspace_member.role = workspace_invite.role
|
||||||
|
workspace_member.save()
|
||||||
|
else:
|
||||||
|
# Create a Workspace
|
||||||
|
_ = WorkspaceMember.objects.create(
|
||||||
workspace=workspace_invite.workspace,
|
workspace=workspace_invite.workspace,
|
||||||
member=user,
|
member=user,
|
||||||
role=workspace_invite.role,
|
role=workspace_invite.role,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Set the user last_workspace_id to the accepted workspace
|
||||||
user.last_workspace_id = workspace_invite.workspace.id
|
user.last_workspace_id = workspace_invite.workspace.id
|
||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
@ -388,6 +406,7 @@ class JoinWorkspaceEndpoint(BaseAPIView):
|
|||||||
status=status.HTTP_200_OK,
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Workspace invitation rejected
|
||||||
return Response(
|
return Response(
|
||||||
{"message": "Workspace Invitation was not accepted"},
|
{"message": "Workspace Invitation was not accepted"},
|
||||||
status=status.HTTP_200_OK,
|
status=status.HTTP_200_OK,
|
||||||
@ -398,37 +417,13 @@ class JoinWorkspaceEndpoint(BaseAPIView):
|
|||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get(self, request, slug, pk):
|
||||||
class WorkspaceInvitationsViewset(BaseViewSet):
|
workspace_invitation = WorkspaceMemberInvite.objects.get(workspace__slug=slug, pk=pk)
|
||||||
serializer_class = WorkSpaceMemberInviteSerializer
|
serializer = WorkSpaceMemberInviteSerializer(workspace_invitation)
|
||||||
model = WorkspaceMemberInvite
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
permission_classes = [
|
|
||||||
WorkSpaceAdminPermission,
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
return self.filter_queryset(
|
|
||||||
super()
|
|
||||||
.get_queryset()
|
|
||||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
|
||||||
.select_related("workspace", "workspace__owner", "created_by")
|
|
||||||
)
|
|
||||||
|
|
||||||
def destroy(self, request, slug, pk):
|
|
||||||
workspace_member_invite = WorkspaceMemberInvite.objects.get(
|
|
||||||
pk=pk, workspace__slug=slug
|
|
||||||
)
|
|
||||||
# delete the user if signup is disabled
|
|
||||||
if settings.DOCKERIZED and not settings.ENABLE_SIGNUP:
|
|
||||||
user = User.objects.filter(email=workspace_member_invite.email).first()
|
|
||||||
if user is not None:
|
|
||||||
user.delete()
|
|
||||||
workspace_member_invite.delete()
|
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
|
||||||
|
|
||||||
|
|
||||||
class UserWorkspaceInvitationsEndpoint(BaseViewSet):
|
class UserWorkspaceInvitationsViewSet(BaseViewSet):
|
||||||
serializer_class = WorkSpaceMemberInviteSerializer
|
serializer_class = WorkSpaceMemberInviteSerializer
|
||||||
model = WorkspaceMemberInvite
|
model = WorkspaceMemberInvite
|
||||||
|
|
||||||
@ -442,9 +437,19 @@ class UserWorkspaceInvitationsEndpoint(BaseViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def create(self, request):
|
def create(self, request):
|
||||||
invitations = request.data.get("invitations")
|
invitations = request.data.get("invitations", [])
|
||||||
workspace_invitations = WorkspaceMemberInvite.objects.filter(pk__in=invitations)
|
workspace_invitations = WorkspaceMemberInvite.objects.filter(
|
||||||
|
pk__in=invitations, email=request.user.email
|
||||||
|
).order_by("-created_at")
|
||||||
|
|
||||||
|
# If the user is already a member of workspace and was deactivated then activate the user
|
||||||
|
for invitation in workspace_invitations:
|
||||||
|
# Update the WorkspaceMember for this specific invitation
|
||||||
|
WorkspaceMember.objects.filter(
|
||||||
|
workspace_id=invitation.workspace_id, member=request.user
|
||||||
|
).update(is_active=True, role=invitation.role)
|
||||||
|
|
||||||
|
# Bulk create the user for all the workspaces
|
||||||
WorkspaceMember.objects.bulk_create(
|
WorkspaceMember.objects.bulk_create(
|
||||||
[
|
[
|
||||||
WorkspaceMember(
|
WorkspaceMember(
|
||||||
@ -481,20 +486,24 @@ class WorkSpaceMemberViewSet(BaseViewSet):
|
|||||||
return self.filter_queryset(
|
return self.filter_queryset(
|
||||||
super()
|
super()
|
||||||
.get_queryset()
|
.get_queryset()
|
||||||
.filter(workspace__slug=self.kwargs.get("slug"), member__is_bot=False)
|
.filter(
|
||||||
|
workspace__slug=self.kwargs.get("slug"),
|
||||||
|
member__is_bot=False,
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
.select_related("workspace", "workspace__owner")
|
.select_related("workspace", "workspace__owner")
|
||||||
.select_related("member")
|
.select_related("member")
|
||||||
)
|
)
|
||||||
|
|
||||||
def list(self, request, slug):
|
def list(self, request, slug):
|
||||||
workspace_member = WorkspaceMember.objects.get(
|
workspace_member = WorkspaceMember.objects.get(
|
||||||
member=request.user, workspace__slug=slug
|
member=request.user,
|
||||||
|
workspace__slug=slug,
|
||||||
|
is_active=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
workspace_members = WorkspaceMember.objects.filter(
|
# Get all active workspace members
|
||||||
workspace__slug=slug,
|
workspace_members = self.get_queryset()
|
||||||
member__is_bot=False,
|
|
||||||
).select_related("workspace", "member")
|
|
||||||
|
|
||||||
if workspace_member.role > 10:
|
if workspace_member.role > 10:
|
||||||
serializer = WorkspaceMemberAdminSerializer(workspace_members, many=True)
|
serializer = WorkspaceMemberAdminSerializer(workspace_members, many=True)
|
||||||
@ -506,7 +515,12 @@ class WorkSpaceMemberViewSet(BaseViewSet):
|
|||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def partial_update(self, request, slug, pk):
|
def partial_update(self, request, slug, pk):
|
||||||
workspace_member = WorkspaceMember.objects.get(pk=pk, workspace__slug=slug)
|
workspace_member = WorkspaceMember.objects.get(
|
||||||
|
pk=pk,
|
||||||
|
workspace__slug=slug,
|
||||||
|
member__is_bot=False,
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
if request.user.id == workspace_member.member_id:
|
if request.user.id == workspace_member.member_id:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "You cannot update your own role"},
|
{"error": "You cannot update your own role"},
|
||||||
@ -515,7 +529,9 @@ class WorkSpaceMemberViewSet(BaseViewSet):
|
|||||||
|
|
||||||
# Get the requested user role
|
# Get the requested user role
|
||||||
requested_workspace_member = WorkspaceMember.objects.get(
|
requested_workspace_member = WorkspaceMember.objects.get(
|
||||||
workspace__slug=slug, member=request.user
|
workspace__slug=slug,
|
||||||
|
member=request.user,
|
||||||
|
is_active=True,
|
||||||
)
|
)
|
||||||
# Check if role is being updated
|
# Check if role is being updated
|
||||||
# One cannot update role higher than his own role
|
# One cannot update role higher than his own role
|
||||||
@ -540,68 +556,121 @@ class WorkSpaceMemberViewSet(BaseViewSet):
|
|||||||
|
|
||||||
def destroy(self, request, slug, pk):
|
def destroy(self, request, slug, pk):
|
||||||
# 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,
|
||||||
|
member__is_bot=False,
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
|
||||||
# check requesting user role
|
# check requesting user role
|
||||||
requesting_workspace_member = WorkspaceMember.objects.get(
|
requesting_workspace_member = WorkspaceMember.objects.get(
|
||||||
workspace__slug=slug, member=request.user
|
workspace__slug=slug,
|
||||||
|
member=request.user,
|
||||||
|
is_active=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if str(workspace_member.id) == str(requesting_workspace_member.id):
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"error": "You cannot remove yourself from the workspace. Please use leave workspace"
|
||||||
|
},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
if requesting_workspace_member.role < workspace_member.role:
|
if requesting_workspace_member.role < workspace_member.role:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "You cannot remove a user having role higher than you"},
|
{"error": "You cannot remove a user having role higher than you"},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check for the only member in the workspace
|
|
||||||
if (
|
if (
|
||||||
workspace_member.role == 20
|
Project.objects.annotate(
|
||||||
and WorkspaceMember.objects.filter(
|
total_members=Count("project_projectmember"),
|
||||||
workspace__slug=slug,
|
member_with_role=Count(
|
||||||
role=20,
|
"project_projectmember",
|
||||||
member__is_bot=False,
|
filter=Q(
|
||||||
).count()
|
project_projectmember__member_id=request.user.id,
|
||||||
== 1
|
project_projectmember__role=20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.filter(total_members=1, member_with_role=1, workspace__slug=slug)
|
||||||
|
.exists()
|
||||||
):
|
):
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Cannot delete the only Admin for the workspace"},
|
{
|
||||||
|
"error": "User is part of some projects where they are the only admin you should leave that project first"
|
||||||
|
},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Delete the user also from all the projects
|
# Deactivate the users from the projects where the user is part of
|
||||||
ProjectMember.objects.filter(
|
_ = ProjectMember.objects.filter(
|
||||||
workspace__slug=slug, member=workspace_member.member
|
workspace__slug=slug,
|
||||||
).delete()
|
member_id=workspace_member.member_id,
|
||||||
# Remove all favorites
|
is_active=True,
|
||||||
ProjectFavorite.objects.filter(
|
).update(is_active=False)
|
||||||
workspace__slug=slug, user=workspace_member.member
|
|
||||||
).delete()
|
|
||||||
CycleFavorite.objects.filter(
|
|
||||||
workspace__slug=slug, user=workspace_member.member
|
|
||||||
).delete()
|
|
||||||
ModuleFavorite.objects.filter(
|
|
||||||
workspace__slug=slug, user=workspace_member.member
|
|
||||||
).delete()
|
|
||||||
PageFavorite.objects.filter(
|
|
||||||
workspace__slug=slug, user=workspace_member.member
|
|
||||||
).delete()
|
|
||||||
IssueViewFavorite.objects.filter(
|
|
||||||
workspace__slug=slug, user=workspace_member.member
|
|
||||||
).delete()
|
|
||||||
# Also remove issue from issue assigned
|
|
||||||
IssueAssignee.objects.filter(
|
|
||||||
workspace__slug=slug, assignee=workspace_member.member
|
|
||||||
).delete()
|
|
||||||
|
|
||||||
# Remove if module member
|
workspace_member.is_active = False
|
||||||
ModuleMember.objects.filter(
|
workspace_member.save()
|
||||||
workspace__slug=slug, member=workspace_member.member
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
).delete()
|
|
||||||
# Delete owned Pages
|
|
||||||
Page.objects.filter(
|
|
||||||
workspace__slug=slug, owned_by=workspace_member.member
|
|
||||||
).delete()
|
|
||||||
|
|
||||||
workspace_member.delete()
|
def leave(self, request, slug):
|
||||||
|
workspace_member = WorkspaceMember.objects.get(
|
||||||
|
workspace__slug=slug,
|
||||||
|
member=request.user,
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if the leaving user is the only admin of the workspace
|
||||||
|
if (
|
||||||
|
workspace_member.role == 20
|
||||||
|
and not WorkspaceMember.objects.filter(
|
||||||
|
workspace__slug=slug,
|
||||||
|
role=20,
|
||||||
|
is_active=True,
|
||||||
|
).count()
|
||||||
|
> 1
|
||||||
|
):
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"error": "You cannot leave the workspace as your the only admin of the workspace you will have to either delete the workspace or create an another admin"
|
||||||
|
},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
Project.objects.annotate(
|
||||||
|
total_members=Count("project_projectmember"),
|
||||||
|
member_with_role=Count(
|
||||||
|
"project_projectmember",
|
||||||
|
filter=Q(
|
||||||
|
project_projectmember__member_id=request.user.id,
|
||||||
|
project_projectmember__role=20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.filter(total_members=1, member_with_role=1, workspace__slug=slug)
|
||||||
|
.exists()
|
||||||
|
):
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"error": "User is part of some projects where they are the only admin you should leave that project first"
|
||||||
|
},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
# # Deactivate the users from the projects where the user is part of
|
||||||
|
_ = ProjectMember.objects.filter(
|
||||||
|
workspace__slug=slug,
|
||||||
|
member_id=workspace_member.member_id,
|
||||||
|
is_active=True,
|
||||||
|
).update(is_active=False)
|
||||||
|
|
||||||
|
# # Deactivate the user
|
||||||
|
workspace_member.is_active = False
|
||||||
|
workspace_member.save()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
@ -629,7 +698,9 @@ class TeamMemberViewSet(BaseViewSet):
|
|||||||
def create(self, request, slug):
|
def create(self, request, slug):
|
||||||
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", []),
|
||||||
|
is_active=True,
|
||||||
)
|
)
|
||||||
.annotate(member_str_id=Cast("member", output_field=CharField()))
|
.annotate(member_str_id=Cast("member", output_field=CharField()))
|
||||||
.distinct()
|
.distinct()
|
||||||
@ -658,23 +729,6 @@ class TeamMemberViewSet(BaseViewSet):
|
|||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
class UserWorkspaceInvitationEndpoint(BaseViewSet):
|
|
||||||
model = WorkspaceMemberInvite
|
|
||||||
serializer_class = WorkSpaceMemberInviteSerializer
|
|
||||||
|
|
||||||
permission_classes = [
|
|
||||||
AllowAny,
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
return self.filter_queryset(
|
|
||||||
super()
|
|
||||||
.get_queryset()
|
|
||||||
.filter(pk=self.kwargs.get("pk"))
|
|
||||||
.select_related("workspace")
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class UserLastProjectWithWorkspaceEndpoint(BaseAPIView):
|
class UserLastProjectWithWorkspaceEndpoint(BaseAPIView):
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
user = User.objects.get(pk=request.user.id)
|
user = User.objects.get(pk=request.user.id)
|
||||||
@ -711,7 +765,9 @@ class UserLastProjectWithWorkspaceEndpoint(BaseAPIView):
|
|||||||
class WorkspaceMemberUserEndpoint(BaseAPIView):
|
class WorkspaceMemberUserEndpoint(BaseAPIView):
|
||||||
def get(self, request, slug):
|
def get(self, request, slug):
|
||||||
workspace_member = WorkspaceMember.objects.get(
|
workspace_member = WorkspaceMember.objects.get(
|
||||||
member=request.user, workspace__slug=slug
|
member=request.user,
|
||||||
|
workspace__slug=slug,
|
||||||
|
is_active=True,
|
||||||
)
|
)
|
||||||
serializer = WorkspaceMemberMeSerializer(workspace_member)
|
serializer = WorkspaceMemberMeSerializer(workspace_member)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
@ -720,7 +776,9 @@ class WorkspaceMemberUserEndpoint(BaseAPIView):
|
|||||||
class WorkspaceMemberUserViewsEndpoint(BaseAPIView):
|
class WorkspaceMemberUserViewsEndpoint(BaseAPIView):
|
||||||
def post(self, request, slug):
|
def post(self, request, slug):
|
||||||
workspace_member = WorkspaceMember.objects.get(
|
workspace_member = WorkspaceMember.objects.get(
|
||||||
workspace__slug=slug, member=request.user
|
workspace__slug=slug,
|
||||||
|
member=request.user,
|
||||||
|
is_active=True,
|
||||||
)
|
)
|
||||||
workspace_member.view_props = request.data.get("view_props", {})
|
workspace_member.view_props = request.data.get("view_props", {})
|
||||||
workspace_member.save()
|
workspace_member.save()
|
||||||
@ -1046,7 +1104,9 @@ class WorkspaceUserProfileEndpoint(BaseAPIView):
|
|||||||
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(
|
||||||
workspace__slug=slug, member=request.user
|
workspace__slug=slug,
|
||||||
|
member=request.user,
|
||||||
|
is_active=True,
|
||||||
)
|
)
|
||||||
projects = []
|
projects = []
|
||||||
if requesting_workspace_member.role >= 10:
|
if requesting_workspace_member.role >= 10:
|
||||||
@ -1250,9 +1310,7 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
|
|||||||
status=status.HTTP_200_OK,
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
|
|
||||||
return Response(
|
return Response(issues, status=status.HTTP_200_OK)
|
||||||
issues, status=status.HTTP_200_OK
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class WorkspaceLabelsEndpoint(BaseAPIView):
|
class WorkspaceLabelsEndpoint(BaseAPIView):
|
||||||
@ -1266,30 +1324,3 @@ class WorkspaceLabelsEndpoint(BaseAPIView):
|
|||||||
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)
|
||||||
|
|
||||||
|
|
||||||
class LeaveWorkspaceEndpoint(BaseAPIView):
|
|
||||||
permission_classes = [
|
|
||||||
WorkspaceEntityPermission,
|
|
||||||
]
|
|
||||||
|
|
||||||
def delete(self, request, slug):
|
|
||||||
workspace_member = WorkspaceMember.objects.get(
|
|
||||||
workspace__slug=slug, member=request.user
|
|
||||||
)
|
|
||||||
|
|
||||||
# Only Admin case
|
|
||||||
if (
|
|
||||||
workspace_member.role == 20
|
|
||||||
and WorkspaceMember.objects.filter(workspace__slug=slug, role=20).count()
|
|
||||||
== 1
|
|
||||||
):
|
|
||||||
return Response(
|
|
||||||
{
|
|
||||||
"error": "You cannot leave the workspace since you are the only admin of the workspace you should delete the workspace"
|
|
||||||
},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
# Delete the member from workspace
|
|
||||||
workspace_member.delete()
|
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
|
||||||
|
0
apiserver/plane/authentication/__init__.py
Normal file
0
apiserver/plane/authentication/__init__.py
Normal file
47
apiserver/plane/authentication/api_authentication.py
Normal file
47
apiserver/plane/authentication/api_authentication.py
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
# Django imports
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
|
# Third party imports
|
||||||
|
from rest_framework import authentication
|
||||||
|
from rest_framework.exceptions import AuthenticationFailed
|
||||||
|
|
||||||
|
# Module imports
|
||||||
|
from plane.db.models import APIToken
|
||||||
|
|
||||||
|
|
||||||
|
class APIKeyAuthentication(authentication.BaseAuthentication):
|
||||||
|
"""
|
||||||
|
Authentication with an API Key
|
||||||
|
"""
|
||||||
|
|
||||||
|
www_authenticate_realm = "api"
|
||||||
|
media_type = "application/json"
|
||||||
|
auth_header_name = "X-Api-Key"
|
||||||
|
|
||||||
|
def get_api_token(self, request):
|
||||||
|
return request.headers.get(self.auth_header_name)
|
||||||
|
|
||||||
|
def validate_api_token(self, token):
|
||||||
|
try:
|
||||||
|
api_token = APIToken.objects.get(
|
||||||
|
Q(Q(expired_at__gt=timezone.now()) | Q(expired_at__isnull=True)),
|
||||||
|
token=token,
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
except APIToken.DoesNotExist:
|
||||||
|
raise AuthenticationFailed("Given API token is not valid")
|
||||||
|
|
||||||
|
# save api token last used
|
||||||
|
api_token.last_used = timezone.now()
|
||||||
|
api_token.save(update_fields=["last_used"])
|
||||||
|
return (api_token.user, api_token.token)
|
||||||
|
|
||||||
|
def authenticate(self, request):
|
||||||
|
token = self.get_api_token(request=request)
|
||||||
|
if not token:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Validate the API token
|
||||||
|
user, token = self.validate_api_token(token)
|
||||||
|
return user, token
|
5
apiserver/plane/authentication/apps.py
Normal file
5
apiserver/plane/authentication/apps.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class ApiConfig(AppConfig):
|
||||||
|
name = "plane.authentication"
|
@ -3,7 +3,7 @@ import csv
|
|||||||
import io
|
import io
|
||||||
|
|
||||||
# Django imports
|
# Django imports
|
||||||
from django.core.mail import EmailMultiAlternatives
|
from django.core.mail import EmailMultiAlternatives, get_connection
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.utils.html import strip_tags
|
from django.utils.html import strip_tags
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@ -16,6 +16,8 @@ from sentry_sdk import capture_exception
|
|||||||
from plane.db.models import Issue
|
from plane.db.models import Issue
|
||||||
from plane.utils.analytics_plot import build_graph_plot
|
from plane.utils.analytics_plot import build_graph_plot
|
||||||
from plane.utils.issue_filters import issue_filters
|
from plane.utils.issue_filters import issue_filters
|
||||||
|
from plane.license.models import InstanceConfiguration
|
||||||
|
from plane.license.utils.instance_value import get_configuration_value
|
||||||
|
|
||||||
row_mapping = {
|
row_mapping = {
|
||||||
"state__name": "State",
|
"state__name": "State",
|
||||||
@ -47,7 +49,19 @@ def send_export_email(email, slug, csv_buffer):
|
|||||||
text_content = strip_tags(html_content)
|
text_content = strip_tags(html_content)
|
||||||
|
|
||||||
csv_buffer.seek(0)
|
csv_buffer.seek(0)
|
||||||
msg = EmailMultiAlternatives(subject, text_content, settings.EMAIL_FROM, [email])
|
|
||||||
|
# Configure email connection from the database
|
||||||
|
instance_configuration = InstanceConfiguration.objects.filter(key__startswith='EMAIL_').values("key", "value")
|
||||||
|
connection = get_connection(
|
||||||
|
host=get_configuration_value(instance_configuration, "EMAIL_HOST"),
|
||||||
|
port=int(get_configuration_value(instance_configuration, "EMAIL_PORT", "587")),
|
||||||
|
username=get_configuration_value(instance_configuration, "EMAIL_HOST_USER"),
|
||||||
|
password=get_configuration_value(instance_configuration, "EMAIL_HOST_PASSWORD"),
|
||||||
|
use_tls=bool(get_configuration_value(instance_configuration, "EMAIL_USE_TLS", "1")),
|
||||||
|
use_ssl=bool(get_configuration_value(instance_configuration, "EMAIL_USE_SSL", "0")),
|
||||||
|
)
|
||||||
|
|
||||||
|
msg = EmailMultiAlternatives(subject=subject, body=text_content, from_email=get_configuration_value(instance_configuration, "EMAIL_FROM"), to=[email], connection=connection)
|
||||||
msg.attach(f"{slug}-analytics.csv", csv_buffer.getvalue())
|
msg.attach(f"{slug}-analytics.csv", csv_buffer.getvalue())
|
||||||
msg.send(fail_silently=False)
|
msg.send(fail_silently=False)
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# Django imports
|
# Django imports
|
||||||
from django.core.mail import EmailMultiAlternatives
|
from django.core.mail import EmailMultiAlternatives, get_connection
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.utils.html import strip_tags
|
from django.utils.html import strip_tags
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@ -11,8 +11,8 @@ from celery import shared_task
|
|||||||
from sentry_sdk import capture_exception
|
from sentry_sdk import capture_exception
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from plane.db.models import User
|
from plane.license.models import InstanceConfiguration
|
||||||
|
from plane.license.utils.instance_value import get_configuration_value
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def email_verification(first_name, email, token, current_site):
|
def email_verification(first_name, email, token, current_site):
|
||||||
@ -34,7 +34,18 @@ def email_verification(first_name, email, token, current_site):
|
|||||||
|
|
||||||
text_content = strip_tags(html_content)
|
text_content = strip_tags(html_content)
|
||||||
|
|
||||||
msg = EmailMultiAlternatives(subject, text_content, from_email_string, [email])
|
# Configure email connection from the database
|
||||||
|
instance_configuration = InstanceConfiguration.objects.filter(key__startswith='EMAIL_').values("key", "value")
|
||||||
|
connection = get_connection(
|
||||||
|
host=get_configuration_value(instance_configuration, "EMAIL_HOST"),
|
||||||
|
port=int(get_configuration_value(instance_configuration, "EMAIL_PORT", "587")),
|
||||||
|
username=get_configuration_value(instance_configuration, "EMAIL_HOST_USER"),
|
||||||
|
password=get_configuration_value(instance_configuration, "EMAIL_HOST_PASSWORD"),
|
||||||
|
use_tls=bool(get_configuration_value(instance_configuration, "EMAIL_USE_TLS", "1")),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Initiate email alternatives
|
||||||
|
msg = EmailMultiAlternatives(subject=subject, body=text_content, from_email=get_configuration_value(instance_configuration, "EMAIL_FROM"), to=[email], connection=connection)
|
||||||
msg.attach_alternative(html_content, "text/html")
|
msg.attach_alternative(html_content, "text/html")
|
||||||
msg.send()
|
msg.send()
|
||||||
return
|
return
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# Django imports
|
# Django imports
|
||||||
from django.core.mail import EmailMultiAlternatives
|
from django.core.mail import EmailMultiAlternatives, get_connection
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.utils.html import strip_tags
|
from django.utils.html import strip_tags
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@ -8,7 +8,9 @@ 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.license.models import InstanceConfiguration
|
||||||
|
from plane.license.utils.instance_value import get_configuration_value
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def forgot_password(first_name, email, uidb64, token, current_site):
|
def forgot_password(first_name, email, uidb64, token, current_site):
|
||||||
@ -30,7 +32,16 @@ def forgot_password(first_name, email, uidb64, token, current_site):
|
|||||||
|
|
||||||
text_content = strip_tags(html_content)
|
text_content = strip_tags(html_content)
|
||||||
|
|
||||||
msg = EmailMultiAlternatives(subject, text_content, from_email_string, [email])
|
instance_configuration = InstanceConfiguration.objects.filter(key__startswith='EMAIL_').values("key", "value")
|
||||||
|
connection = get_connection(
|
||||||
|
host=get_configuration_value(instance_configuration, "EMAIL_HOST"),
|
||||||
|
port=int(get_configuration_value(instance_configuration, "EMAIL_PORT", "587")),
|
||||||
|
username=get_configuration_value(instance_configuration, "EMAIL_HOST_USER"),
|
||||||
|
password=get_configuration_value(instance_configuration, "EMAIL_HOST_PASSWORD"),
|
||||||
|
use_tls=bool(get_configuration_value(instance_configuration, "EMAIL_USE_TLS", "1")),
|
||||||
|
)
|
||||||
|
# Initiate email alternatives
|
||||||
|
msg = EmailMultiAlternatives(subject=subject, body=text_content, from_email=get_configuration_value(instance_configuration, "EMAIL_FROM"), to=[email], connection=connection)
|
||||||
msg.attach_alternative(html_content, "text/html")
|
msg.attach_alternative(html_content, "text/html")
|
||||||
msg.send()
|
msg.send()
|
||||||
return
|
return
|
||||||
|
@ -73,6 +73,12 @@ def service_importer(service, importer_id):
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Check if any of the users are already member of workspace
|
||||||
|
_ = WorkspaceMember.objects.filter(
|
||||||
|
member__in=[user for user in workspace_users],
|
||||||
|
workspace_id=importer.workspace_id,
|
||||||
|
).update(is_active=True)
|
||||||
|
|
||||||
# Add new users to Workspace and project automatically
|
# Add new users to Workspace and project automatically
|
||||||
WorkspaceMember.objects.bulk_create(
|
WorkspaceMember.objects.bulk_create(
|
||||||
[
|
[
|
||||||
|
@ -12,7 +12,7 @@ from celery import shared_task
|
|||||||
from sentry_sdk import capture_exception
|
from sentry_sdk import capture_exception
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from plane.db.models import Issue, Project, State
|
from plane.db.models import Issue, Project, State, Page
|
||||||
from plane.bgtasks.issue_activites_task import issue_activity
|
from plane.bgtasks.issue_activites_task import issue_activity
|
||||||
|
|
||||||
|
|
||||||
@ -20,6 +20,7 @@ from plane.bgtasks.issue_activites_task import issue_activity
|
|||||||
def archive_and_close_old_issues():
|
def archive_and_close_old_issues():
|
||||||
archive_old_issues()
|
archive_old_issues()
|
||||||
close_old_issues()
|
close_old_issues()
|
||||||
|
delete_archived_pages()
|
||||||
|
|
||||||
|
|
||||||
def archive_old_issues():
|
def archive_old_issues():
|
||||||
@ -80,7 +81,7 @@ def archive_old_issues():
|
|||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
current_instance=json.dumps({"archived_at": 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 issues_to_update
|
for issue in issues_to_update
|
||||||
]
|
]
|
||||||
@ -142,17 +143,21 @@ 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:
|
||||||
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",
|
||||||
requested_data=json.dumps({"closed_to": str(issue.state_id)}),
|
requested_data=json.dumps(
|
||||||
|
{"closed_to": str(issue.state_id)}
|
||||||
|
),
|
||||||
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=None,
|
||||||
subscriber=False,
|
subscriber=False,
|
||||||
epoch=int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp()),
|
||||||
)
|
)
|
||||||
for issue in issues_to_update
|
for issue in issues_to_update
|
||||||
]
|
]
|
||||||
@ -162,3 +167,20 @@ def close_old_issues():
|
|||||||
print(e)
|
print(e)
|
||||||
capture_exception(e)
|
capture_exception(e)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def delete_archived_pages():
|
||||||
|
try:
|
||||||
|
pages_to_delete = Page.objects.filter(
|
||||||
|
archived_at__isnull=False,
|
||||||
|
archived_at__lte=(timezone.now() - timedelta(days=30)),
|
||||||
|
)
|
||||||
|
|
||||||
|
pages_to_delete._raw_delete(pages_to_delete.db)
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
if settings.DEBUG:
|
||||||
|
print(e)
|
||||||
|
capture_exception(e)
|
||||||
|
return
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# Django imports
|
# Django imports
|
||||||
from django.core.mail import EmailMultiAlternatives
|
from django.core.mail import EmailMultiAlternatives, get_connection
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.utils.html import strip_tags
|
from django.utils.html import strip_tags
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@ -8,6 +8,9 @@ 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.license.models import InstanceConfiguration
|
||||||
|
from plane.license.utils.instance_value import get_configuration_value
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def magic_link(email, key, token, current_site):
|
def magic_link(email, key, token, current_site):
|
||||||
@ -15,8 +18,6 @@ def magic_link(email, key, token, current_site):
|
|||||||
realtivelink = f"/magic-sign-in/?password={token}&key={key}"
|
realtivelink = f"/magic-sign-in/?password={token}&key={key}"
|
||||||
abs_url = current_site + realtivelink
|
abs_url = current_site + realtivelink
|
||||||
|
|
||||||
from_email_string = settings.EMAIL_FROM
|
|
||||||
|
|
||||||
subject = "Login for Plane"
|
subject = "Login for Plane"
|
||||||
|
|
||||||
context = {"magic_url": abs_url, "code": token}
|
context = {"magic_url": abs_url, "code": token}
|
||||||
@ -25,7 +26,17 @@ def magic_link(email, key, token, current_site):
|
|||||||
|
|
||||||
text_content = strip_tags(html_content)
|
text_content = strip_tags(html_content)
|
||||||
|
|
||||||
msg = EmailMultiAlternatives(subject, text_content, from_email_string, [email])
|
instance_configuration = InstanceConfiguration.objects.filter(key__startswith='EMAIL_').values("key", "value")
|
||||||
|
connection = get_connection(
|
||||||
|
host=get_configuration_value(instance_configuration, "EMAIL_HOST"),
|
||||||
|
port=int(get_configuration_value(instance_configuration, "EMAIL_PORT", "587")),
|
||||||
|
username=get_configuration_value(instance_configuration, "EMAIL_HOST_USER"),
|
||||||
|
password=get_configuration_value(instance_configuration, "EMAIL_HOST_PASSWORD"),
|
||||||
|
use_tls=bool(get_configuration_value(instance_configuration, "EMAIL_USE_TLS", "1")),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Initiate email alternatives
|
||||||
|
msg = EmailMultiAlternatives(subject=subject, body=text_content, from_email=get_configuration_value(instance_configuration, "EMAIL_FROM"), to=[email], connection=connection)
|
||||||
msg.attach_alternative(html_content, "text/html")
|
msg.attach_alternative(html_content, "text/html")
|
||||||
msg.send()
|
msg.send()
|
||||||
return
|
return
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# Django imports
|
# Django imports
|
||||||
from django.core.mail import EmailMultiAlternatives
|
from django.core.mail import EmailMultiAlternatives, get_connection
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.utils.html import strip_tags
|
from django.utils.html import strip_tags
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@ -10,26 +10,28 @@ from sentry_sdk import capture_exception
|
|||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from plane.db.models import Project, User, ProjectMemberInvite
|
from plane.db.models import Project, User, ProjectMemberInvite
|
||||||
|
from plane.license.models import InstanceConfiguration
|
||||||
|
from plane.license.utils.instance_value import get_configuration_value
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def project_invitation(email, project_id, token, current_site):
|
def project_invitation(email, project_id, token, current_site, invitor):
|
||||||
try:
|
try:
|
||||||
|
user = User.objects.get(email=invitor)
|
||||||
project = Project.objects.get(pk=project_id)
|
project = Project.objects.get(pk=project_id)
|
||||||
project_member_invite = ProjectMemberInvite.objects.get(
|
project_member_invite = ProjectMemberInvite.objects.get(
|
||||||
token=token, email=email
|
token=token, email=email
|
||||||
)
|
)
|
||||||
|
|
||||||
relativelink = f"/project-member-invitation/{project_member_invite.id}"
|
relativelink = f"/project-invitations/?invitation_id={project_member_invite.id}&email={email}&slug={project.workspace.slug}&project_id={str(project_id)}"
|
||||||
abs_url = current_site + relativelink
|
abs_url = current_site + relativelink
|
||||||
|
|
||||||
from_email_string = settings.EMAIL_FROM
|
from_email_string = settings.EMAIL_FROM
|
||||||
|
|
||||||
subject = f"{project.created_by.first_name or project.created_by.email} invited you to join {project.name} on Plane"
|
subject = f"{user.first_name or user.display_name or user.email} invited you to join {project.name} on Plane"
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
"email": email,
|
"email": email,
|
||||||
"first_name": project.created_by.first_name,
|
"first_name": user.first_name,
|
||||||
"project_name": project.name,
|
"project_name": project.name,
|
||||||
"invitation_url": abs_url,
|
"invitation_url": abs_url,
|
||||||
}
|
}
|
||||||
@ -43,7 +45,17 @@ def project_invitation(email, project_id, token, current_site):
|
|||||||
project_member_invite.message = text_content
|
project_member_invite.message = text_content
|
||||||
project_member_invite.save()
|
project_member_invite.save()
|
||||||
|
|
||||||
msg = EmailMultiAlternatives(subject, text_content, from_email_string, [email])
|
# Configure email connection from the database
|
||||||
|
instance_configuration = InstanceConfiguration.objects.filter(key__startswith='EMAIL_').values("key", "value")
|
||||||
|
connection = get_connection(
|
||||||
|
host=get_configuration_value(instance_configuration, "EMAIL_HOST"),
|
||||||
|
port=int(get_configuration_value(instance_configuration, "EMAIL_PORT", "587")),
|
||||||
|
username=get_configuration_value(instance_configuration, "EMAIL_HOST_USER"),
|
||||||
|
password=get_configuration_value(instance_configuration, "EMAIL_HOST_PASSWORD"),
|
||||||
|
use_tls=bool(get_configuration_value(instance_configuration, "EMAIL_USE_TLS", "1")),
|
||||||
|
)
|
||||||
|
# Initiate email alternatives
|
||||||
|
msg = EmailMultiAlternatives(subject=subject, body=text_content, from_email=get_configuration_value(instance_configuration, "EMAIL_FROM"), to=[email], connection=connection)
|
||||||
msg.attach_alternative(html_content, "text/html")
|
msg.attach_alternative(html_content, "text/html")
|
||||||
msg.send()
|
msg.send()
|
||||||
return
|
return
|
||||||
|
139
apiserver/plane/bgtasks/webhook_task.py
Normal file
139
apiserver/plane/bgtasks/webhook_task.py
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
import requests
|
||||||
|
import uuid
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
|
||||||
|
# Django imports
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
# Third party imports
|
||||||
|
from celery import shared_task
|
||||||
|
from sentry_sdk import capture_exception
|
||||||
|
|
||||||
|
from plane.db.models import Webhook, WebhookLog
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task(
|
||||||
|
bind=True,
|
||||||
|
autoretry_for=(requests.RequestException,),
|
||||||
|
retry_backoff=600,
|
||||||
|
max_retries=5,
|
||||||
|
retry_jitter=True,
|
||||||
|
)
|
||||||
|
def webhook_task(self, webhook, slug, event, event_data, action):
|
||||||
|
try:
|
||||||
|
webhook = Webhook.objects.get(id=webhook, workspace__slug=slug)
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"User-Agent": "Autopilot",
|
||||||
|
"X-Plane-Delivery": str(uuid.uuid4()),
|
||||||
|
"X-Plane-Event": event,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Your secret key
|
||||||
|
if webhook.secret_key:
|
||||||
|
# Concatenate the data and the secret key
|
||||||
|
message = event_data + webhook.secret_key
|
||||||
|
|
||||||
|
# Create a SHA-256 hash of the message
|
||||||
|
sha256 = hashlib.sha256()
|
||||||
|
sha256.update(message.encode("utf-8"))
|
||||||
|
signature = sha256.hexdigest()
|
||||||
|
headers["X-Plane-Signature"] = signature
|
||||||
|
|
||||||
|
event_data = json.loads(event_data) if event_data is not None else None
|
||||||
|
|
||||||
|
action = {
|
||||||
|
"POST": "create",
|
||||||
|
"PATCH": "update",
|
||||||
|
"PUT": "update",
|
||||||
|
"DELETE": "delete",
|
||||||
|
}.get(action, action)
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"event": event,
|
||||||
|
"action": action,
|
||||||
|
"webhook_id": str(webhook.id),
|
||||||
|
"workspace_id": str(webhook.workspace_id),
|
||||||
|
"data": event_data,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Send the webhook event
|
||||||
|
response = requests.post(
|
||||||
|
webhook.url,
|
||||||
|
headers=headers,
|
||||||
|
json=payload,
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Log the webhook request
|
||||||
|
WebhookLog.objects.create(
|
||||||
|
workspace_id=str(webhook.workspace_id),
|
||||||
|
webhook_id=str(webhook.id),
|
||||||
|
event_type=str(event),
|
||||||
|
request_method=str(action),
|
||||||
|
request_headers=str(headers),
|
||||||
|
request_body=str(payload),
|
||||||
|
response_status=str(response.status_code),
|
||||||
|
response_headers=str(response.headers),
|
||||||
|
response_body=str(response.text),
|
||||||
|
retry_count=str(self.request.retries),
|
||||||
|
)
|
||||||
|
|
||||||
|
except requests.RequestException as e:
|
||||||
|
# Log the failed webhook request
|
||||||
|
WebhookLog.objects.create(
|
||||||
|
workspace_id=str(webhook.workspace_id),
|
||||||
|
webhook_id=str(webhook.id),
|
||||||
|
event_type=str(event),
|
||||||
|
request_method=str(action),
|
||||||
|
request_headers=str(headers),
|
||||||
|
request_body=str(payload),
|
||||||
|
response_status=500,
|
||||||
|
response_headers="",
|
||||||
|
response_body=str(e),
|
||||||
|
retry_count=str(self.request.retries),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Retry logic
|
||||||
|
if self.request.retries >= self.max_retries:
|
||||||
|
Webhook.objects.filter(pk=webhook.id).update(is_active=False)
|
||||||
|
return
|
||||||
|
raise requests.RequestException()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
if settings.DEBUG:
|
||||||
|
print(e)
|
||||||
|
capture_exception(e)
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task()
|
||||||
|
def send_webhook(event, event_data, action, slug):
|
||||||
|
try:
|
||||||
|
webhooks = Webhook.objects.filter(workspace__slug=slug, is_active=True)
|
||||||
|
|
||||||
|
if event == "project":
|
||||||
|
webhooks = webhooks.filter(project=True)
|
||||||
|
|
||||||
|
if event == "issue":
|
||||||
|
webhooks = webhooks.filter(issue=True)
|
||||||
|
|
||||||
|
if event == "module":
|
||||||
|
webhooks = webhooks.filter(module=True)
|
||||||
|
|
||||||
|
if event == "cycle":
|
||||||
|
webhooks = webhooks.filter(cycle=True)
|
||||||
|
|
||||||
|
if event == "issue-comment":
|
||||||
|
webhooks = webhooks.filter(issue_comment=True)
|
||||||
|
|
||||||
|
for webhook in webhooks:
|
||||||
|
webhook_task.delay(webhook.id, slug, event, event_data, action)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
if settings.DEBUG:
|
||||||
|
print(e)
|
||||||
|
capture_exception(e)
|
||||||
|
return
|
@ -1,5 +1,5 @@
|
|||||||
# Django imports
|
# Django imports
|
||||||
from django.core.mail import EmailMultiAlternatives
|
from django.core.mail import EmailMultiAlternatives, get_connection
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.utils.html import strip_tags
|
from django.utils.html import strip_tags
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@ -11,25 +11,32 @@ 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, WorkspaceMemberInvite
|
from plane.db.models import Workspace, WorkspaceMemberInvite, User
|
||||||
|
from plane.license.models import InstanceConfiguration
|
||||||
|
from plane.license.utils.instance_value import get_configuration_value
|
||||||
|
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def workspace_invitation(email, workspace_id, token, current_site, invitor):
|
def workspace_invitation(email, workspace_id, token, current_site, invitor):
|
||||||
try:
|
try:
|
||||||
|
user = User.objects.get(email=invitor)
|
||||||
|
|
||||||
workspace = Workspace.objects.get(pk=workspace_id)
|
workspace = Workspace.objects.get(pk=workspace_id)
|
||||||
workspace_member_invite = WorkspaceMemberInvite.objects.get(
|
workspace_member_invite = WorkspaceMemberInvite.objects.get(
|
||||||
token=token, email=email
|
token=token, email=email
|
||||||
)
|
)
|
||||||
|
|
||||||
realtivelink = (
|
# Relative link
|
||||||
f"/workspace-member-invitation/?invitation_id={workspace_member_invite.id}&email={email}"
|
relative_link = f"/workspace-invitations/?invitation_id={workspace_member_invite.id}&email={email}&slug={workspace.slug}"
|
||||||
)
|
|
||||||
abs_url = current_site + realtivelink
|
|
||||||
|
|
||||||
|
# The complete url including the domain
|
||||||
|
abs_url = current_site + relative_link
|
||||||
|
|
||||||
|
# The email from
|
||||||
from_email_string = settings.EMAIL_FROM
|
from_email_string = settings.EMAIL_FROM
|
||||||
|
|
||||||
subject = f"{invitor or email} invited you to join {workspace.name} on Plane"
|
# Subject of the email
|
||||||
|
subject = f"{user.first_name or user.display_name or user.email} invited you to join {workspace.name} on Plane"
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
"email": email,
|
"email": email,
|
||||||
@ -47,7 +54,30 @@ def workspace_invitation(email, workspace_id, token, current_site, invitor):
|
|||||||
workspace_member_invite.message = text_content
|
workspace_member_invite.message = text_content
|
||||||
workspace_member_invite.save()
|
workspace_member_invite.save()
|
||||||
|
|
||||||
msg = EmailMultiAlternatives(subject, text_content, from_email_string, [email])
|
instance_configuration = InstanceConfiguration.objects.filter(
|
||||||
|
key__startswith="EMAIL_"
|
||||||
|
).values("key", "value")
|
||||||
|
connection = get_connection(
|
||||||
|
host=get_configuration_value(instance_configuration, "EMAIL_HOST"),
|
||||||
|
port=int(
|
||||||
|
get_configuration_value(instance_configuration, "EMAIL_PORT", "587")
|
||||||
|
),
|
||||||
|
username=get_configuration_value(instance_configuration, "EMAIL_HOST_USER"),
|
||||||
|
password=get_configuration_value(
|
||||||
|
instance_configuration, "EMAIL_HOST_PASSWORD"
|
||||||
|
),
|
||||||
|
use_tls=bool(
|
||||||
|
get_configuration_value(instance_configuration, "EMAIL_USE_TLS", "1")
|
||||||
|
),
|
||||||
|
)
|
||||||
|
# Initiate email alternatives
|
||||||
|
msg = EmailMultiAlternatives(
|
||||||
|
subject=subject,
|
||||||
|
body=text_content,
|
||||||
|
from_email=get_configuration_value(instance_configuration, "EMAIL_FROM"),
|
||||||
|
to=[email],
|
||||||
|
connection=connection,
|
||||||
|
)
|
||||||
msg.attach_alternative(html_content, "text/html")
|
msg.attach_alternative(html_content, "text/html")
|
||||||
msg.send()
|
msg.send()
|
||||||
|
|
||||||
|
71
apiserver/plane/db/management/commands/create_bucket.py
Normal file
71
apiserver/plane/db/management/commands/create_bucket.py
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
# Python imports
|
||||||
|
import boto3
|
||||||
|
import json
|
||||||
|
from botocore.exceptions import ClientError
|
||||||
|
|
||||||
|
# Django imports
|
||||||
|
from django.core.management import BaseCommand
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = "Create the default bucket for the instance"
|
||||||
|
|
||||||
|
def set_bucket_public_policy(self, s3_client, bucket_name):
|
||||||
|
public_policy = {
|
||||||
|
"Version": "2012-10-17",
|
||||||
|
"Statement": [{
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Principal": "*",
|
||||||
|
"Action": ["s3:GetObject"],
|
||||||
|
"Resource": [f"arn:aws:s3:::{bucket_name}/*"]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
s3_client.put_bucket_policy(
|
||||||
|
Bucket=bucket_name,
|
||||||
|
Policy=json.dumps(public_policy)
|
||||||
|
)
|
||||||
|
self.stdout.write(self.style.SUCCESS(f"Public read access policy set for bucket '{bucket_name}'."))
|
||||||
|
except ClientError as e:
|
||||||
|
self.stdout.write(self.style.ERROR(f"Error setting public read access policy: {e}"))
|
||||||
|
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
# Create a session using the credentials from Django settings
|
||||||
|
try:
|
||||||
|
session = boto3.session.Session(
|
||||||
|
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
|
||||||
|
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
|
||||||
|
)
|
||||||
|
# Create an S3 client using the session
|
||||||
|
s3_client = session.client('s3', endpoint_url=settings.AWS_S3_ENDPOINT_URL)
|
||||||
|
bucket_name = settings.AWS_STORAGE_BUCKET_NAME
|
||||||
|
|
||||||
|
self.stdout.write(self.style.NOTICE("Checking bucket..."))
|
||||||
|
|
||||||
|
# Check if the bucket exists
|
||||||
|
s3_client.head_bucket(Bucket=bucket_name)
|
||||||
|
|
||||||
|
self.set_bucket_public_policy(s3_client, bucket_name)
|
||||||
|
except ClientError as e:
|
||||||
|
error_code = int(e.response['Error']['Code'])
|
||||||
|
bucket_name = settings.AWS_STORAGE_BUCKET_NAME
|
||||||
|
if error_code == 404:
|
||||||
|
# Bucket does not exist, create it
|
||||||
|
self.stdout.write(self.style.WARNING(f"Bucket '{bucket_name}' does not exist. Creating bucket..."))
|
||||||
|
try:
|
||||||
|
s3_client.create_bucket(Bucket=bucket_name)
|
||||||
|
self.stdout.write(self.style.SUCCESS(f"Bucket '{bucket_name}' created successfully."))
|
||||||
|
self.set_bucket_public_policy(s3_client, bucket_name)
|
||||||
|
except ClientError as create_error:
|
||||||
|
self.stdout.write(self.style.ERROR(f"Failed to create bucket: {create_error}"))
|
||||||
|
elif error_code == 403:
|
||||||
|
# Access to the bucket is forbidden
|
||||||
|
self.stdout.write(self.style.ERROR(f"Access to the bucket '{bucket_name}' is forbidden. Check permissions."))
|
||||||
|
else:
|
||||||
|
# Another ClientError occurred
|
||||||
|
self.stdout.write(self.style.ERROR(f"Failed to check bucket: {e}"))
|
||||||
|
except Exception as ex:
|
||||||
|
# Handle any other exception
|
||||||
|
self.stdout.write(self.style.ERROR(f"An error occurred: {ex}"))
|
@ -3,7 +3,7 @@
|
|||||||
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.api_token
|
import plane.db.models.api
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
@ -40,8 +40,8 @@ class Migration(migrations.Migration):
|
|||||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified 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)),
|
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
|
||||||
('token', models.CharField(default=plane.db.models.api_token.generate_token, max_length=255, unique=True)),
|
('token', models.CharField(default=plane.db.models.api.generate_token, max_length=255, unique=True)),
|
||||||
('label', models.CharField(default=plane.db.models.api_token.generate_label_token, max_length=255)),
|
('label', models.CharField(default=plane.db.models.api.generate_label_token, max_length=255)),
|
||||||
('user_type', models.PositiveSmallIntegerField(choices=[(0, 'Human'), (1, 'Bot')], default=0)),
|
('user_type', models.PositiveSmallIntegerField(choices=[(0, 'Human'), (1, 'Bot')], default=0)),
|
||||||
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='apitoken_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='apitoken_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='apitoken_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='apitoken_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
|
||||||
|
@ -1,41 +0,0 @@
|
|||||||
# 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),
|
|
||||||
),
|
|
||||||
]
|
|
@ -0,0 +1,984 @@
|
|||||||
|
# Generated by Django 4.2.5 on 2023-11-15 09:47
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import plane.db.models.issue
|
||||||
|
import uuid
|
||||||
|
import random
|
||||||
|
|
||||||
|
def random_sort_ordering(apps, schema_editor):
|
||||||
|
Label = apps.get_model("db", "Label")
|
||||||
|
|
||||||
|
bulk_labels = []
|
||||||
|
for label in Label.objects.all():
|
||||||
|
label.sort_order = random.randint(0,65535)
|
||||||
|
bulk_labels.append(label)
|
||||||
|
|
||||||
|
Label.objects.bulk_update(bulk_labels, ["sort_order"], batch_size=1000)
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('db', '0045_issueactivity_epoch_workspacemember_issue_props_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='label',
|
||||||
|
name='sort_order',
|
||||||
|
field=models.FloatField(default=65535),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='analyticview',
|
||||||
|
name='created_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='analyticview',
|
||||||
|
name='updated_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='apitoken',
|
||||||
|
name='created_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='apitoken',
|
||||||
|
name='updated_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cycle',
|
||||||
|
name='created_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cycle',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cycle',
|
||||||
|
name='updated_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cycle',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cyclefavorite',
|
||||||
|
name='created_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cyclefavorite',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cyclefavorite',
|
||||||
|
name='updated_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cyclefavorite',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cycleissue',
|
||||||
|
name='created_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cycleissue',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cycleissue',
|
||||||
|
name='updated_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cycleissue',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='estimate',
|
||||||
|
name='created_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='estimate',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='estimate',
|
||||||
|
name='updated_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='estimate',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='estimatepoint',
|
||||||
|
name='created_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='estimatepoint',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='estimatepoint',
|
||||||
|
name='updated_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='estimatepoint',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='fileasset',
|
||||||
|
name='created_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='fileasset',
|
||||||
|
name='updated_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='githubcommentsync',
|
||||||
|
name='created_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='githubcommentsync',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='githubcommentsync',
|
||||||
|
name='updated_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='githubcommentsync',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='githubissuesync',
|
||||||
|
name='created_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='githubissuesync',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='githubissuesync',
|
||||||
|
name='updated_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='githubissuesync',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='githubrepository',
|
||||||
|
name='created_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='githubrepository',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='githubrepository',
|
||||||
|
name='updated_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='githubrepository',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='githubrepositorysync',
|
||||||
|
name='created_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='githubrepositorysync',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='githubrepositorysync',
|
||||||
|
name='updated_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='githubrepositorysync',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='importer',
|
||||||
|
name='created_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='importer',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='importer',
|
||||||
|
name='updated_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='importer',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='inbox',
|
||||||
|
name='created_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='inbox',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='inbox',
|
||||||
|
name='updated_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='inbox',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='inboxissue',
|
||||||
|
name='created_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='inboxissue',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='inboxissue',
|
||||||
|
name='updated_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='inboxissue',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='integration',
|
||||||
|
name='created_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='integration',
|
||||||
|
name='updated_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issue',
|
||||||
|
name='created_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issue',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issue',
|
||||||
|
name='updated_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issue',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issueactivity',
|
||||||
|
name='created_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issueactivity',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issueactivity',
|
||||||
|
name='updated_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issueactivity',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issueassignee',
|
||||||
|
name='created_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issueassignee',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issueassignee',
|
||||||
|
name='updated_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issueassignee',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issueattachment',
|
||||||
|
name='created_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issueattachment',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issueattachment',
|
||||||
|
name='updated_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issueattachment',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issueblocker',
|
||||||
|
name='created_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issueblocker',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issueblocker',
|
||||||
|
name='updated_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issueblocker',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issuecomment',
|
||||||
|
name='created_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issuecomment',
|
||||||
|
name='issue',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_comments', to='db.issue'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issuecomment',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issuecomment',
|
||||||
|
name='updated_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issuecomment',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issuelabel',
|
||||||
|
name='created_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issuelabel',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issuelabel',
|
||||||
|
name='updated_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issuelabel',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issuelink',
|
||||||
|
name='created_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issuelink',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issuelink',
|
||||||
|
name='updated_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issuelink',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issueproperty',
|
||||||
|
name='created_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issueproperty',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issueproperty',
|
||||||
|
name='properties',
|
||||||
|
field=models.JSONField(default=plane.db.models.issue.get_default_properties),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issueproperty',
|
||||||
|
name='updated_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issueproperty',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issuesequence',
|
||||||
|
name='created_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issuesequence',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issuesequence',
|
||||||
|
name='updated_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issuesequence',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issueview',
|
||||||
|
name='created_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issueview',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issueview',
|
||||||
|
name='updated_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issueview',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issueviewfavorite',
|
||||||
|
name='created_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issueviewfavorite',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issueviewfavorite',
|
||||||
|
name='updated_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issueviewfavorite',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='label',
|
||||||
|
name='created_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='label',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='label',
|
||||||
|
name='updated_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='label',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='module',
|
||||||
|
name='created_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='module',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='module',
|
||||||
|
name='updated_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='module',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='modulefavorite',
|
||||||
|
name='created_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='modulefavorite',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='modulefavorite',
|
||||||
|
name='updated_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='modulefavorite',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='moduleissue',
|
||||||
|
name='created_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='moduleissue',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='moduleissue',
|
||||||
|
name='updated_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='moduleissue',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='modulelink',
|
||||||
|
name='created_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='modulelink',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='modulelink',
|
||||||
|
name='updated_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='modulelink',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='modulemember',
|
||||||
|
name='created_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='modulemember',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='modulemember',
|
||||||
|
name='updated_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='modulemember',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='page',
|
||||||
|
name='created_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='page',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='page',
|
||||||
|
name='updated_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='page',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='pageblock',
|
||||||
|
name='created_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='pageblock',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='pageblock',
|
||||||
|
name='updated_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='pageblock',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='pagefavorite',
|
||||||
|
name='created_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='pagefavorite',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='pagefavorite',
|
||||||
|
name='updated_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='pagefavorite',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='pagelabel',
|
||||||
|
name='created_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='pagelabel',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='pagelabel',
|
||||||
|
name='updated_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='pagelabel',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='project',
|
||||||
|
name='created_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='project',
|
||||||
|
name='updated_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='projectfavorite',
|
||||||
|
name='created_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='projectfavorite',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='projectfavorite',
|
||||||
|
name='updated_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='projectfavorite',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='projectidentifier',
|
||||||
|
name='created_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='projectidentifier',
|
||||||
|
name='updated_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='projectmember',
|
||||||
|
name='created_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='projectmember',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='projectmember',
|
||||||
|
name='updated_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='projectmember',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='projectmemberinvite',
|
||||||
|
name='created_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='projectmemberinvite',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='projectmemberinvite',
|
||||||
|
name='updated_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='projectmemberinvite',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='slackprojectsync',
|
||||||
|
name='created_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='slackprojectsync',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='slackprojectsync',
|
||||||
|
name='updated_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='slackprojectsync',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='socialloginconnection',
|
||||||
|
name='created_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='socialloginconnection',
|
||||||
|
name='updated_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='state',
|
||||||
|
name='created_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='state',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='state',
|
||||||
|
name='updated_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='state',
|
||||||
|
name='workspace',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='team',
|
||||||
|
name='created_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='team',
|
||||||
|
name='updated_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='teammember',
|
||||||
|
name='created_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='teammember',
|
||||||
|
name='updated_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='workspace',
|
||||||
|
name='created_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='workspace',
|
||||||
|
name='updated_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='workspaceintegration',
|
||||||
|
name='created_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='workspaceintegration',
|
||||||
|
name='updated_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='workspacemember',
|
||||||
|
name='created_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='workspacemember',
|
||||||
|
name='updated_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='workspacememberinvite',
|
||||||
|
name='created_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='workspacememberinvite',
|
||||||
|
name='updated_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='workspacetheme',
|
||||||
|
name='created_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='workspacetheme',
|
||||||
|
name='updated_by',
|
||||||
|
field=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'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='IssueMention',
|
||||||
|
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)),
|
||||||
|
('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')),
|
||||||
|
('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_mention', to='db.issue')),
|
||||||
|
('mention', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_mention', to=settings.AUTH_USER_MODEL)),
|
||||||
|
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project')),
|
||||||
|
('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='workspace_%(class)s', to='db.workspace')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Issue Mention',
|
||||||
|
'verbose_name_plural': 'Issue Mentions',
|
||||||
|
'db_table': 'issue_mentions',
|
||||||
|
'ordering': ('-created_at',),
|
||||||
|
'unique_together': {('issue', 'mention')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.RunPython(random_sort_ordering),
|
||||||
|
]
|
@ -0,0 +1,131 @@
|
|||||||
|
# Generated by Django 4.2.5 on 2023-11-15 11:20
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import plane.db.models.api
|
||||||
|
import plane.db.models.webhook
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('db', '0046_label_sort_order_alter_analyticview_created_by_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Webhook',
|
||||||
|
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)),
|
||||||
|
('url', models.URLField(validators=[plane.db.models.webhook.validate_schema, plane.db.models.webhook.validate_domain])),
|
||||||
|
('is_active', models.BooleanField(default=True)),
|
||||||
|
('secret_key', models.CharField(default=plane.db.models.webhook.generate_token, max_length=255)),
|
||||||
|
('project', models.BooleanField(default=False)),
|
||||||
|
('issue', models.BooleanField(default=False)),
|
||||||
|
('module', models.BooleanField(default=False)),
|
||||||
|
('cycle', models.BooleanField(default=False)),
|
||||||
|
('issue_comment', models.BooleanField(default=False)),
|
||||||
|
('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')),
|
||||||
|
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_webhooks', to='db.workspace')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Webhook',
|
||||||
|
'verbose_name_plural': 'Webhooks',
|
||||||
|
'db_table': 'webhooks',
|
||||||
|
'ordering': ('-created_at',),
|
||||||
|
'unique_together': {('workspace', 'url')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='apitoken',
|
||||||
|
name='description',
|
||||||
|
field=models.TextField(blank=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='apitoken',
|
||||||
|
name='expired_at',
|
||||||
|
field=models.DateTimeField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='apitoken',
|
||||||
|
name='is_active',
|
||||||
|
field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='apitoken',
|
||||||
|
name='last_used',
|
||||||
|
field=models.DateTimeField(null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='projectmember',
|
||||||
|
name='is_active',
|
||||||
|
field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='workspacemember',
|
||||||
|
name='is_active',
|
||||||
|
field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='apitoken',
|
||||||
|
name='token',
|
||||||
|
field=models.CharField(db_index=True, default=plane.db.models.api.generate_token, max_length=255, unique=True),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='WebhookLog',
|
||||||
|
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)),
|
||||||
|
('event_type', models.CharField(blank=True, max_length=255, null=True)),
|
||||||
|
('request_method', models.CharField(blank=True, max_length=10, null=True)),
|
||||||
|
('request_headers', models.TextField(blank=True, null=True)),
|
||||||
|
('request_body', models.TextField(blank=True, null=True)),
|
||||||
|
('response_status', models.TextField(blank=True, null=True)),
|
||||||
|
('response_headers', models.TextField(blank=True, null=True)),
|
||||||
|
('response_body', models.TextField(blank=True, null=True)),
|
||||||
|
('retry_count', models.PositiveSmallIntegerField(default=0)),
|
||||||
|
('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')),
|
||||||
|
('webhook', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='logs', to='db.webhook')),
|
||||||
|
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='webhook_logs', to='db.workspace')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Webhook Log',
|
||||||
|
'verbose_name_plural': 'Webhook Logs',
|
||||||
|
'db_table': 'webhook_logs',
|
||||||
|
'ordering': ('-created_at',),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='APIActivityLog',
|
||||||
|
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)),
|
||||||
|
('token_identifier', models.CharField(max_length=255)),
|
||||||
|
('path', models.CharField(max_length=255)),
|
||||||
|
('method', models.CharField(max_length=10)),
|
||||||
|
('query_params', models.TextField(blank=True, null=True)),
|
||||||
|
('headers', models.TextField(blank=True, null=True)),
|
||||||
|
('body', models.TextField(blank=True, null=True)),
|
||||||
|
('response_code', models.PositiveIntegerField()),
|
||||||
|
('response_body', models.TextField(blank=True, null=True)),
|
||||||
|
('ip_address', models.GenericIPAddressField(blank=True, null=True)),
|
||||||
|
('user_agent', models.CharField(blank=True, max_length=512, null=True)),
|
||||||
|
('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')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'API Activity Log',
|
||||||
|
'verbose_name_plural': 'API Activity Logs',
|
||||||
|
'db_table': 'api_activity_logs',
|
||||||
|
'ordering': ('-created_at',),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
54
apiserver/plane/db/migrations/0048_auto_20231116_0713.py
Normal file
54
apiserver/plane/db/migrations/0048_auto_20231116_0713.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
# Generated by Django 4.2.5 on 2023-11-13 12:53
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('db', '0047_webhook_apitoken_description_apitoken_expired_at_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PageLog',
|
||||||
|
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)),
|
||||||
|
('transaction', models.UUIDField(default=uuid.uuid4)),
|
||||||
|
('entity_identifier', models.UUIDField(null=True)),
|
||||||
|
('entity_name', models.CharField(choices=[('to_do', 'To Do'), ('issue', 'issue'), ('image', 'Image'), ('video', 'Video'), ('file', 'File'), ('link', 'Link'), ('cycle', 'Cycle'), ('module', 'Module'), ('back_link', 'Back Link'), ('forward_link', 'Forward Link'), ('mention', 'Mention')], max_length=30, verbose_name='Transaction Type')),
|
||||||
|
('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')),
|
||||||
|
('page', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='page_log', to='db.page')),
|
||||||
|
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project')),
|
||||||
|
('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='workspace_%(class)s', to='db.workspace')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Page Log',
|
||||||
|
'verbose_name_plural': 'Page Logs',
|
||||||
|
'db_table': 'page_logs',
|
||||||
|
'ordering': ('-created_at',),
|
||||||
|
'unique_together': {('page', 'transaction')}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='page',
|
||||||
|
name='archived_at',
|
||||||
|
field=models.DateField(null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='page',
|
||||||
|
name='is_locked',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='page',
|
||||||
|
name='parent',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='child_page', to='db.page'),
|
||||||
|
),
|
||||||
|
]
|
72
apiserver/plane/db/migrations/0049_auto_20231116_0713.py
Normal file
72
apiserver/plane/db/migrations/0049_auto_20231116_0713.py
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
# Generated by Django 4.2.5 on 2023-11-15 09:16
|
||||||
|
|
||||||
|
# Python imports
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def update_pages(apps, schema_editor):
|
||||||
|
try:
|
||||||
|
Page = apps.get_model("db", "Page")
|
||||||
|
PageBlock = apps.get_model("db", "PageBlock")
|
||||||
|
PageLog = apps.get_model("db", "PageLog")
|
||||||
|
|
||||||
|
updated_pages = []
|
||||||
|
page_logs = []
|
||||||
|
|
||||||
|
# looping through all the pages
|
||||||
|
for page in Page.objects.all():
|
||||||
|
page_blocks = PageBlock.objects.filter(
|
||||||
|
page_id=page.id, project_id=page.project_id, workspace_id=page.workspace_id
|
||||||
|
).order_by("sort_order")
|
||||||
|
|
||||||
|
if page_blocks:
|
||||||
|
# looping through all the page blocks in a page
|
||||||
|
for page_block in page_blocks:
|
||||||
|
if page_block.issue is not None:
|
||||||
|
project_identifier = page.project.identifier
|
||||||
|
sequence_id = page_block.issue.sequence_id
|
||||||
|
transaction = uuid.uuid4().hex
|
||||||
|
embed_component = f'<issue-embed-component id="{transaction}" entity_name="issue" entity_identifier="{page_block.issue_id}" sequence_id="{sequence_id}" project_identifier="{project_identifier}" title="{page_block.name}"></issue-embed-component>'
|
||||||
|
page.description_html += embed_component
|
||||||
|
|
||||||
|
# create the page transaction for the issue
|
||||||
|
page_logs.append(
|
||||||
|
PageLog(
|
||||||
|
page_id=page_block.page_id,
|
||||||
|
transaction=transaction,
|
||||||
|
entity_identifier=page_block.issue_id,
|
||||||
|
entity_name="issue",
|
||||||
|
project_id=page.project_id,
|
||||||
|
workspace_id=page.workspace_id,
|
||||||
|
created_by_id=page_block.created_by_id,
|
||||||
|
updated_by_id=page_block.updated_by_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# adding the page block name and description to the page description
|
||||||
|
page.description_html += f"<h2>{page_block.name}</h2>"
|
||||||
|
page.description_html += page_block.description_html
|
||||||
|
|
||||||
|
updated_pages.append(page)
|
||||||
|
|
||||||
|
Page.objects.bulk_update(
|
||||||
|
updated_pages,
|
||||||
|
["description_html"],
|
||||||
|
batch_size=100,
|
||||||
|
)
|
||||||
|
PageLog.objects.bulk_create(page_logs, batch_size=100)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("db", "0048_auto_20231116_0713"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(update_pages),
|
||||||
|
]
|
@ -54,7 +54,7 @@ from .view import GlobalView, IssueView, IssueViewFavorite
|
|||||||
|
|
||||||
from .module import Module, ModuleMember, ModuleIssue, ModuleLink, ModuleFavorite
|
from .module import Module, ModuleMember, ModuleIssue, ModuleLink, ModuleFavorite
|
||||||
|
|
||||||
from .api_token import APIToken
|
from .api import APIToken, APIActivityLog
|
||||||
|
|
||||||
from .integration import (
|
from .integration import (
|
||||||
WorkspaceIntegration,
|
WorkspaceIntegration,
|
||||||
@ -68,7 +68,7 @@ from .integration import (
|
|||||||
|
|
||||||
from .importer import Importer
|
from .importer import Importer
|
||||||
|
|
||||||
from .page import Page, PageBlock, PageFavorite, PageLabel
|
from .page import Page, PageLog, PageFavorite, PageLabel
|
||||||
|
|
||||||
from .estimate import Estimate, EstimatePoint
|
from .estimate import Estimate, EstimatePoint
|
||||||
|
|
||||||
@ -79,3 +79,5 @@ from .analytic import AnalyticView
|
|||||||
from .notification import Notification
|
from .notification import Notification
|
||||||
|
|
||||||
from .exporter import ExporterHistory
|
from .exporter import ExporterHistory
|
||||||
|
|
||||||
|
from .webhook import Webhook, WebhookLog
|
||||||
|
80
apiserver/plane/db/models/api.py
Normal file
80
apiserver/plane/db/models/api.py
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
# Python imports
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
# Django imports
|
||||||
|
from django.db import models
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
from .base import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
def generate_label_token():
|
||||||
|
return uuid4().hex
|
||||||
|
|
||||||
|
|
||||||
|
def generate_token():
|
||||||
|
return "plane_api_" + uuid4().hex
|
||||||
|
|
||||||
|
|
||||||
|
class APIToken(BaseModel):
|
||||||
|
# Meta information
|
||||||
|
label = models.CharField(max_length=255, default=generate_label_token)
|
||||||
|
description = models.TextField(blank=True)
|
||||||
|
is_active = models.BooleanField(default=True)
|
||||||
|
last_used = models.DateTimeField(null=True)
|
||||||
|
|
||||||
|
# Token
|
||||||
|
token = models.CharField(
|
||||||
|
max_length=255, unique=True, default=generate_token, db_index=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# User Information
|
||||||
|
user = models.ForeignKey(
|
||||||
|
settings.AUTH_USER_MODEL,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="bot_tokens",
|
||||||
|
)
|
||||||
|
user_type = models.PositiveSmallIntegerField(
|
||||||
|
choices=((0, "Human"), (1, "Bot")), default=0
|
||||||
|
)
|
||||||
|
workspace = models.ForeignKey(
|
||||||
|
"db.Workspace", related_name="api_tokens", on_delete=models.CASCADE, null=True
|
||||||
|
)
|
||||||
|
expired_at = models.DateTimeField(blank=True, null=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "API Token"
|
||||||
|
verbose_name_plural = "API Tokems"
|
||||||
|
db_table = "api_tokens"
|
||||||
|
ordering = ("-created_at",)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return str(self.user.id)
|
||||||
|
|
||||||
|
|
||||||
|
class APIActivityLog(BaseModel):
|
||||||
|
token_identifier = models.CharField(max_length=255)
|
||||||
|
|
||||||
|
# Request Info
|
||||||
|
path = models.CharField(max_length=255)
|
||||||
|
method = models.CharField(max_length=10)
|
||||||
|
query_params = models.TextField(null=True, blank=True)
|
||||||
|
headers = models.TextField(null=True, blank=True)
|
||||||
|
body = models.TextField(null=True, blank=True)
|
||||||
|
|
||||||
|
# Response info
|
||||||
|
response_code = models.PositiveIntegerField()
|
||||||
|
response_body = models.TextField(null=True, blank=True)
|
||||||
|
|
||||||
|
# Meta information
|
||||||
|
ip_address = models.GenericIPAddressField(null=True, blank=True)
|
||||||
|
user_agent = models.CharField(max_length=512, null=True, blank=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "API Activity Log"
|
||||||
|
verbose_name_plural = "API Activity Logs"
|
||||||
|
db_table = "api_activity_logs"
|
||||||
|
ordering = ("-created_at",)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return str(self.token_identifier)
|
@ -1,41 +0,0 @@
|
|||||||
# Python imports
|
|
||||||
from uuid import uuid4
|
|
||||||
|
|
||||||
# Django imports
|
|
||||||
from django.db import models
|
|
||||||
from django.conf import settings
|
|
||||||
|
|
||||||
from .base import BaseModel
|
|
||||||
|
|
||||||
|
|
||||||
def generate_label_token():
|
|
||||||
return uuid4().hex
|
|
||||||
|
|
||||||
|
|
||||||
def generate_token():
|
|
||||||
return uuid4().hex + uuid4().hex
|
|
||||||
|
|
||||||
|
|
||||||
class APIToken(BaseModel):
|
|
||||||
token = models.CharField(max_length=255, unique=True, default=generate_token)
|
|
||||||
label = models.CharField(max_length=255, default=generate_label_token)
|
|
||||||
user = models.ForeignKey(
|
|
||||||
settings.AUTH_USER_MODEL,
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
related_name="bot_tokens",
|
|
||||||
)
|
|
||||||
user_type = models.PositiveSmallIntegerField(
|
|
||||||
choices=((0, "Human"), (1, "Bot")), default=0
|
|
||||||
)
|
|
||||||
workspace = models.ForeignKey(
|
|
||||||
"db.Workspace", related_name="api_tokens", on_delete=models.CASCADE, null=True
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = "API Token"
|
|
||||||
verbose_name_plural = "API Tokems"
|
|
||||||
db_table = "api_tokens"
|
|
||||||
ordering = ("-created_at",)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return str(self.user.name)
|
|
@ -132,25 +132,7 @@ class Issue(ProjectBaseModel):
|
|||||||
self.state = default_state
|
self.state = default_state
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
else:
|
|
||||||
try:
|
|
||||||
from plane.db.models import State, PageBlock
|
|
||||||
|
|
||||||
# Check if the current issue state and completed state id are same
|
|
||||||
if self.state.group == "completed":
|
|
||||||
self.completed_at = timezone.now()
|
|
||||||
# check if there are any page blocks
|
|
||||||
PageBlock.objects.filter(issue_id=self.id).filter().update(
|
|
||||||
completed_at=timezone.now()
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
PageBlock.objects.filter(issue_id=self.id).filter().update(
|
|
||||||
completed_at=None
|
|
||||||
)
|
|
||||||
self.completed_at = None
|
|
||||||
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
if self._state.adding:
|
if self._state.adding:
|
||||||
# Get the maximum display_id value from the database
|
# Get the maximum display_id value from the database
|
||||||
last_id = IssueSequence.objects.filter(project=self.project).aggregate(
|
last_id = IssueSequence.objects.filter(project=self.project).aggregate(
|
||||||
@ -431,6 +413,7 @@ class Label(ProjectBaseModel):
|
|||||||
name = models.CharField(max_length=255)
|
name = models.CharField(max_length=255)
|
||||||
description = models.TextField(blank=True)
|
description = models.TextField(blank=True)
|
||||||
color = models.CharField(max_length=255, blank=True)
|
color = models.CharField(max_length=255, blank=True)
|
||||||
|
sort_order = models.FloatField(default=65535)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = ["name", "project"]
|
unique_together = ["name", "project"]
|
||||||
@ -439,6 +422,18 @@ class Label(ProjectBaseModel):
|
|||||||
db_table = "labels"
|
db_table = "labels"
|
||||||
ordering = ("-created_at",)
|
ordering = ("-created_at",)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if self._state.adding:
|
||||||
|
# Get the maximum sequence value from the database
|
||||||
|
last_id = Label.objects.filter(project=self.project).aggregate(
|
||||||
|
largest=models.Max("sort_order")
|
||||||
|
)["largest"]
|
||||||
|
# if last_id is not None
|
||||||
|
if last_id is not None:
|
||||||
|
self.sort_order = last_id + 10000
|
||||||
|
|
||||||
|
super(Label, self).save(*args, **kwargs)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return str(self.name)
|
return str(self.name)
|
||||||
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import uuid
|
||||||
|
|
||||||
# Django imports
|
# Django imports
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@ -22,6 +24,15 @@ class Page(ProjectBaseModel):
|
|||||||
labels = models.ManyToManyField(
|
labels = models.ManyToManyField(
|
||||||
"db.Label", blank=True, related_name="pages", through="db.PageLabel"
|
"db.Label", blank=True, related_name="pages", through="db.PageLabel"
|
||||||
)
|
)
|
||||||
|
parent = models.ForeignKey(
|
||||||
|
"self",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
related_name="child_page",
|
||||||
|
)
|
||||||
|
archived_at = models.DateField(null=True)
|
||||||
|
is_locked = models.BooleanField(default=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Page"
|
verbose_name = "Page"
|
||||||
@ -34,6 +45,42 @@ class Page(ProjectBaseModel):
|
|||||||
return f"{self.owned_by.email} <{self.name}>"
|
return f"{self.owned_by.email} <{self.name}>"
|
||||||
|
|
||||||
|
|
||||||
|
class PageLog(ProjectBaseModel):
|
||||||
|
TYPE_CHOICES = (
|
||||||
|
("to_do", "To Do"),
|
||||||
|
("issue", "issue"),
|
||||||
|
("image", "Image"),
|
||||||
|
("video", "Video"),
|
||||||
|
("file", "File"),
|
||||||
|
("link", "Link"),
|
||||||
|
("cycle","Cycle"),
|
||||||
|
("module", "Module"),
|
||||||
|
("back_link", "Back Link"),
|
||||||
|
("forward_link", "Forward Link"),
|
||||||
|
("mention", "Mention"),
|
||||||
|
)
|
||||||
|
transaction = models.UUIDField(default=uuid.uuid4)
|
||||||
|
page = models.ForeignKey(
|
||||||
|
Page, related_name="page_log", on_delete=models.CASCADE
|
||||||
|
)
|
||||||
|
entity_identifier = models.UUIDField(null=True)
|
||||||
|
entity_name = models.CharField(
|
||||||
|
max_length=30,
|
||||||
|
choices=TYPE_CHOICES,
|
||||||
|
verbose_name="Transaction Type",
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ["page", "transaction"]
|
||||||
|
verbose_name = "Page Log"
|
||||||
|
verbose_name_plural = "Page Logs"
|
||||||
|
db_table = "page_logs"
|
||||||
|
ordering = ("-created_at",)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.page.name} {self.type}"
|
||||||
|
|
||||||
|
|
||||||
class PageBlock(ProjectBaseModel):
|
class PageBlock(ProjectBaseModel):
|
||||||
page = models.ForeignKey("db.Page", on_delete=models.CASCADE, related_name="blocks")
|
page = models.ForeignKey("db.Page", on_delete=models.CASCADE, related_name="blocks")
|
||||||
name = models.CharField(max_length=255)
|
name = models.CharField(max_length=255)
|
||||||
|
@ -166,6 +166,7 @@ class ProjectMember(ProjectBaseModel):
|
|||||||
default_props = models.JSONField(default=get_default_props)
|
default_props = models.JSONField(default=get_default_props)
|
||||||
preferences = models.JSONField(default=get_default_preferences)
|
preferences = models.JSONField(default=get_default_preferences)
|
||||||
sort_order = models.FloatField(default=65535)
|
sort_order = models.FloatField(default=65535)
|
||||||
|
is_active = models.BooleanField(default=True)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
if self._state.adding:
|
if self._state.adding:
|
||||||
|
90
apiserver/plane/db/models/webhook.py
Normal file
90
apiserver/plane/db/models/webhook.py
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
# Python imports
|
||||||
|
from uuid import uuid4
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
# Django imports
|
||||||
|
from django.db import models
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
|
# Module imports
|
||||||
|
from plane.db.models import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
def generate_token():
|
||||||
|
return "plane_wh_" + uuid4().hex
|
||||||
|
|
||||||
|
|
||||||
|
def validate_schema(value):
|
||||||
|
parsed_url = urlparse(value)
|
||||||
|
print(parsed_url)
|
||||||
|
if parsed_url.scheme not in ["http", "https"]:
|
||||||
|
raise ValidationError("Invalid schema. Only HTTP and HTTPS are allowed.")
|
||||||
|
|
||||||
|
|
||||||
|
def validate_domain(value):
|
||||||
|
parsed_url = urlparse(value)
|
||||||
|
domain = parsed_url.netloc
|
||||||
|
if domain in ["localhost", "127.0.0.1"]:
|
||||||
|
raise ValidationError("Local URLs are not allowed.")
|
||||||
|
|
||||||
|
|
||||||
|
class Webhook(BaseModel):
|
||||||
|
workspace = models.ForeignKey(
|
||||||
|
"db.Workspace",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="workspace_webhooks",
|
||||||
|
)
|
||||||
|
url = models.URLField(
|
||||||
|
validators=[
|
||||||
|
validate_schema,
|
||||||
|
validate_domain,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
is_active = models.BooleanField(default=True)
|
||||||
|
secret_key = models.CharField(max_length=255, default=generate_token)
|
||||||
|
project = models.BooleanField(default=False)
|
||||||
|
issue = models.BooleanField(default=False)
|
||||||
|
module = models.BooleanField(default=False)
|
||||||
|
cycle = models.BooleanField(default=False)
|
||||||
|
issue_comment = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.workspace.slug} {self.url}"
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ["workspace", "url"]
|
||||||
|
verbose_name = "Webhook"
|
||||||
|
verbose_name_plural = "Webhooks"
|
||||||
|
db_table = "webhooks"
|
||||||
|
ordering = ("-created_at",)
|
||||||
|
|
||||||
|
|
||||||
|
class WebhookLog(BaseModel):
|
||||||
|
workspace = models.ForeignKey(
|
||||||
|
"db.Workspace", on_delete=models.CASCADE, related_name="webhook_logs"
|
||||||
|
)
|
||||||
|
# Associated webhook
|
||||||
|
webhook = models.ForeignKey(Webhook, on_delete=models.CASCADE, related_name="logs")
|
||||||
|
|
||||||
|
# Basic request details
|
||||||
|
event_type = models.CharField(max_length=255, blank=True, null=True)
|
||||||
|
request_method = models.CharField(max_length=10, blank=True, null=True)
|
||||||
|
request_headers = models.TextField(blank=True, null=True)
|
||||||
|
request_body = models.TextField(blank=True, null=True)
|
||||||
|
|
||||||
|
# Response details
|
||||||
|
response_status = models.TextField(blank=True, null=True)
|
||||||
|
response_headers = models.TextField(blank=True, null=True)
|
||||||
|
response_body = models.TextField(blank=True, null=True)
|
||||||
|
|
||||||
|
# Retry Count
|
||||||
|
retry_count = models.PositiveSmallIntegerField(default=0)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Webhook Log"
|
||||||
|
verbose_name_plural = "Webhook Logs"
|
||||||
|
db_table = "webhook_logs"
|
||||||
|
ordering = ("-created_at",)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.event_type} {str(self.webhook.url)}"
|
@ -99,6 +99,7 @@ class WorkspaceMember(BaseModel):
|
|||||||
view_props = models.JSONField(default=get_default_props)
|
view_props = models.JSONField(default=get_default_props)
|
||||||
default_props = models.JSONField(default=get_default_props)
|
default_props = models.JSONField(default=get_default_props)
|
||||||
issue_props = models.JSONField(default=get_issue_props)
|
issue_props = models.JSONField(default=get_issue_props)
|
||||||
|
is_active = models.BooleanField(default=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = ["workspace", "member"]
|
unique_together = ["workspace", "member"]
|
||||||
|
0
apiserver/plane/license/__init__.py
Normal file
0
apiserver/plane/license/__init__.py
Normal file
0
apiserver/plane/license/api/__init__.py
Normal file
0
apiserver/plane/license/api/__init__.py
Normal file
1
apiserver/plane/license/api/permissions/__init__.py
Normal file
1
apiserver/plane/license/api/permissions/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .instance import InstanceOwnerPermission, InstanceAdminPermission
|
33
apiserver/plane/license/api/permissions/instance.py
Normal file
33
apiserver/plane/license/api/permissions/instance.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# Third party imports
|
||||||
|
from rest_framework.permissions import BasePermission
|
||||||
|
|
||||||
|
# Module imports
|
||||||
|
from plane.license.models import Instance, InstanceAdmin
|
||||||
|
|
||||||
|
|
||||||
|
class InstanceOwnerPermission(BasePermission):
|
||||||
|
def has_permission(self, request, view):
|
||||||
|
|
||||||
|
if request.user.is_anonymous:
|
||||||
|
return False
|
||||||
|
|
||||||
|
instance = Instance.objects.first()
|
||||||
|
return InstanceAdmin.objects.filter(
|
||||||
|
role=20,
|
||||||
|
instance=instance,
|
||||||
|
user=request.user,
|
||||||
|
).exists()
|
||||||
|
|
||||||
|
|
||||||
|
class InstanceAdminPermission(BasePermission):
|
||||||
|
def has_permission(self, request, view):
|
||||||
|
|
||||||
|
if request.user.is_anonymous:
|
||||||
|
return False
|
||||||
|
|
||||||
|
instance = Instance.objects.first()
|
||||||
|
return InstanceAdmin.objects.filter(
|
||||||
|
role__gte=15,
|
||||||
|
instance=instance,
|
||||||
|
user=request.user,
|
||||||
|
).exists()
|
1
apiserver/plane/license/api/serializers/__init__.py
Normal file
1
apiserver/plane/license/api/serializers/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .instance import InstanceSerializer, InstanceAdminSerializer, InstanceConfigurationSerializer
|
42
apiserver/plane/license/api/serializers/instance.py
Normal file
42
apiserver/plane/license/api/serializers/instance.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
# Module imports
|
||||||
|
from plane.license.models import Instance, InstanceAdmin, InstanceConfiguration
|
||||||
|
from plane.api.serializers import BaseSerializer
|
||||||
|
from plane.api.serializers import UserAdminLiteSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class InstanceSerializer(BaseSerializer):
|
||||||
|
primary_owner_details = UserAdminLiteSerializer(source="primary_owner", read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Instance
|
||||||
|
fields = "__all__"
|
||||||
|
read_only_fields = [
|
||||||
|
"id",
|
||||||
|
"primary_owner",
|
||||||
|
"primary_email",
|
||||||
|
"instance_id",
|
||||||
|
"license_key",
|
||||||
|
"api_key",
|
||||||
|
"version",
|
||||||
|
"email",
|
||||||
|
"last_checked_at",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class InstanceAdminSerializer(BaseSerializer):
|
||||||
|
user_detail = UserAdminLiteSerializer(source="user", read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = InstanceAdmin
|
||||||
|
fields = "__all__"
|
||||||
|
read_only_fields = [
|
||||||
|
"id",
|
||||||
|
"instance",
|
||||||
|
"user",
|
||||||
|
]
|
||||||
|
|
||||||
|
class InstanceConfigurationSerializer(BaseSerializer):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = InstanceConfiguration
|
||||||
|
fields = "__all__"
|
6
apiserver/plane/license/api/views/__init__.py
Normal file
6
apiserver/plane/license/api/views/__init__.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from .instance import (
|
||||||
|
InstanceEndpoint,
|
||||||
|
TransferPrimaryOwnerEndpoint,
|
||||||
|
InstanceAdminEndpoint,
|
||||||
|
InstanceConfigurationEndpoint,
|
||||||
|
)
|
242
apiserver/plane/license/api/views/instance.py
Normal file
242
apiserver/plane/license/api/views/instance.py
Normal file
@ -0,0 +1,242 @@
|
|||||||
|
# Python imports
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import requests
|
||||||
|
|
||||||
|
# Django imports
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
# Third party imports
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
# Module imports
|
||||||
|
from plane.api.views import BaseAPIView
|
||||||
|
from plane.license.models import Instance, InstanceAdmin, InstanceConfiguration
|
||||||
|
from plane.license.api.serializers import (
|
||||||
|
InstanceSerializer,
|
||||||
|
InstanceAdminSerializer,
|
||||||
|
InstanceConfigurationSerializer,
|
||||||
|
)
|
||||||
|
from plane.license.api.permissions import (
|
||||||
|
InstanceOwnerPermission,
|
||||||
|
InstanceAdminPermission,
|
||||||
|
)
|
||||||
|
from plane.db.models import User
|
||||||
|
|
||||||
|
|
||||||
|
class InstanceEndpoint(BaseAPIView):
|
||||||
|
def get_permissions(self):
|
||||||
|
if self.request.method in ["POST", "PATCH"]:
|
||||||
|
self.permission_classes = [
|
||||||
|
InstanceOwnerPermission,
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
self.permission_classes = [
|
||||||
|
InstanceAdminPermission,
|
||||||
|
]
|
||||||
|
return super(InstanceEndpoint, self).get_permissions()
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
# Check if the instance is registered
|
||||||
|
instance = Instance.objects.first()
|
||||||
|
|
||||||
|
# If instance is None then register this instance
|
||||||
|
if instance is None:
|
||||||
|
with open("package.json", "r") as file:
|
||||||
|
# Load JSON content from the file
|
||||||
|
data = json.load(file)
|
||||||
|
|
||||||
|
license_engine_base_url = os.environ.get("LICENSE_ENGINE_BASE_URL")
|
||||||
|
|
||||||
|
if not license_engine_base_url:
|
||||||
|
raise Response(
|
||||||
|
{"error": "LICENSE_ENGINE_BASE_URL is required"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
headers = {"Content-Type": "application/json"}
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"email": request.user.email,
|
||||||
|
"version": data.get("version", 0.1),
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(
|
||||||
|
f"{license_engine_base_url}/api/instances",
|
||||||
|
headers=headers,
|
||||||
|
data=json.dumps(payload),
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code == 201:
|
||||||
|
data = response.json()
|
||||||
|
# Create instance
|
||||||
|
instance = Instance.objects.create(
|
||||||
|
instance_name="Plane Free",
|
||||||
|
instance_id=data.get("id"),
|
||||||
|
license_key=data.get("license_key"),
|
||||||
|
api_key=data.get("api_key"),
|
||||||
|
version=data.get("version"),
|
||||||
|
primary_email=data.get("email"),
|
||||||
|
primary_owner=request.user,
|
||||||
|
last_checked_at=timezone.now(),
|
||||||
|
)
|
||||||
|
# Create instance admin
|
||||||
|
_ = InstanceAdmin.objects.create(
|
||||||
|
user=request.user,
|
||||||
|
instance=instance,
|
||||||
|
role=20,
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"message": f"Instance succesfully registered with owner: {instance.primary_owner.email}"
|
||||||
|
},
|
||||||
|
status=status.HTTP_201_CREATED,
|
||||||
|
)
|
||||||
|
return Response(
|
||||||
|
{"error": "Instance could not be registered"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"message": f"Instance already registered with instance owner: {instance.primary_owner.email}"
|
||||||
|
},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
instance = Instance.objects.first()
|
||||||
|
# get the instance
|
||||||
|
if instance is None:
|
||||||
|
return Response({"activated": False}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
# Return instance
|
||||||
|
serializer = InstanceSerializer(instance)
|
||||||
|
serializer.data["activated"] = True
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
def patch(self, request):
|
||||||
|
# Get the instance
|
||||||
|
instance = Instance.objects.first()
|
||||||
|
serializer = InstanceSerializer(instance, data=request.data, partial=True)
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save()
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
|
class TransferPrimaryOwnerEndpoint(BaseAPIView):
|
||||||
|
permission_classes = [
|
||||||
|
InstanceOwnerPermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
# Transfer the owner of the instance
|
||||||
|
def post(self, request):
|
||||||
|
instance = Instance.objects.first()
|
||||||
|
|
||||||
|
# Get the email of the new user
|
||||||
|
email = request.data.get("email", False)
|
||||||
|
if not email:
|
||||||
|
return Response(
|
||||||
|
{"error": "User is required"}, status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get users
|
||||||
|
user = User.objects.get(email=email)
|
||||||
|
|
||||||
|
# Save the instance user
|
||||||
|
instance.primary_owner = user
|
||||||
|
instance.primary_email = user.email
|
||||||
|
instance.save(update_fields=["owner", "email"])
|
||||||
|
|
||||||
|
# Add the user to admin
|
||||||
|
_ = InstanceAdmin.objects.get_or_create(
|
||||||
|
instance=instance,
|
||||||
|
user=user,
|
||||||
|
role=20,
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
{"message": "Owner successfully updated"}, status=status.HTTP_200_OK
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class InstanceAdminEndpoint(BaseAPIView):
|
||||||
|
def get_permissions(self):
|
||||||
|
if self.request.method in ["POST", "DELETE"]:
|
||||||
|
self.permission_classes = [
|
||||||
|
InstanceOwnerPermission,
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
self.permission_classes = [
|
||||||
|
InstanceAdminPermission,
|
||||||
|
]
|
||||||
|
return super(InstanceAdminEndpoint, self).get_permissions()
|
||||||
|
|
||||||
|
# Create an instance admin
|
||||||
|
def post(self, request):
|
||||||
|
email = request.data.get("email", False)
|
||||||
|
role = request.data.get("role", 15)
|
||||||
|
|
||||||
|
if not email:
|
||||||
|
return Response(
|
||||||
|
{"error": "Email is required"}, status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
instance = Instance.objects.first()
|
||||||
|
if instance is None:
|
||||||
|
return Response(
|
||||||
|
{"error": "Instance is not registered yet"},
|
||||||
|
status=status.HTTP_403_FORBIDDEN,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Fetch the user
|
||||||
|
user = User.objects.get(email=email)
|
||||||
|
|
||||||
|
instance_admin = InstanceAdmin.objects.create(
|
||||||
|
instance=instance,
|
||||||
|
user=user,
|
||||||
|
role=role,
|
||||||
|
)
|
||||||
|
serializer = InstanceAdminSerializer(instance_admin)
|
||||||
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
instance = Instance.objects.first()
|
||||||
|
if instance is None:
|
||||||
|
return Response(
|
||||||
|
{"error": "Instance is not registered yet"},
|
||||||
|
status=status.HTTP_403_FORBIDDEN,
|
||||||
|
)
|
||||||
|
instance_admins = InstanceAdmin.objects.filter(instance=instance)
|
||||||
|
serializer = InstanceAdminSerializer(instance_admins, many=True)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
def delete(self, request, pk):
|
||||||
|
instance = Instance.objects.first()
|
||||||
|
instance_admin = InstanceAdmin.objects.filter(instance=instance, pk=pk).delete()
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
|
class InstanceConfigurationEndpoint(BaseAPIView):
|
||||||
|
permission_classes = [
|
||||||
|
InstanceAdminPermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
instance_configurations = InstanceConfiguration.objects.all()
|
||||||
|
serializer = InstanceConfigurationSerializer(instance_configurations, many=True)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
def patch(self, request):
|
||||||
|
key = request.data.get("key", False)
|
||||||
|
if not key:
|
||||||
|
return Response(
|
||||||
|
{"error": "Key is required"}, status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
configuration = InstanceConfiguration.objects.get(key=key)
|
||||||
|
configuration.value = request.data.get("value")
|
||||||
|
configuration.save()
|
||||||
|
serializer = InstanceConfigurationSerializer(configuration)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
5
apiserver/plane/license/apps.py
Normal file
5
apiserver/plane/license/apps.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class LicenseConfig(AppConfig):
|
||||||
|
name = "plane.license"
|
0
apiserver/plane/license/management/__init__.py
Normal file
0
apiserver/plane/license/management/__init__.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
# Python imports
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Django imports
|
||||||
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
# Module imports
|
||||||
|
from plane.license.models import InstanceConfiguration
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = "Configure instance variables"
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
config_keys = {
|
||||||
|
# Authentication Settings
|
||||||
|
"GOOGLE_CLIENT_ID": os.environ.get("GOOGLE_CLIENT_ID"),
|
||||||
|
"GITHUB_CLIENT_ID": os.environ.get("GITHUB_CLIENT_ID"),
|
||||||
|
"GITHUB_CLIENT_SECRET": os.environ.get("GITHUB_CLIENT_SECRET"),
|
||||||
|
"ENABLE_SIGNUP": os.environ.get("ENABLE_SIGNUP", "1"),
|
||||||
|
"ENABLE_EMAIL_PASSWORD": os.environ.get("ENABLE_EMAIL_PASSWORD", "1"),
|
||||||
|
"ENABLE_MAGIC_LINK_LOGIN": os.environ.get("ENABLE_MAGIC_LINK_LOGIN", "0"),
|
||||||
|
# Email Settings
|
||||||
|
"EMAIL_HOST": os.environ.get("EMAIL_HOST", ""),
|
||||||
|
"EMAIL_HOST_USER": os.environ.get("EMAIL_HOST_USER", ""),
|
||||||
|
"EMAIL_HOST_PASSWORD": os.environ.get("EMAIL_HOST_PASSWORD"),
|
||||||
|
"EMAIL_PORT": os.environ.get("EMAIL_PORT", "587"),
|
||||||
|
"EMAIL_FROM": os.environ.get("EMAIL_FROM", ""),
|
||||||
|
"EMAIL_USE_TLS": os.environ.get("EMAIL_USE_TLS", "1"),
|
||||||
|
"EMAIL_USE_SSL": os.environ.get("EMAIL_USE_SSL", "0"),
|
||||||
|
# Open AI Settings
|
||||||
|
"OPENAI_API_BASE": os.environ.get("", "https://api.openai.com/v1"),
|
||||||
|
"OPENAI_API_KEY": os.environ.get("OPENAI_API_KEY", "sk-"),
|
||||||
|
"GPT_ENGINE": os.environ.get("GPT_ENGINE", "gpt-3.5-turbo"),
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value in config_keys.items():
|
||||||
|
obj, created = InstanceConfiguration.objects.get_or_create(
|
||||||
|
key=key
|
||||||
|
)
|
||||||
|
if created:
|
||||||
|
obj.value = value
|
||||||
|
obj.save()
|
||||||
|
self.stdout.write(self.style.SUCCESS(f"{key} loaded with value from environment variable."))
|
||||||
|
else:
|
||||||
|
self.stdout.write(self.style.WARNING(f"{key} configuration already exists"))
|
104
apiserver/plane/license/management/commands/register_instance.py
Normal file
104
apiserver/plane/license/management/commands/register_instance.py
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
# Python imports
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import requests
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
# Django imports
|
||||||
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.core.validators import validate_email
|
||||||
|
|
||||||
|
# Module imports
|
||||||
|
from plane.db.models import User
|
||||||
|
from plane.license.models import Instance, InstanceAdmin
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = "Check if instance in registered else register"
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
# Check if the instance is registered
|
||||||
|
instance = Instance.objects.first()
|
||||||
|
|
||||||
|
# If instance is None then register this instance
|
||||||
|
if instance is None:
|
||||||
|
with open("package.json", "r") as file:
|
||||||
|
# Load JSON content from the file
|
||||||
|
data = json.load(file)
|
||||||
|
|
||||||
|
admin_email = os.environ.get("ADMIN_EMAIL")
|
||||||
|
|
||||||
|
try:
|
||||||
|
validate_email(admin_email)
|
||||||
|
except ValidationError:
|
||||||
|
CommandError(f"{admin_email} is not a valid ADMIN_EMAIL")
|
||||||
|
|
||||||
|
# Raise an exception if the admin email is not provided
|
||||||
|
if not admin_email:
|
||||||
|
raise CommandError("ADMIN_EMAIL is required")
|
||||||
|
|
||||||
|
# Check if the admin email user exists
|
||||||
|
user = User.objects.filter(email=admin_email).first()
|
||||||
|
|
||||||
|
# If the user does not exist create the user and add him to the database
|
||||||
|
if user is None:
|
||||||
|
user = User.objects.create(email=admin_email, username=uuid.uuid4().hex)
|
||||||
|
user.set_password(uuid.uuid4().hex)
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
license_engine_base_url = os.environ.get("LICENSE_ENGINE_BASE_URL")
|
||||||
|
|
||||||
|
if not license_engine_base_url:
|
||||||
|
raise CommandError("LICENSE_ENGINE_BASE_URL is required")
|
||||||
|
|
||||||
|
headers = {"Content-Type": "application/json"}
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"email": user.email,
|
||||||
|
"version": data.get("version", 0.1),
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(
|
||||||
|
f"{license_engine_base_url}/api/instances",
|
||||||
|
headers=headers,
|
||||||
|
data=json.dumps(payload),
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code == 201:
|
||||||
|
data = response.json()
|
||||||
|
# Create instance
|
||||||
|
instance = Instance.objects.create(
|
||||||
|
instance_name="Plane Free",
|
||||||
|
instance_id=data.get("id"),
|
||||||
|
license_key=data.get("license_key"),
|
||||||
|
api_key=data.get("api_key"),
|
||||||
|
version=data.get("version"),
|
||||||
|
primary_email=data.get("email"),
|
||||||
|
primary_owner=user,
|
||||||
|
last_checked_at=timezone.now(),
|
||||||
|
)
|
||||||
|
# Create instance admin
|
||||||
|
_ = InstanceAdmin.objects.create(
|
||||||
|
user=user,
|
||||||
|
instance=instance,
|
||||||
|
role=20,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.SUCCESS(
|
||||||
|
f"Instance succesfully registered with owner: {instance.primary_owner.email}"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.stdout.write(self.style.WARNING("Instance could not be registered"))
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.SUCCESS(
|
||||||
|
f"Instance already registered with instance owner: {instance.primary_owner.email}"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
83
apiserver/plane/license/migrations/0001_initial.py
Normal file
83
apiserver/plane/license/migrations/0001_initial.py
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
# Generated by Django 4.2.5 on 2023-11-15 14:22
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Instance',
|
||||||
|
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)),
|
||||||
|
('instance_name', models.CharField(max_length=255)),
|
||||||
|
('whitelist_emails', models.TextField(blank=True, null=True)),
|
||||||
|
('instance_id', models.CharField(max_length=25, unique=True)),
|
||||||
|
('license_key', models.CharField(blank=True, max_length=256, null=True)),
|
||||||
|
('api_key', models.CharField(max_length=16)),
|
||||||
|
('version', models.CharField(max_length=10)),
|
||||||
|
('primary_email', models.CharField(max_length=256)),
|
||||||
|
('last_checked_at', models.DateTimeField()),
|
||||||
|
('namespace', models.CharField(blank=True, max_length=50, null=True)),
|
||||||
|
('is_telemetry_enabled', models.BooleanField(default=True)),
|
||||||
|
('is_support_required', models.BooleanField(default=True)),
|
||||||
|
('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')),
|
||||||
|
('primary_owner', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='instance_primary_owner', to=settings.AUTH_USER_MODEL)),
|
||||||
|
('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')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Instance',
|
||||||
|
'verbose_name_plural': 'Instances',
|
||||||
|
'db_table': 'instances',
|
||||||
|
'ordering': ('-created_at',),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='InstanceConfiguration',
|
||||||
|
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)),
|
||||||
|
('key', models.CharField(max_length=100, unique=True)),
|
||||||
|
('value', models.TextField(blank=True, default=None, null=True)),
|
||||||
|
('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')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Instance Configuration',
|
||||||
|
'verbose_name_plural': 'Instance Configurations',
|
||||||
|
'db_table': 'instance_configurations',
|
||||||
|
'ordering': ('-created_at',),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='InstanceAdmin',
|
||||||
|
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)),
|
||||||
|
('role', models.PositiveIntegerField(choices=[(20, 'Owner'), (15, 'Admin')], default=15)),
|
||||||
|
('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')),
|
||||||
|
('instance', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='admins', to='license.instance')),
|
||||||
|
('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')),
|
||||||
|
('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='instance_owner', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Instance Admin',
|
||||||
|
'verbose_name_plural': 'Instance Admins',
|
||||||
|
'db_table': 'instance_admins',
|
||||||
|
'ordering': ('-created_at',),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 4.2.5 on 2023-11-16 09:45
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('license', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='instanceadmin',
|
||||||
|
unique_together={('instance', 'user')},
|
||||||
|
),
|
||||||
|
]
|
0
apiserver/plane/license/migrations/__init__.py
Normal file
0
apiserver/plane/license/migrations/__init__.py
Normal file
1
apiserver/plane/license/models/__init__.py
Normal file
1
apiserver/plane/license/models/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .instance import Instance, InstanceAdmin, InstanceConfiguration
|
73
apiserver/plane/license/models/instance.py
Normal file
73
apiserver/plane/license/models/instance.py
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
# Django imports
|
||||||
|
from django.db import models
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
# Module imports
|
||||||
|
from plane.db.models import BaseModel
|
||||||
|
from plane.db.mixins import AuditModel
|
||||||
|
|
||||||
|
ROLE_CHOICES = (
|
||||||
|
(20, "Owner"),
|
||||||
|
(15, "Admin"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Instance(BaseModel):
|
||||||
|
# General informations
|
||||||
|
instance_name = models.CharField(max_length=255)
|
||||||
|
whitelist_emails = models.TextField(blank=True, null=True)
|
||||||
|
instance_id = models.CharField(max_length=25, unique=True)
|
||||||
|
license_key = models.CharField(max_length=256, null=True, blank=True)
|
||||||
|
api_key = models.CharField(max_length=16)
|
||||||
|
version = models.CharField(max_length=10)
|
||||||
|
# User information
|
||||||
|
primary_email = models.CharField(max_length=256)
|
||||||
|
primary_owner = models.ForeignKey(
|
||||||
|
settings.AUTH_USER_MODEL,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
null=True,
|
||||||
|
related_name="instance_primary_owner",
|
||||||
|
)
|
||||||
|
# Instnace specifics
|
||||||
|
last_checked_at = models.DateTimeField()
|
||||||
|
namespace = models.CharField(max_length=50, blank=True, null=True)
|
||||||
|
# telemetry and support
|
||||||
|
is_telemetry_enabled = models.BooleanField(default=True)
|
||||||
|
is_support_required = models.BooleanField(default=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Instance"
|
||||||
|
verbose_name_plural = "Instances"
|
||||||
|
db_table = "instances"
|
||||||
|
ordering = ("-created_at",)
|
||||||
|
|
||||||
|
|
||||||
|
class InstanceAdmin(BaseModel):
|
||||||
|
user = models.ForeignKey(
|
||||||
|
settings.AUTH_USER_MODEL,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
null=True,
|
||||||
|
related_name="instance_owner",
|
||||||
|
)
|
||||||
|
instance = models.ForeignKey(Instance, on_delete=models.CASCADE, related_name="admins")
|
||||||
|
role = models.PositiveIntegerField(choices=ROLE_CHOICES, default=15)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ["instance", "user"]
|
||||||
|
verbose_name = "Instance Admin"
|
||||||
|
verbose_name_plural = "Instance Admins"
|
||||||
|
db_table = "instance_admins"
|
||||||
|
ordering = ("-created_at",)
|
||||||
|
|
||||||
|
|
||||||
|
class InstanceConfiguration(BaseModel):
|
||||||
|
# The instance configuration variables
|
||||||
|
key = models.CharField(max_length=100, unique=True)
|
||||||
|
value = models.TextField(null=True, blank=True, default=None)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Instance Configuration"
|
||||||
|
verbose_name_plural = "Instance Configurations"
|
||||||
|
db_table = "instance_configurations"
|
||||||
|
ordering = ("-created_at",)
|
||||||
|
|
36
apiserver/plane/license/urls.py
Normal file
36
apiserver/plane/license/urls.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from plane.license.api.views import (
|
||||||
|
InstanceEndpoint,
|
||||||
|
TransferPrimaryOwnerEndpoint,
|
||||||
|
InstanceAdminEndpoint,
|
||||||
|
InstanceConfigurationEndpoint,
|
||||||
|
)
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path(
|
||||||
|
"instances/",
|
||||||
|
InstanceEndpoint.as_view(),
|
||||||
|
name="instance",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"instances/transfer-primary-owner/",
|
||||||
|
TransferPrimaryOwnerEndpoint.as_view(),
|
||||||
|
name="instance",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"instances/admins/",
|
||||||
|
InstanceAdminEndpoint.as_view(),
|
||||||
|
name="instance-admins",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"instances/admins/<uuid:pk>/",
|
||||||
|
InstanceAdminEndpoint.as_view(),
|
||||||
|
name="instance-admins",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"instances/configurations/",
|
||||||
|
InstanceConfigurationEndpoint.as_view(),
|
||||||
|
name="instance-configuration",
|
||||||
|
),
|
||||||
|
]
|
0
apiserver/plane/license/utils/__init__.py
Normal file
0
apiserver/plane/license/utils/__init__.py
Normal file
6
apiserver/plane/license/utils/instance_value.py
Normal file
6
apiserver/plane/license/utils/instance_value.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# Helper function to return value from the passed key
|
||||||
|
def get_configuration_value(query, key, default=None):
|
||||||
|
for item in query:
|
||||||
|
if item['key'] == key:
|
||||||
|
return item.get("value", default)
|
||||||
|
return default
|
40
apiserver/plane/middleware/api_log_middleware.py
Normal file
40
apiserver/plane/middleware/api_log_middleware.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
from plane.db.models import APIToken, APIActivityLog
|
||||||
|
|
||||||
|
|
||||||
|
class APITokenLogMiddleware:
|
||||||
|
def __init__(self, get_response):
|
||||||
|
self.get_response = get_response
|
||||||
|
|
||||||
|
def __call__(self, request):
|
||||||
|
request_body = request.body
|
||||||
|
response = self.get_response(request)
|
||||||
|
self.process_request(request, response, request_body)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def process_request(self, request, response, request_body):
|
||||||
|
api_key_header = "X-Api-Key"
|
||||||
|
api_key = request.headers.get(api_key_header)
|
||||||
|
# If the API key is present, log the request
|
||||||
|
if api_key:
|
||||||
|
try:
|
||||||
|
APIActivityLog.objects.create(
|
||||||
|
token_identifier=api_key,
|
||||||
|
path=request.path,
|
||||||
|
method=request.method,
|
||||||
|
query_params=request.META.get("QUERY_STRING", ""),
|
||||||
|
headers=str(request.headers),
|
||||||
|
body=(request_body.decode('utf-8') if request_body else None),
|
||||||
|
response_body=(
|
||||||
|
response.content.decode("utf-8") if response.content else None
|
||||||
|
),
|
||||||
|
response_code=response.status_code,
|
||||||
|
ip_address=request.META.get("REMOTE_ADDR", None),
|
||||||
|
user_agent=request.META.get("HTTP_USER_AGENT", None),
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
# If the token does not exist, you can decide whether to log this as an invalid attempt
|
||||||
|
pass
|
||||||
|
|
||||||
|
return None
|
0
apiserver/plane/proxy/__init__.py
Normal file
0
apiserver/plane/proxy/__init__.py
Normal file
5
apiserver/plane/proxy/apps.py
Normal file
5
apiserver/plane/proxy/apps.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class ProxyConfig(AppConfig):
|
||||||
|
name = "plane.proxy"
|
45
apiserver/plane/proxy/rate_limit.py
Normal file
45
apiserver/plane/proxy/rate_limit.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
from django.utils import timezone
|
||||||
|
from rest_framework.throttling import SimpleRateThrottle
|
||||||
|
|
||||||
|
|
||||||
|
class ApiKeyRateThrottle(SimpleRateThrottle):
|
||||||
|
scope = 'api_key'
|
||||||
|
|
||||||
|
def get_cache_key(self, request, view):
|
||||||
|
# Retrieve the API key from the request header
|
||||||
|
api_key = request.headers.get('X-Api-Key')
|
||||||
|
if not api_key:
|
||||||
|
return None # Allow the request if there's no API key
|
||||||
|
|
||||||
|
# Use the API key as part of the cache key
|
||||||
|
return f'{self.scope}:{api_key}'
|
||||||
|
|
||||||
|
def allow_request(self, request, view):
|
||||||
|
# Calculate the current time as a Unix timestamp
|
||||||
|
now = timezone.now().timestamp()
|
||||||
|
|
||||||
|
# Use the parent class's method to check if the request is allowed
|
||||||
|
allowed = super().allow_request(request, view)
|
||||||
|
|
||||||
|
if allowed:
|
||||||
|
# Calculate the remaining limit and reset time
|
||||||
|
history = self.cache.get(self.key, [])
|
||||||
|
|
||||||
|
# Remove old histories
|
||||||
|
while history and history[-1] <= now - self.duration:
|
||||||
|
history.pop()
|
||||||
|
|
||||||
|
# Calculate the requests
|
||||||
|
num_requests = len(history)
|
||||||
|
|
||||||
|
# Check available requests
|
||||||
|
available = self.num_requests - num_requests
|
||||||
|
|
||||||
|
# Unix timestamp for when the rate limit will reset
|
||||||
|
reset_time = int(now + self.duration)
|
||||||
|
|
||||||
|
# Add headers
|
||||||
|
request.META['X-RateLimit-Remaining'] = max(0, available)
|
||||||
|
request.META['X-RateLimit-Reset'] = reset_time
|
||||||
|
|
||||||
|
return allowed
|
13
apiserver/plane/proxy/urls/__init__.py
Normal file
13
apiserver/plane/proxy/urls/__init__.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
from .cycle import urlpatterns as cycle_patterns
|
||||||
|
from .inbox import urlpatterns as inbox_patterns
|
||||||
|
from .issue import urlpatterns as issue_patterns
|
||||||
|
from .module import urlpatterns as module_patterns
|
||||||
|
from .project import urlpatterns as project_patterns
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
*cycle_patterns,
|
||||||
|
*inbox_patterns,
|
||||||
|
*issue_patterns,
|
||||||
|
*module_patterns,
|
||||||
|
*project_patterns,
|
||||||
|
]
|
35
apiserver/plane/proxy/urls/cycle.py
Normal file
35
apiserver/plane/proxy/urls/cycle.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from plane.proxy.views.cycle import (
|
||||||
|
CycleAPIEndpoint,
|
||||||
|
CycleIssueAPIEndpoint,
|
||||||
|
TransferCycleIssueAPIEndpoint,
|
||||||
|
)
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/",
|
||||||
|
CycleAPIEndpoint.as_view(),
|
||||||
|
name="cycles",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/",
|
||||||
|
CycleAPIEndpoint.as_view(),
|
||||||
|
name="cycles",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/cycle-issues/",
|
||||||
|
CycleIssueAPIEndpoint.as_view(),
|
||||||
|
name="cycle-issues",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/cycle-issues/<uuid:pk>/",
|
||||||
|
CycleIssueAPIEndpoint.as_view(),
|
||||||
|
name="cycle-issues",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/transfer-issues/",
|
||||||
|
TransferCycleIssueAPIEndpoint.as_view(),
|
||||||
|
name="transfer-issues",
|
||||||
|
),
|
||||||
|
]
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user