Compare commits
72
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
827a00c1d3 | ||
|
|
784be47e91 | ||
|
|
0fdd9c28bf | ||
|
|
644b06749b | ||
|
|
dd8c7a7487 | ||
|
|
e6a1f34713 | ||
|
|
1dff6b63f8 | ||
|
|
59dbbb29cd | ||
|
|
6cb3939835 | ||
|
|
021c0675b7 | ||
|
|
67000892e5 | ||
|
|
3df4794e77 | ||
|
|
42ccd1de58 | ||
|
|
c8c89007c0 | ||
|
|
4cf3e69e22 | ||
|
|
fb1f65c2c1 | ||
|
|
d91b4e6fa1 | ||
|
|
561223ea71 | ||
|
|
982eba0bd1 | ||
|
|
7aaf840fb1 | ||
|
|
15927c9cae | ||
|
|
d46d70fcd5 | ||
|
|
de581102e3 | ||
|
|
b903126e5a | ||
|
|
f44f70168f | ||
|
|
3c10f00b04 | ||
|
|
f1de05e4de | ||
|
|
61d4e2e016 | ||
|
|
c1eb5055e5 | ||
|
|
8d942e28da | ||
|
|
f7461af3f5 | ||
|
|
29f3e02adc | ||
|
|
9a704458b3 | ||
|
|
668dfd2e38 | ||
|
|
3b3f94ed03 | ||
|
|
e945aa9b71 | ||
|
|
6595a387d0 | ||
|
|
8839e42dc0 | ||
|
|
9db6312081 | ||
|
|
779ef2a4aa | ||
|
|
51e17643a2 | ||
|
|
4c2074b6ff | ||
|
|
c9ffc9465f | ||
|
|
2b6c489513 | ||
|
|
0c63f21718 | ||
|
|
a987df38f4 | ||
|
|
878707f444 | ||
|
|
9369ee5008 | ||
|
|
0a88db975a | ||
|
|
dd60dec887 | ||
|
|
0c1097592e | ||
|
|
bed66235f2 | ||
|
|
26b1e9d5f1 | ||
|
+1 |
79347ec62b | ||
|
|
7b965179d8 | ||
|
|
fc51ffc589 | ||
|
|
96f6e37cc5 | ||
|
|
29774ce84a | ||
|
|
8cbe9c26fc | ||
|
|
7f42566207 | ||
|
|
b60237b676 | ||
|
|
1fe09d369f | ||
|
|
b7757c6b1a | ||
|
|
1a25bacce1 | ||
|
|
6797df239d | ||
|
|
43e7c10eb7 | ||
|
|
bdc9c9c2a8 | ||
|
|
f0c72bf249 | ||
|
|
a8904bfc48 | ||
|
|
b31041726b | ||
|
|
e6f947ad90 | ||
|
|
7963993171 |
@@ -1,11 +1,5 @@
|
|||||||
version = 1
|
version = 1
|
||||||
|
|
||||||
exclude_patterns = [
|
|
||||||
"bin/**",
|
|
||||||
"**/node_modules/",
|
|
||||||
"**/*.min.js"
|
|
||||||
]
|
|
||||||
|
|
||||||
[[analyzers]]
|
[[analyzers]]
|
||||||
name = "shell"
|
name = "shell"
|
||||||
|
|
||||||
|
|||||||
+2
-13
@@ -2,16 +2,5 @@
|
|||||||
*.pyc
|
*.pyc
|
||||||
.env
|
.env
|
||||||
venv
|
venv
|
||||||
node_modules/
|
node_modules
|
||||||
**/node_modules/
|
npm-debug.log
|
||||||
npm-debug.log
|
|
||||||
.next/
|
|
||||||
**/.next/
|
|
||||||
.turbo/
|
|
||||||
**/.turbo/
|
|
||||||
build/
|
|
||||||
**/build/
|
|
||||||
out/
|
|
||||||
**/out/
|
|
||||||
dist/
|
|
||||||
**/dist/
|
|
||||||
+6
-4
@@ -1,12 +1,14 @@
|
|||||||
# Database Settings
|
# Database Settings
|
||||||
POSTGRES_USER="plane"
|
PGUSER="plane"
|
||||||
POSTGRES_PASSWORD="plane"
|
PGPASSWORD="plane"
|
||||||
POSTGRES_DB="plane"
|
PGHOST="plane-db"
|
||||||
PGDATA="/var/lib/postgresql/data"
|
PGDATABASE="plane"
|
||||||
|
DATABASE_URL=postgresql://${PGUSER}:${PGPASSWORD}@${PGHOST}/${PGDATABASE}
|
||||||
|
|
||||||
# Redis Settings
|
# Redis Settings
|
||||||
REDIS_HOST="plane-redis"
|
REDIS_HOST="plane-redis"
|
||||||
REDIS_PORT="6379"
|
REDIS_PORT="6379"
|
||||||
|
REDIS_URL="redis://${REDIS_HOST}:6379/"
|
||||||
|
|
||||||
# AWS Settings
|
# AWS Settings
|
||||||
AWS_REGION=""
|
AWS_REGION=""
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
name: Bug report
|
name: Bug report
|
||||||
description: Create a bug report to help us improve Plane
|
description: Create a bug report to help us improve Plane
|
||||||
title: "[bug]: "
|
title: "[bug]: "
|
||||||
labels: [🐛bug]
|
labels: [bug, need testing]
|
||||||
assignees: [srinivaspendem, pushya-plane]
|
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
name: Feature request
|
name: Feature request
|
||||||
description: Suggest a feature to improve Plane
|
description: Suggest a feature to improve Plane
|
||||||
title: "[feature]: "
|
title: "[feature]: "
|
||||||
labels: [✨feature]
|
labels: [feature]
|
||||||
assignees: [srinivaspendem, pushya-plane]
|
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
+117
-124
@@ -1,74 +1,120 @@
|
|||||||
|
|
||||||
name: Branch Build
|
name: Branch Build
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
pull_request:
|
||||||
inputs:
|
types:
|
||||||
branch_name:
|
- closed
|
||||||
description: "Branch Name"
|
branches:
|
||||||
required: true
|
|
||||||
default: "preview"
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
- master
|
||||||
- preview
|
- release
|
||||||
|
- qa
|
||||||
- develop
|
- develop
|
||||||
release:
|
|
||||||
types: [released, prereleased]
|
|
||||||
|
|
||||||
env:
|
env:
|
||||||
TARGET_BRANCH: ${{ inputs.branch_name || github.ref_name || github.event.release.target_commitish }}
|
TARGET_BRANCH: ${{ github.event.pull_request.base.ref }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
branch_build_setup:
|
branch_build_and_push:
|
||||||
|
if: ${{ (github.event_name == 'pull_request' && github.event.action =='closed' && github.event.pull_request.merged == true) }}
|
||||||
name: Build-Push Web/Space/API/Proxy Docker Image
|
name: Build-Push Web/Space/API/Proxy Docker Image
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out the repo
|
- name: Check out the repo
|
||||||
uses: actions/checkout@v3.3.0
|
uses: actions/checkout@v3.3.0
|
||||||
|
|
||||||
|
# - name: Set Target Branch Name on PR close
|
||||||
|
# if: ${{ github.event_name == 'pull_request' && github.event.action =='closed' }}
|
||||||
|
# run: echo "TARGET_BRANCH=${{ github.event.pull_request.base.ref }}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
# - name: Set Target Branch Name on other than PR close
|
||||||
|
# if: ${{ github.event_name == 'push' }}
|
||||||
|
# run: echo "TARGET_BRANCH=${{ github.ref_name }}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- uses: ASzc/change-string-case-action@v2
|
||||||
|
id: gh_branch_upper_lower
|
||||||
|
with:
|
||||||
|
string: ${{env.TARGET_BRANCH}}
|
||||||
|
|
||||||
|
- uses: mad9000/actions-find-and-replace-string@2
|
||||||
|
id: gh_branch_replace_slash
|
||||||
|
with:
|
||||||
|
source: ${{ steps.gh_branch_upper_lower.outputs.lowercase }}
|
||||||
|
find: '/'
|
||||||
|
replace: '-'
|
||||||
|
|
||||||
|
- uses: mad9000/actions-find-and-replace-string@2
|
||||||
|
id: gh_branch_replace_dot
|
||||||
|
with:
|
||||||
|
source: ${{ steps.gh_branch_replace_slash.outputs.value }}
|
||||||
|
find: '.'
|
||||||
|
replace: ''
|
||||||
|
|
||||||
|
- uses: mad9000/actions-find-and-replace-string@2
|
||||||
|
id: gh_branch_clean
|
||||||
|
with:
|
||||||
|
source: ${{ steps.gh_branch_replace_dot.outputs.value }}
|
||||||
|
find: '_'
|
||||||
|
replace: ''
|
||||||
|
- name: Uploading Proxy Source
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: proxy-src-code
|
||||||
|
path: ./nginx
|
||||||
|
- name: Uploading Backend Source
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: backend-src-code
|
||||||
|
path: ./apiserver
|
||||||
|
- name: Uploading Web Source
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: web-src-code
|
||||||
|
path: |
|
||||||
|
./
|
||||||
|
!./apiserver
|
||||||
|
!./nginx
|
||||||
|
!./deploy
|
||||||
|
!./space
|
||||||
|
|
||||||
|
- name: Uploading Space Source
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: space-src-code
|
||||||
|
path: |
|
||||||
|
./
|
||||||
|
!./apiserver
|
||||||
|
!./nginx
|
||||||
|
!./deploy
|
||||||
|
!./web
|
||||||
outputs:
|
outputs:
|
||||||
gh_branch_name: ${{ env.TARGET_BRANCH }}
|
gh_branch_name: ${{ steps.gh_branch_clean.outputs.value }}
|
||||||
|
|
||||||
branch_build_push_frontend:
|
branch_build_push_frontend:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
needs: [branch_build_setup]
|
needs: [ branch_build_and_push ]
|
||||||
env:
|
|
||||||
FRONTEND_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend:${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
|
||||||
steps:
|
steps:
|
||||||
- name: Set Frontend Docker Tag
|
|
||||||
run: |
|
|
||||||
if [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ] && [ "${{ github.event_name }}" == "release" ]; then
|
|
||||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend:${{ github.event.release.tag_name }}
|
|
||||||
elif [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ]; then
|
|
||||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend:stable
|
|
||||||
else
|
|
||||||
TAG=${{ env.FRONTEND_TAG }}
|
|
||||||
fi
|
|
||||||
echo "FRONTEND_TAG=${TAG}" >> $GITHUB_ENV
|
|
||||||
- name: Docker Setup QEMU
|
|
||||||
uses: docker/setup-qemu-action@v3.0.0
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3.0.0
|
uses: docker/setup-buildx-action@v2.5.0
|
||||||
with:
|
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
buildkitd-flags: "--allow-insecure-entitlement security.insecure"
|
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v3.0.0
|
uses: docker/login-action@v2.1.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
- name: Downloading Web Source Code
|
||||||
- name: Check out the repo
|
uses: actions/download-artifact@v3
|
||||||
uses: actions/checkout@v4.1.1
|
with:
|
||||||
|
name: web-src-code
|
||||||
|
|
||||||
- name: Build and Push Frontend to Docker Container Registry
|
- name: Build and Push Frontend to Docker Container Registry
|
||||||
uses: docker/build-push-action@v5.1.0
|
uses: docker/build-push-action@v4.0.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./web/Dockerfile.web
|
file: ./web/Dockerfile.web
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
tags: ${{ env.FRONTEND_TAG }}
|
tags: ${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend-private:${{ needs.branch_build_and_push.outputs.gh_branch_name }}
|
||||||
push: true
|
push: true
|
||||||
env:
|
env:
|
||||||
DOCKER_BUILDKIT: 1
|
DOCKER_BUILDKIT: 1
|
||||||
@@ -77,46 +123,28 @@ jobs:
|
|||||||
|
|
||||||
branch_build_push_space:
|
branch_build_push_space:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
needs: [branch_build_setup]
|
needs: [ branch_build_and_push ]
|
||||||
env:
|
|
||||||
SPACE_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-space:${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
|
||||||
steps:
|
steps:
|
||||||
- name: Set Space Docker Tag
|
|
||||||
run: |
|
|
||||||
if [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ] && [ "${{ github.event_name }}" == "release" ]; then
|
|
||||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-space:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-space:${{ github.event.release.tag_name }}
|
|
||||||
elif [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ]; then
|
|
||||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-space:stable
|
|
||||||
else
|
|
||||||
TAG=${{ env.SPACE_TAG }}
|
|
||||||
fi
|
|
||||||
echo "SPACE_TAG=${TAG}" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Docker Setup QEMU
|
|
||||||
uses: docker/setup-qemu-action@v3.0.0
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3.0.0
|
uses: docker/setup-buildx-action@v2.5.0
|
||||||
with:
|
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
buildkitd-flags: "--allow-insecure-entitlement security.insecure"
|
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v3.0.0
|
uses: docker/login-action@v2.1.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
- name: Downloading Space Source Code
|
||||||
- name: Check out the repo
|
uses: actions/download-artifact@v3
|
||||||
uses: actions/checkout@v4.1.1
|
with:
|
||||||
|
name: space-src-code
|
||||||
|
|
||||||
- name: Build and Push Space to Docker Hub
|
- name: Build and Push Space to Docker Hub
|
||||||
uses: docker/build-push-action@v5.1.0
|
uses: docker/build-push-action@v4.0.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./space/Dockerfile.space
|
file: ./space/Dockerfile.space
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
tags: ${{ env.SPACE_TAG }}
|
tags: ${{ secrets.DOCKERHUB_USERNAME }}/plane-space-private:${{ needs.branch_build_and_push.outputs.gh_branch_name }}
|
||||||
push: true
|
push: true
|
||||||
env:
|
env:
|
||||||
DOCKER_BUILDKIT: 1
|
DOCKER_BUILDKIT: 1
|
||||||
@@ -125,47 +153,29 @@ jobs:
|
|||||||
|
|
||||||
branch_build_push_backend:
|
branch_build_push_backend:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
needs: [branch_build_setup]
|
needs: [ branch_build_and_push ]
|
||||||
env:
|
|
||||||
BACKEND_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-backend:${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
|
||||||
steps:
|
steps:
|
||||||
- name: Set Backend Docker Tag
|
|
||||||
run: |
|
|
||||||
if [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ] && [ "${{ github.event_name }}" == "release" ]; then
|
|
||||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-backend:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-backend:${{ github.event.release.tag_name }}
|
|
||||||
elif [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ]; then
|
|
||||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-backend:stable
|
|
||||||
else
|
|
||||||
TAG=${{ env.BACKEND_TAG }}
|
|
||||||
fi
|
|
||||||
echo "BACKEND_TAG=${TAG}" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Docker Setup QEMU
|
|
||||||
uses: docker/setup-qemu-action@v3.0.0
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3.0.0
|
uses: docker/setup-buildx-action@v2.5.0
|
||||||
with:
|
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
buildkitd-flags: "--allow-insecure-entitlement security.insecure"
|
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v3.0.0
|
uses: docker/login-action@v2.1.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
- name: Downloading Backend Source Code
|
||||||
- name: Check out the repo
|
uses: actions/download-artifact@v3
|
||||||
uses: actions/checkout@v4.1.1
|
with:
|
||||||
|
name: backend-src-code
|
||||||
|
|
||||||
- name: Build and Push Backend to Docker Hub
|
- name: Build and Push Backend to Docker Hub
|
||||||
uses: docker/build-push-action@v5.1.0
|
uses: docker/build-push-action@v4.0.0
|
||||||
with:
|
with:
|
||||||
context: ./apiserver
|
context: .
|
||||||
file: ./apiserver/Dockerfile.api
|
file: ./Dockerfile.api
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ env.BACKEND_TAG }}
|
tags: ${{ secrets.DOCKERHUB_USERNAME }}/plane-backend-private:${{ needs.branch_build_and_push.outputs.gh_branch_name }}
|
||||||
env:
|
env:
|
||||||
DOCKER_BUILDKIT: 1
|
DOCKER_BUILDKIT: 1
|
||||||
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
@@ -173,46 +183,29 @@ jobs:
|
|||||||
|
|
||||||
branch_build_push_proxy:
|
branch_build_push_proxy:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
needs: [branch_build_setup]
|
needs: [ branch_build_and_push ]
|
||||||
env:
|
|
||||||
PROXY_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy:${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
|
||||||
steps:
|
steps:
|
||||||
- name: Set Proxy Docker Tag
|
|
||||||
run: |
|
|
||||||
if [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ] && [ "${{ github.event_name }}" == "release" ]; then
|
|
||||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy:${{ github.event.release.tag_name }}
|
|
||||||
elif [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ]; then
|
|
||||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy:stable
|
|
||||||
else
|
|
||||||
TAG=${{ env.PROXY_TAG }}
|
|
||||||
fi
|
|
||||||
echo "PROXY_TAG=${TAG}" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Docker Setup QEMU
|
|
||||||
uses: docker/setup-qemu-action@v3.0.0
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3.0.0
|
uses: docker/setup-buildx-action@v2.5.0
|
||||||
with:
|
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
buildkitd-flags: "--allow-insecure-entitlement security.insecure"
|
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v3.0.0
|
uses: docker/login-action@v2.1.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Check out the repo
|
- name: Downloading Proxy Source Code
|
||||||
uses: actions/checkout@v4.1.1
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: proxy-src-code
|
||||||
|
|
||||||
- name: Build and Push Plane-Proxy to Docker Hub
|
- name: Build and Push Plane-Proxy to Docker Hub
|
||||||
uses: docker/build-push-action@v5.1.0
|
uses: docker/build-push-action@v4.0.0
|
||||||
with:
|
with:
|
||||||
context: ./nginx
|
context: .
|
||||||
file: ./nginx/Dockerfile
|
file: ./Dockerfile
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
tags: ${{ env.PROXY_TAG }}
|
tags: ${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy-private:${{ needs.branch_build_and_push.outputs.gh_branch_name }}
|
||||||
push: true
|
push: true
|
||||||
env:
|
env:
|
||||||
DOCKER_BUILDKIT: 1
|
DOCKER_BUILDKIT: 1
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
name: Build Pull Request Contents
|
name: Build Pull Request Contents
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
types: ["opened", "synchronize"]
|
types: ["opened", "synchronize"]
|
||||||
|
|
||||||
@@ -14,18 +14,16 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout Repository to Actions
|
- name: Checkout Repository to Actions
|
||||||
uses: actions/checkout@v3.3.0
|
uses: actions/checkout@v3.3.0
|
||||||
with:
|
|
||||||
token: ${{ secrets.ACCESS_TOKEN }}
|
|
||||||
|
|
||||||
- name: Setup Node.js 18.x
|
- name: Setup Node.js 18.x
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: 18.x
|
node-version: 18.x
|
||||||
cache: "yarn"
|
cache: 'yarn'
|
||||||
|
|
||||||
- name: Get changed files
|
- name: Get changed files
|
||||||
id: changed-files
|
id: changed-files
|
||||||
uses: tj-actions/changed-files@v41
|
uses: tj-actions/changed-files@v38
|
||||||
with:
|
with:
|
||||||
files_yaml: |
|
files_yaml: |
|
||||||
apiserver:
|
apiserver:
|
||||||
@@ -46,3 +44,5 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
yarn
|
yarn
|
||||||
yarn build --filter=space
|
yarn build --filter=space
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
name: "CodeQL"
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ 'develop', 'preview', 'master' ]
|
|
||||||
pull_request:
|
|
||||||
# The branches below must be a subset of the branches above
|
|
||||||
branches: [ 'develop', 'preview', 'master' ]
|
|
||||||
schedule:
|
|
||||||
- cron: '53 19 * * 5'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
analyze:
|
|
||||||
name: Analyze
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
actions: read
|
|
||||||
contents: read
|
|
||||||
security-events: write
|
|
||||||
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
language: [ 'python', 'javascript' ]
|
|
||||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
|
||||||
# Use only 'java' to analyze code written in Java, Kotlin or both
|
|
||||||
# Use only 'javascript' to analyze code written in JavaScript, TypeScript or both
|
|
||||||
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
|
||||||
- name: Initialize CodeQL
|
|
||||||
uses: github/codeql-action/init@v2
|
|
||||||
with:
|
|
||||||
languages: ${{ matrix.language }}
|
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
|
||||||
# By default, queries listed here will override any specified in a config file.
|
|
||||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
|
||||||
|
|
||||||
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
|
||||||
# queries: security-extended,security-and-quality
|
|
||||||
|
|
||||||
|
|
||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
|
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
|
||||||
- name: Autobuild
|
|
||||||
uses: github/codeql-action/autobuild@v2
|
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
|
||||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
|
||||||
|
|
||||||
# If the Autobuild fails above, remove it and uncomment the following three lines.
|
|
||||||
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
|
|
||||||
|
|
||||||
# - run: |
|
|
||||||
# echo "Run, Build Application using script"
|
|
||||||
# ./location_of_script_within_repo/buildscript.sh
|
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
|
||||||
uses: github/codeql-action/analyze@v2
|
|
||||||
with:
|
|
||||||
category: "/language:${{matrix.language}}"
|
|
||||||
@@ -1,28 +1,42 @@
|
|||||||
name: Create Sync Action
|
name: Create PR in Plane EE Repository to sync the changes
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
pull_request:
|
||||||
push:
|
|
||||||
branches:
|
branches:
|
||||||
- preview
|
- master
|
||||||
|
types:
|
||||||
env:
|
- closed
|
||||||
SOURCE_BRANCH_NAME: ${{ github.ref_name }}
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
sync_changes:
|
create_pr:
|
||||||
|
# Only run the job when a PR is merged
|
||||||
|
if: github.event.pull_request.merged == true
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
|
- name: Check SOURCE_REPO
|
||||||
|
id: check_repo
|
||||||
|
env:
|
||||||
|
SOURCE_REPO: ${{ secrets.SOURCE_REPO_NAME }}
|
||||||
|
run: |
|
||||||
|
echo "::set-output name=is_correct_repo::$(if [[ "$SOURCE_REPO" == "makeplane/plane" ]]; then echo 'true'; else echo 'false'; fi)"
|
||||||
|
|
||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
uses: actions/checkout@v4.1.1
|
if: steps.check_repo.outputs.is_correct_repo == 'true'
|
||||||
|
uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up Branch Name
|
||||||
|
if: steps.check_repo.outputs.is_correct_repo == 'true'
|
||||||
|
run: |
|
||||||
|
echo "SOURCE_BRANCH_NAME=${{ github.head_ref }}" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Setup GH CLI
|
- name: Setup GH CLI
|
||||||
|
if: steps.check_repo.outputs.is_correct_repo == 'true'
|
||||||
run: |
|
run: |
|
||||||
type -p curl >/dev/null || (sudo apt update && sudo apt install curl -y)
|
type -p curl >/dev/null || (sudo apt update && sudo apt install curl -y)
|
||||||
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
|
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
|
||||||
@@ -31,14 +45,35 @@ jobs:
|
|||||||
sudo apt update
|
sudo apt update
|
||||||
sudo apt install gh -y
|
sudo apt install gh -y
|
||||||
|
|
||||||
- name: Push Changes to Target Repo
|
- name: Create Pull Request
|
||||||
|
if: steps.check_repo.outputs.is_correct_repo == 'true'
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
GH_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
TARGET_REPO="${{ secrets.SYNC_TARGET_REPO_NAME }}"
|
TARGET_REPO="${{ secrets.TARGET_REPO_NAME }}"
|
||||||
TARGET_BRANCH="${{ secrets.SYNC_TARGET_BRANCH_NAME }}"
|
TARGET_BRANCH="${{ secrets.TARGET_REPO_BRANCH }}"
|
||||||
SOURCE_BRANCH="${{ env.SOURCE_BRANCH_NAME }}"
|
SOURCE_BRANCH="${{ env.SOURCE_BRANCH_NAME }}"
|
||||||
|
|
||||||
git checkout $SOURCE_BRANCH
|
git checkout $SOURCE_BRANCH
|
||||||
git remote add target-origin "https://$GH_TOKEN@github.com/$TARGET_REPO.git"
|
git remote add target "https://$GH_TOKEN@github.com/$TARGET_REPO.git"
|
||||||
git push target-origin $SOURCE_BRANCH:$TARGET_BRANCH
|
git push target $SOURCE_BRANCH:$SOURCE_BRANCH
|
||||||
|
|
||||||
|
PR_TITLE="${{ github.event.pull_request.title }}"
|
||||||
|
PR_BODY="${{ github.event.pull_request.body }}"
|
||||||
|
|
||||||
|
# Remove double quotes
|
||||||
|
PR_TITLE_CLEANED="${PR_TITLE//\"/}"
|
||||||
|
PR_BODY_CLEANED="${PR_BODY//\"/}"
|
||||||
|
|
||||||
|
# Construct PR_BODY_CONTENT using a here-document
|
||||||
|
PR_BODY_CONTENT=$(cat <<EOF
|
||||||
|
$PR_BODY_CLEANED
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
|
||||||
|
gh pr create \
|
||||||
|
--base $TARGET_BRANCH \
|
||||||
|
--head $SOURCE_BRANCH \
|
||||||
|
--title "[SYNC] $PR_TITLE_CLEANED" \
|
||||||
|
--body "$PR_BODY_CONTENT" \
|
||||||
|
--repo $TARGET_REPO
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
name: Update Docker Images for Plane on Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [released, prereleased]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build_push_backend:
|
||||||
|
name: Build and Push Api Server Docker Image
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Check out the repo
|
||||||
|
uses: actions/checkout@v3.3.0
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2.5.0
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v2.1.0
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract metadata (tags, labels) for Docker (Docker Hub) from Github Release
|
||||||
|
id: metaFrontend
|
||||||
|
uses: docker/metadata-action@v4.3.0
|
||||||
|
with:
|
||||||
|
images: ${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend
|
||||||
|
tags: |
|
||||||
|
type=ref,event=tag
|
||||||
|
|
||||||
|
- name: Extract metadata (tags, labels) for Docker (Docker Hub) from Github Release
|
||||||
|
id: metaBackend
|
||||||
|
uses: docker/metadata-action@v4.3.0
|
||||||
|
with:
|
||||||
|
images: ${{ secrets.DOCKERHUB_USERNAME }}/plane-backend
|
||||||
|
tags: |
|
||||||
|
type=ref,event=tag
|
||||||
|
|
||||||
|
- name: Extract metadata (tags, labels) for Docker (Docker Hub) from Github Release
|
||||||
|
id: metaSpace
|
||||||
|
uses: docker/metadata-action@v4.3.0
|
||||||
|
with:
|
||||||
|
images: ${{ secrets.DOCKERHUB_USERNAME }}/plane-space
|
||||||
|
tags: |
|
||||||
|
type=ref,event=tag
|
||||||
|
|
||||||
|
- name: Extract metadata (tags, labels) for Docker (Docker Hub) from Github Release
|
||||||
|
id: metaProxy
|
||||||
|
uses: docker/metadata-action@v4.3.0
|
||||||
|
with:
|
||||||
|
images: ${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy
|
||||||
|
tags: |
|
||||||
|
type=ref,event=tag
|
||||||
|
|
||||||
|
- name: Build and Push Frontend to Docker Container Registry
|
||||||
|
uses: docker/build-push-action@v4.0.0
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./web/Dockerfile.web
|
||||||
|
platforms: linux/amd64
|
||||||
|
tags: ${{ steps.metaFrontend.outputs.tags }}
|
||||||
|
push: true
|
||||||
|
env:
|
||||||
|
DOCKER_BUILDKIT: 1
|
||||||
|
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
DOCKET_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and Push Backend to Docker Hub
|
||||||
|
uses: docker/build-push-action@v4.0.0
|
||||||
|
with:
|
||||||
|
context: ./apiserver
|
||||||
|
file: ./apiserver/Dockerfile.api
|
||||||
|
platforms: linux/amd64
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.metaBackend.outputs.tags }}
|
||||||
|
env:
|
||||||
|
DOCKER_BUILDKIT: 1
|
||||||
|
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
DOCKET_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and Push Plane-Deploy to Docker Hub
|
||||||
|
uses: docker/build-push-action@v4.0.0
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./space/Dockerfile.space
|
||||||
|
platforms: linux/amd64
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.metaSpace.outputs.tags }}
|
||||||
|
env:
|
||||||
|
DOCKER_BUILDKIT: 1
|
||||||
|
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
DOCKET_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and Push Plane-Proxy to Docker Hub
|
||||||
|
uses: docker/build-push-action@v4.0.0
|
||||||
|
with:
|
||||||
|
context: ./nginx
|
||||||
|
file: ./nginx/Dockerfile
|
||||||
|
platforms: linux/amd64
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.metaProxy.outputs.tags }}
|
||||||
|
env:
|
||||||
|
DOCKER_BUILDKIT: 1
|
||||||
|
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
DOCKET_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
+25
-4
@@ -33,8 +33,8 @@ The backend is a django project which is kept inside apiserver
|
|||||||
1. Clone the repo
|
1. Clone the repo
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/makeplane/plane.git [folder-name]
|
git clone https://github.com/makeplane/plane
|
||||||
cd [folder-name]
|
cd plane
|
||||||
chmod +x setup.sh
|
chmod +x setup.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -44,12 +44,33 @@ chmod +x setup.sh
|
|||||||
./setup.sh
|
./setup.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Start the containers
|
3. Define `NEXT_PUBLIC_API_BASE_URL=http://localhost` in **web/.env** and **space/.env** file
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose -f docker-compose-local.yml up
|
echo "\nNEXT_PUBLIC_API_BASE_URL=http://localhost\n" >> ./web/.env
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo "\nNEXT_PUBLIC_API_BASE_URL=http://localhost\n" >> ./space/.env
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Run Docker compose up
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Install dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn install
|
||||||
|
```
|
||||||
|
|
||||||
|
6. Run the web app in development mode
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn dev
|
||||||
|
```
|
||||||
|
|
||||||
## Missing a Feature?
|
## Missing a Feature?
|
||||||
|
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ COPY apiserver/manage.py manage.py
|
|||||||
COPY apiserver/plane plane/
|
COPY apiserver/plane plane/
|
||||||
COPY apiserver/templates templates/
|
COPY apiserver/templates templates/
|
||||||
|
|
||||||
|
COPY apiserver/gunicorn.config.py ./
|
||||||
RUN apk --no-cache add "bash~=5.2"
|
RUN apk --no-cache add "bash~=5.2"
|
||||||
COPY apiserver/bin ./bin/
|
COPY apiserver/bin ./bin/
|
||||||
|
|
||||||
|
|||||||
@@ -49,10 +49,24 @@ NGINX_PORT=80
|
|||||||
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
# Enable/Disable OAUTH - default 0 for selfhosted instance
|
||||||
|
NEXT_PUBLIC_ENABLE_OAUTH=0
|
||||||
# Public boards deploy URL
|
# Public boards deploy URL
|
||||||
NEXT_PUBLIC_DEPLOY_URL="http://localhost/spaces"
|
NEXT_PUBLIC_DEPLOY_URL="http://localhost/spaces"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## {PROJECT_FOLDER}/spaces/.env.example
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
# Flag to toggle OAuth
|
||||||
|
NEXT_PUBLIC_ENABLE_OAUTH=0
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## {PROJECT_FOLDER}/apiserver/.env
|
## {PROJECT_FOLDER}/apiserver/.env
|
||||||
|
|
||||||
@@ -62,6 +76,7 @@ NEXT_PUBLIC_DEPLOY_URL="http://localhost/spaces"
|
|||||||
# 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" # deprecated
|
||||||
|
|
||||||
# Error logs
|
# Error logs
|
||||||
SENTRY_DSN=""
|
SENTRY_DSN=""
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ Meet [Plane](https://plane.so). An open-source software development tool to mana
|
|||||||
|
|
||||||
> Plane is still in its early days, not everything will be perfect yet, and hiccups may happen. Please let us know of any suggestions, ideas, or bugs that you encounter on our [Discord](https://discord.com/invite/A92xrEGCge) or GitHub issues, and we will use your feedback to improve on our upcoming releases.
|
> Plane is still in its early days, not everything will be perfect yet, and hiccups may happen. Please let us know of any suggestions, ideas, or bugs that you encounter on our [Discord](https://discord.com/invite/A92xrEGCge) or GitHub issues, and we will use your feedback to improve on our upcoming releases.
|
||||||
|
|
||||||
The easiest way to get started with Plane is by creating a [Plane Cloud](https://app.plane.so) account. Plane Cloud offers a hosted solution for Plane. If you prefer to self-host Plane, please refer to our [deployment documentation](https://docs.plane.so/docker-compose).
|
The easiest way to get started with Plane is by creating a [Plane Cloud](https://app.plane.so) account. Plane Cloud offers a hosted solution for Plane. If you prefer to self-host Plane, please refer to our [deployment documentation](https://docs.plane.so/self-hosting).
|
||||||
|
|
||||||
## ⚡️ Contributors Quick Start
|
## ⚡️ Contributors Quick Start
|
||||||
|
|
||||||
@@ -57,13 +57,17 @@ Setting up local environment is extremely easy and straight forward. Follow the
|
|||||||
1. Review the `.env` files available in various folders. Visit [Environment Setup](./ENV_SETUP.md) to know about various environment variables used in system
|
1. Review the `.env` files available in various folders. Visit [Environment Setup](./ENV_SETUP.md) to know about various environment variables used in system
|
||||||
1. Run the docker command to initiate various services `docker compose -f docker-compose-local.yml up -d`
|
1. Run the docker command to initiate various services `docker compose -f docker-compose-local.yml up -d`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./setup.sh
|
||||||
|
```
|
||||||
|
|
||||||
You are ready to make changes to the code. Do not forget to refresh the browser (in case id does not auto-reload)
|
You are ready to make changes to the code. Do not forget to refresh the browser (in case id does not auto-reload)
|
||||||
|
|
||||||
Thats it!
|
Thats it!
|
||||||
|
|
||||||
## 🍙 Self Hosting
|
## 🍙 Self Hosting
|
||||||
|
|
||||||
For self hosting environment setup, visit the [Self Hosting](https://docs.plane.so/docker-compose) documentation page
|
For self hosting environment setup, visit the [Self Hosting](https://docs.plane.so/self-hosting) documentation page
|
||||||
|
|
||||||
## 🚀 Features
|
## 🚀 Features
|
||||||
|
|
||||||
|
|||||||
+9
-10
@@ -8,16 +8,11 @@ SENTRY_DSN=""
|
|||||||
SENTRY_ENVIRONMENT="development"
|
SENTRY_ENVIRONMENT="development"
|
||||||
|
|
||||||
# Database Settings
|
# Database Settings
|
||||||
POSTGRES_USER="plane"
|
PGUSER="plane"
|
||||||
POSTGRES_PASSWORD="plane"
|
PGPASSWORD="plane"
|
||||||
POSTGRES_HOST="plane-db"
|
PGHOST="plane-db"
|
||||||
POSTGRES_DB="plane"
|
PGDATABASE="plane"
|
||||||
DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}/${POSTGRES_DB}
|
DATABASE_URL=postgresql://${PGUSER}:${PGPASSWORD}@${PGHOST}/${PGDATABASE}
|
||||||
|
|
||||||
# Oauth variables
|
|
||||||
GOOGLE_CLIENT_ID=""
|
|
||||||
GITHUB_CLIENT_ID=""
|
|
||||||
GITHUB_CLIENT_SECRET=""
|
|
||||||
|
|
||||||
# Redis Settings
|
# Redis Settings
|
||||||
REDIS_HOST="plane-redis"
|
REDIS_HOST="plane-redis"
|
||||||
@@ -39,6 +34,9 @@ OPENAI_API_BASE="https://api.openai.com/v1" # deprecated
|
|||||||
OPENAI_API_KEY="sk-" # deprecated
|
OPENAI_API_KEY="sk-" # deprecated
|
||||||
GPT_ENGINE="gpt-3.5-turbo" # deprecated
|
GPT_ENGINE="gpt-3.5-turbo" # deprecated
|
||||||
|
|
||||||
|
# Github
|
||||||
|
GITHUB_CLIENT_SECRET="" # For fetching release notes
|
||||||
|
|
||||||
# Settings related to Docker
|
# Settings related to Docker
|
||||||
DOCKERIZED=1 # deprecated
|
DOCKERIZED=1 # deprecated
|
||||||
|
|
||||||
@@ -52,6 +50,7 @@ NGINX_PORT=80
|
|||||||
# SignUps
|
# SignUps
|
||||||
ENABLE_SIGNUP="1"
|
ENABLE_SIGNUP="1"
|
||||||
|
|
||||||
|
|
||||||
# Enable Email/Password Signup
|
# Enable Email/Password Signup
|
||||||
ENABLE_EMAIL_PASSWORD="1"
|
ENABLE_EMAIL_PASSWORD="1"
|
||||||
|
|
||||||
|
|||||||
@@ -41,10 +41,10 @@ USER captain
|
|||||||
|
|
||||||
# Add in Django deps and generate Django's static files
|
# Add in Django deps and generate Django's static files
|
||||||
COPY manage.py manage.py
|
COPY manage.py manage.py
|
||||||
COPY server.py server.py
|
|
||||||
COPY plane plane/
|
COPY plane plane/
|
||||||
COPY templates templates/
|
COPY templates templates/
|
||||||
COPY package.json package.json
|
COPY package.json package.json
|
||||||
|
COPY gunicorn.config.py ./
|
||||||
USER root
|
USER root
|
||||||
RUN apk --no-cache add "bash~=5.2"
|
RUN apk --no-cache add "bash~=5.2"
|
||||||
COPY ./bin ./bin/
|
COPY ./bin ./bin/
|
||||||
|
|||||||
@@ -27,16 +27,20 @@ WORKDIR /code
|
|||||||
COPY requirements.txt ./requirements.txt
|
COPY requirements.txt ./requirements.txt
|
||||||
ADD requirements ./requirements
|
ADD requirements ./requirements
|
||||||
|
|
||||||
# Install the local development settings
|
RUN pip install -r requirements.txt --compile --no-cache-dir
|
||||||
RUN pip install -r requirements/local.txt --compile --no-cache-dir
|
|
||||||
|
|
||||||
RUN addgroup -S plane && \
|
RUN addgroup -S plane && \
|
||||||
adduser -S captain -G plane
|
adduser -S captain -G plane
|
||||||
|
|
||||||
COPY . .
|
RUN chown captain.plane /code
|
||||||
|
|
||||||
RUN chown -R captain.plane /code
|
USER captain
|
||||||
RUN chmod -R +x /code/bin
|
|
||||||
|
# Add in Django deps and generate Django's static files
|
||||||
|
|
||||||
|
USER root
|
||||||
|
|
||||||
|
# RUN chmod +x ./bin/takeoff ./bin/worker ./bin/beat
|
||||||
RUN chmod -R 777 /code
|
RUN chmod -R 777 /code
|
||||||
|
|
||||||
USER captain
|
USER captain
|
||||||
@@ -44,5 +48,5 @@ USER captain
|
|||||||
# Expose container port and run entry point script
|
# Expose container port and run entry point script
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
|
|
||||||
CMD [ "./bin/takeoff.local" ]
|
# CMD [ "./bin/takeoff" ]
|
||||||
|
|
||||||
|
|||||||
@@ -26,9 +26,7 @@ def update_description():
|
|||||||
updated_issues.append(issue)
|
updated_issues.append(issue)
|
||||||
|
|
||||||
Issue.objects.bulk_update(
|
Issue.objects.bulk_update(
|
||||||
updated_issues,
|
updated_issues, ["description_html", "description_stripped"], batch_size=100
|
||||||
["description_html", "description_stripped"],
|
|
||||||
batch_size=100,
|
|
||||||
)
|
)
|
||||||
print("Success")
|
print("Success")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -42,9 +40,7 @@ def update_comments():
|
|||||||
updated_issue_comments = []
|
updated_issue_comments = []
|
||||||
|
|
||||||
for issue_comment in issue_comments:
|
for issue_comment in issue_comments:
|
||||||
issue_comment.comment_html = (
|
issue_comment.comment_html = f"<p>{issue_comment.comment_stripped}</p>"
|
||||||
f"<p>{issue_comment.comment_stripped}</p>"
|
|
||||||
)
|
|
||||||
updated_issue_comments.append(issue_comment)
|
updated_issue_comments.append(issue_comment)
|
||||||
|
|
||||||
IssueComment.objects.bulk_update(
|
IssueComment.objects.bulk_update(
|
||||||
@@ -103,9 +99,7 @@ def updated_issue_sort_order():
|
|||||||
issue.sort_order = issue.sequence_id * random.randint(100, 500)
|
issue.sort_order = issue.sequence_id * random.randint(100, 500)
|
||||||
updated_issues.append(issue)
|
updated_issues.append(issue)
|
||||||
|
|
||||||
Issue.objects.bulk_update(
|
Issue.objects.bulk_update(updated_issues, ["sort_order"], batch_size=100)
|
||||||
updated_issues, ["sort_order"], batch_size=100
|
|
||||||
)
|
|
||||||
print("Success")
|
print("Success")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
print(e)
|
||||||
@@ -143,9 +137,7 @@ def update_project_cover_images():
|
|||||||
project.cover_image = project_cover_images[random.randint(0, 19)]
|
project.cover_image = project_cover_images[random.randint(0, 19)]
|
||||||
updated_projects.append(project)
|
updated_projects.append(project)
|
||||||
|
|
||||||
Project.objects.bulk_update(
|
Project.objects.bulk_update(updated_projects, ["cover_image"], batch_size=100)
|
||||||
updated_projects, ["cover_image"], batch_size=100
|
|
||||||
)
|
|
||||||
print("Success")
|
print("Success")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
print(e)
|
||||||
@@ -194,9 +186,7 @@ def update_label_color():
|
|||||||
|
|
||||||
def create_slack_integration():
|
def create_slack_integration():
|
||||||
try:
|
try:
|
||||||
_ = Integration.objects.create(
|
_ = Integration.objects.create(provider="slack", network=2, title="Slack")
|
||||||
provider="slack", network=2, title="Slack"
|
|
||||||
)
|
|
||||||
print("Success")
|
print("Success")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
print(e)
|
||||||
@@ -222,16 +212,12 @@ def update_integration_verified():
|
|||||||
|
|
||||||
def update_start_date():
|
def update_start_date():
|
||||||
try:
|
try:
|
||||||
issues = Issue.objects.filter(
|
issues = Issue.objects.filter(state__group__in=["started", "completed"])
|
||||||
state__group__in=["started", "completed"]
|
|
||||||
)
|
|
||||||
updated_issues = []
|
updated_issues = []
|
||||||
for issue in issues:
|
for issue in issues:
|
||||||
issue.start_date = issue.created_at.date()
|
issue.start_date = issue.created_at.date()
|
||||||
updated_issues.append(issue)
|
updated_issues.append(issue)
|
||||||
Issue.objects.bulk_update(
|
Issue.objects.bulk_update(updated_issues, ["start_date"], batch_size=500)
|
||||||
updated_issues, ["start_date"], batch_size=500
|
|
||||||
)
|
|
||||||
print("Success")
|
print("Success")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
print(e)
|
||||||
|
|||||||
Executable → Regular
-3
@@ -2,7 +2,4 @@
|
|||||||
set -e
|
set -e
|
||||||
|
|
||||||
python manage.py wait_for_db
|
python manage.py wait_for_db
|
||||||
# Wait for migrations
|
|
||||||
python manage.py wait_for_migrations
|
|
||||||
# Run the processes
|
|
||||||
celery -A plane beat -l info
|
celery -A plane beat -l info
|
||||||
+11
-22
@@ -1,31 +1,20 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
python manage.py wait_for_db
|
python manage.py wait_for_db
|
||||||
# Wait for migrations
|
python manage.py migrate
|
||||||
python manage.py wait_for_migrations
|
|
||||||
|
|
||||||
# Create the default bucket
|
# Set default value for ENABLE_REGISTRATION
|
||||||
#!/bin/bash
|
ENABLE_REGISTRATION=${ENABLE_REGISTRATION:-1}
|
||||||
|
|
||||||
# Collect system information
|
# Check if ENABLE_REGISTRATION is not set to '0'
|
||||||
HOSTNAME=$(hostname)
|
if [ "$ENABLE_REGISTRATION" != "0" ]; then
|
||||||
MAC_ADDRESS=$(ip link show | awk '/ether/ {print $2}' | head -n 1)
|
# Register instance
|
||||||
CPU_INFO=$(cat /proc/cpuinfo)
|
python manage.py register_instance
|
||||||
MEMORY_INFO=$(free -h)
|
# Load the configuration variable
|
||||||
DISK_INFO=$(df -h)
|
python manage.py configure_instance
|
||||||
|
fi
|
||||||
# Concatenate information and compute SHA-256 hash
|
|
||||||
SIGNATURE=$(echo "$HOSTNAME$MAC_ADDRESS$CPU_INFO$MEMORY_INFO$DISK_INFO" | sha256sum | awk '{print $1}')
|
|
||||||
|
|
||||||
# Export the variables
|
|
||||||
export MACHINE_SIGNATURE=$SIGNATURE
|
|
||||||
|
|
||||||
# Register instance
|
|
||||||
python manage.py register_instance $MACHINE_SIGNATURE
|
|
||||||
# Load the configuration variable
|
|
||||||
python manage.py configure_instance
|
|
||||||
|
|
||||||
# Create the default bucket
|
# Create the default bucket
|
||||||
python manage.py create_bucket
|
python manage.py create_bucket
|
||||||
|
|
||||||
python server.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 -
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
python manage.py wait_for_db
|
|
||||||
# Wait for migrations
|
|
||||||
python manage.py wait_for_migrations
|
|
||||||
|
|
||||||
# Create the default bucket
|
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Collect system information
|
|
||||||
HOSTNAME=$(hostname)
|
|
||||||
MAC_ADDRESS=$(ip link show | awk '/ether/ {print $2}' | head -n 1)
|
|
||||||
CPU_INFO=$(cat /proc/cpuinfo)
|
|
||||||
MEMORY_INFO=$(free -h)
|
|
||||||
DISK_INFO=$(df -h)
|
|
||||||
|
|
||||||
# Concatenate information and compute SHA-256 hash
|
|
||||||
SIGNATURE=$(echo "$HOSTNAME$MAC_ADDRESS$CPU_INFO$MEMORY_INFO$DISK_INFO" | sha256sum | awk '{print $1}')
|
|
||||||
|
|
||||||
# Export the variables
|
|
||||||
export MACHINE_SIGNATURE=$SIGNATURE
|
|
||||||
|
|
||||||
# Register instance
|
|
||||||
python manage.py register_instance $MACHINE_SIGNATURE
|
|
||||||
# Load the configuration variable
|
|
||||||
python manage.py configure_instance
|
|
||||||
|
|
||||||
# Create the default bucket
|
|
||||||
python manage.py create_bucket
|
|
||||||
|
|
||||||
python manage.py runserver 0.0.0.0:8000 --settings=plane.settings.local
|
|
||||||
|
|
||||||
@@ -2,7 +2,4 @@
|
|||||||
set -e
|
set -e
|
||||||
|
|
||||||
python manage.py wait_for_db
|
python manage.py wait_for_db
|
||||||
# Wait for migrations
|
|
||||||
python manage.py wait_for_migrations
|
|
||||||
# Run the processes
|
|
||||||
celery -A plane worker -l info
|
celery -A plane worker -l info
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
from psycogreen.gevent import patch_psycopg
|
||||||
|
|
||||||
|
|
||||||
|
def post_fork(server, worker):
|
||||||
|
patch_psycopg()
|
||||||
|
worker.log.info("Made Psycopg2 Green")
|
||||||
+3
-3
@@ -2,10 +2,10 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == '__main__':
|
||||||
os.environ.setdefault(
|
os.environ.setdefault(
|
||||||
"DJANGO_SETTINGS_MODULE", "plane.settings.production"
|
'DJANGO_SETTINGS_MODULE',
|
||||||
)
|
'plane.settings.production')
|
||||||
try:
|
try:
|
||||||
from django.core.management import execute_from_command_line
|
from django.core.management import execute_from_command_line
|
||||||
except ImportError as exc:
|
except ImportError as exc:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"name": "plane-api",
|
"name": "plane-api",
|
||||||
"version": "0.15.0"
|
"version": "0.13.2"
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
from .celery import app as celery_app
|
from .celery import app as celery_app
|
||||||
|
|
||||||
__all__ = ("celery_app",)
|
__all__ = ('celery_app',)
|
||||||
|
|||||||
@@ -2,4 +2,4 @@ from django.apps import AppConfig
|
|||||||
|
|
||||||
|
|
||||||
class AnalyticsConfig(AppConfig):
|
class AnalyticsConfig(AppConfig):
|
||||||
name = "plane.analytics"
|
name = 'plane.analytics'
|
||||||
|
|||||||
@@ -2,4 +2,4 @@ from django.apps import AppConfig
|
|||||||
|
|
||||||
|
|
||||||
class ApiConfig(AppConfig):
|
class ApiConfig(AppConfig):
|
||||||
name = "plane.api"
|
name = "plane.api"
|
||||||
@@ -25,10 +25,7 @@ class APIKeyAuthentication(authentication.BaseAuthentication):
|
|||||||
def validate_api_token(self, token):
|
def validate_api_token(self, token):
|
||||||
try:
|
try:
|
||||||
api_token = APIToken.objects.get(
|
api_token = APIToken.objects.get(
|
||||||
Q(
|
Q(Q(expired_at__gt=timezone.now()) | Q(expired_at__isnull=True)),
|
||||||
Q(expired_at__gt=timezone.now())
|
|
||||||
| Q(expired_at__isnull=True)
|
|
||||||
),
|
|
||||||
token=token,
|
token=token,
|
||||||
is_active=True,
|
is_active=True,
|
||||||
)
|
)
|
||||||
@@ -47,4 +44,4 @@ class APIKeyAuthentication(authentication.BaseAuthentication):
|
|||||||
|
|
||||||
# Validate the API token
|
# Validate the API token
|
||||||
user, token = self.validate_api_token(token)
|
user, token = self.validate_api_token(token)
|
||||||
return user, token
|
return user, token
|
||||||
@@ -1,31 +1,34 @@
|
|||||||
|
from django.utils import timezone
|
||||||
from rest_framework.throttling import SimpleRateThrottle
|
from rest_framework.throttling import SimpleRateThrottle
|
||||||
|
|
||||||
|
|
||||||
class ApiKeyRateThrottle(SimpleRateThrottle):
|
class ApiKeyRateThrottle(SimpleRateThrottle):
|
||||||
scope = "api_key"
|
scope = 'api_key'
|
||||||
rate = "60/minute"
|
|
||||||
|
|
||||||
def get_cache_key(self, request, view):
|
def get_cache_key(self, request, view):
|
||||||
# Retrieve the API key from the request header
|
# Retrieve the API key from the request header
|
||||||
api_key = request.headers.get("X-Api-Key")
|
api_key = request.headers.get('X-Api-Key')
|
||||||
if not api_key:
|
if not api_key:
|
||||||
return None # Allow the request if there's no API key
|
return None # Allow the request if there's no API key
|
||||||
|
|
||||||
# Use the API key as part of the cache key
|
# Use the API key as part of the cache key
|
||||||
return f"{self.scope}:{api_key}"
|
return f'{self.scope}:{api_key}'
|
||||||
|
|
||||||
def allow_request(self, request, view):
|
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)
|
allowed = super().allow_request(request, view)
|
||||||
|
|
||||||
if allowed:
|
if allowed:
|
||||||
now = self.timer()
|
|
||||||
# Calculate the remaining limit and reset time
|
# Calculate the remaining limit and reset time
|
||||||
history = self.cache.get(self.key, [])
|
history = self.cache.get(self.key, [])
|
||||||
|
|
||||||
# Remove old histories
|
# Remove old histories
|
||||||
while history and history[-1] <= now - self.duration:
|
while history and history[-1] <= now - self.duration:
|
||||||
history.pop()
|
history.pop()
|
||||||
|
|
||||||
# Calculate the requests
|
# Calculate the requests
|
||||||
num_requests = len(history)
|
num_requests = len(history)
|
||||||
|
|
||||||
@@ -36,7 +39,7 @@ class ApiKeyRateThrottle(SimpleRateThrottle):
|
|||||||
reset_time = int(now + self.duration)
|
reset_time = int(now + self.duration)
|
||||||
|
|
||||||
# Add headers
|
# Add headers
|
||||||
request.META["X-RateLimit-Remaining"] = max(0, available)
|
request.META['X-RateLimit-Remaining'] = max(0, available)
|
||||||
request.META["X-RateLimit-Reset"] = reset_time
|
request.META['X-RateLimit-Reset'] = reset_time
|
||||||
|
|
||||||
return allowed
|
return allowed
|
||||||
@@ -9,13 +9,8 @@ from .issue import (
|
|||||||
IssueCommentSerializer,
|
IssueCommentSerializer,
|
||||||
IssueAttachmentSerializer,
|
IssueAttachmentSerializer,
|
||||||
IssueActivitySerializer,
|
IssueActivitySerializer,
|
||||||
IssueExpandSerializer,
|
|
||||||
)
|
)
|
||||||
from .state import StateLiteSerializer, StateSerializer
|
from .state import StateLiteSerializer, StateSerializer
|
||||||
from .cycle import CycleSerializer, CycleIssueSerializer, CycleLiteSerializer
|
from .cycle import CycleSerializer, CycleIssueSerializer
|
||||||
from .module import (
|
from .module import ModuleSerializer, ModuleIssueSerializer
|
||||||
ModuleSerializer,
|
from .inbox import InboxIssueSerializer
|
||||||
ModuleIssueSerializer,
|
|
||||||
ModuleLiteSerializer,
|
|
||||||
)
|
|
||||||
from .inbox import InboxIssueSerializer
|
|
||||||
@@ -97,11 +97,9 @@ class BaseSerializer(serializers.ModelSerializer):
|
|||||||
exp_serializer = expansion[expand](
|
exp_serializer = expansion[expand](
|
||||||
getattr(instance, expand)
|
getattr(instance, expand)
|
||||||
)
|
)
|
||||||
response[expand] = exp_serializer.data
|
response[expand] = exp_serializer.data
|
||||||
else:
|
else:
|
||||||
# You might need to handle this case differently
|
# You might need to handle this case differently
|
||||||
response[expand] = getattr(
|
response[expand] = getattr(instance, f"{expand}_id", None)
|
||||||
instance, f"{expand}_id", None
|
|
||||||
)
|
|
||||||
|
|
||||||
return response
|
return response
|
||||||
@@ -23,20 +23,13 @@ class CycleSerializer(BaseSerializer):
|
|||||||
and data.get("end_date", None) is not None
|
and data.get("end_date", None) is not None
|
||||||
and data.get("start_date", None) > data.get("end_date", None)
|
and data.get("start_date", None) > data.get("end_date", None)
|
||||||
):
|
):
|
||||||
raise serializers.ValidationError(
|
raise serializers.ValidationError("Start date cannot exceed end date")
|
||||||
"Start date cannot exceed end date"
|
|
||||||
)
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Cycle
|
model = Cycle
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
read_only_fields = [
|
read_only_fields = [
|
||||||
"id",
|
|
||||||
"created_at",
|
|
||||||
"updated_at",
|
|
||||||
"created_by",
|
|
||||||
"updated_by",
|
|
||||||
"workspace",
|
"workspace",
|
||||||
"project",
|
"project",
|
||||||
"owned_by",
|
"owned_by",
|
||||||
@@ -53,10 +46,4 @@ class CycleIssueSerializer(BaseSerializer):
|
|||||||
"workspace",
|
"workspace",
|
||||||
"project",
|
"project",
|
||||||
"cycle",
|
"cycle",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class CycleLiteSerializer(BaseSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = Cycle
|
|
||||||
fields = "__all__"
|
|
||||||
@@ -2,18 +2,12 @@
|
|||||||
from .base import BaseSerializer
|
from .base import BaseSerializer
|
||||||
from plane.db.models import InboxIssue
|
from plane.db.models import InboxIssue
|
||||||
|
|
||||||
|
|
||||||
class InboxIssueSerializer(BaseSerializer):
|
class InboxIssueSerializer(BaseSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = InboxIssue
|
model = InboxIssue
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
read_only_fields = [
|
read_only_fields = [
|
||||||
"id",
|
|
||||||
"workspace",
|
|
||||||
"project",
|
"project",
|
||||||
"issue",
|
"workspace",
|
||||||
"created_by",
|
]
|
||||||
"updated_by",
|
|
||||||
"created_at",
|
|
||||||
"updated_at",
|
|
||||||
]
|
|
||||||
@@ -1,6 +1,3 @@
|
|||||||
from lxml import html
|
|
||||||
|
|
||||||
|
|
||||||
# Django imports
|
# Django imports
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
@@ -22,10 +19,6 @@ from plane.db.models import (
|
|||||||
ProjectMember,
|
ProjectMember,
|
||||||
)
|
)
|
||||||
from .base import BaseSerializer
|
from .base import BaseSerializer
|
||||||
from .cycle import CycleSerializer, CycleLiteSerializer
|
|
||||||
from .module import ModuleSerializer, ModuleLiteSerializer
|
|
||||||
from .user import UserLiteSerializer
|
|
||||||
from .state import StateLiteSerializer
|
|
||||||
|
|
||||||
|
|
||||||
class IssueSerializer(BaseSerializer):
|
class IssueSerializer(BaseSerializer):
|
||||||
@@ -47,8 +40,8 @@ class IssueSerializer(BaseSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Issue
|
model = Issue
|
||||||
|
fields = "__all__"
|
||||||
read_only_fields = [
|
read_only_fields = [
|
||||||
"id",
|
|
||||||
"workspace",
|
"workspace",
|
||||||
"project",
|
"project",
|
||||||
"created_by",
|
"created_by",
|
||||||
@@ -56,10 +49,6 @@ class IssueSerializer(BaseSerializer):
|
|||||||
"created_at",
|
"created_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
]
|
]
|
||||||
exclude = [
|
|
||||||
"description",
|
|
||||||
"description_stripped",
|
|
||||||
]
|
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
if (
|
if (
|
||||||
@@ -67,24 +56,13 @@ class IssueSerializer(BaseSerializer):
|
|||||||
and data.get("target_date", None) is not None
|
and data.get("target_date", None) is not None
|
||||||
and data.get("start_date", None) > data.get("target_date", None)
|
and data.get("start_date", None) > data.get("target_date", None)
|
||||||
):
|
):
|
||||||
raise serializers.ValidationError(
|
raise serializers.ValidationError("Start date cannot exceed target date")
|
||||||
"Start date cannot exceed target date"
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
if data.get("description_html", None) is not None:
|
|
||||||
parsed = html.fromstring(data["description_html"])
|
|
||||||
parsed_str = html.tostring(parsed, encoding="unicode")
|
|
||||||
data["description_html"] = parsed_str
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
raise serializers.ValidationError(f"Invalid HTML: {str(e)}")
|
|
||||||
|
|
||||||
# Validate assignees are from project
|
# Validate assignees are from project
|
||||||
if data.get("assignees", []):
|
if data.get("assignees", []):
|
||||||
|
print(data.get("assignees"))
|
||||||
data["assignees"] = ProjectMember.objects.filter(
|
data["assignees"] = ProjectMember.objects.filter(
|
||||||
project_id=self.context.get("project_id"),
|
project_id=self.context.get("project_id"),
|
||||||
is_active=True,
|
|
||||||
member_id__in=data["assignees"],
|
member_id__in=data["assignees"],
|
||||||
).values_list("member_id", flat=True)
|
).values_list("member_id", flat=True)
|
||||||
|
|
||||||
@@ -99,8 +77,7 @@ class IssueSerializer(BaseSerializer):
|
|||||||
if (
|
if (
|
||||||
data.get("state")
|
data.get("state")
|
||||||
and not State.objects.filter(
|
and not State.objects.filter(
|
||||||
project_id=self.context.get("project_id"),
|
project_id=self.context.get("project_id"), pk=data.get("state")
|
||||||
pk=data.get("state").id,
|
|
||||||
).exists()
|
).exists()
|
||||||
):
|
):
|
||||||
raise serializers.ValidationError(
|
raise serializers.ValidationError(
|
||||||
@@ -111,8 +88,7 @@ class IssueSerializer(BaseSerializer):
|
|||||||
if (
|
if (
|
||||||
data.get("parent")
|
data.get("parent")
|
||||||
and not Issue.objects.filter(
|
and not Issue.objects.filter(
|
||||||
workspace_id=self.context.get("workspace_id"),
|
workspce_id=self.context.get("workspace_id"), pk=data.get("parent")
|
||||||
pk=data.get("parent").id,
|
|
||||||
).exists()
|
).exists()
|
||||||
):
|
):
|
||||||
raise serializers.ValidationError(
|
raise serializers.ValidationError(
|
||||||
@@ -243,13 +219,9 @@ class IssueSerializer(BaseSerializer):
|
|||||||
]
|
]
|
||||||
if "labels" in self.fields:
|
if "labels" in self.fields:
|
||||||
if "labels" in self.expand:
|
if "labels" in self.expand:
|
||||||
data["labels"] = LabelSerializer(
|
data["labels"] = LabelSerializer(instance.labels.all(), many=True).data
|
||||||
instance.labels.all(), many=True
|
|
||||||
).data
|
|
||||||
else:
|
else:
|
||||||
data["labels"] = [
|
data["labels"] = [str(label.id) for label in instance.labels.all()]
|
||||||
str(label.id) for label in instance.labels.all()
|
|
||||||
]
|
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@@ -259,13 +231,8 @@ class LabelSerializer(BaseSerializer):
|
|||||||
model = Label
|
model = Label
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
read_only_fields = [
|
read_only_fields = [
|
||||||
"id",
|
|
||||||
"workspace",
|
"workspace",
|
||||||
"project",
|
"project",
|
||||||
"created_by",
|
|
||||||
"updated_by",
|
|
||||||
"created_at",
|
|
||||||
"updated_at",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -274,21 +241,19 @@ class IssueLinkSerializer(BaseSerializer):
|
|||||||
model = IssueLink
|
model = IssueLink
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
read_only_fields = [
|
read_only_fields = [
|
||||||
"id",
|
|
||||||
"workspace",
|
"workspace",
|
||||||
"project",
|
"project",
|
||||||
"issue",
|
|
||||||
"created_by",
|
"created_by",
|
||||||
"updated_by",
|
"updated_by",
|
||||||
"created_at",
|
"created_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
|
"issue",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Validation if url already exists
|
# Validation if url already exists
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
if IssueLink.objects.filter(
|
if IssueLink.objects.filter(
|
||||||
url=validated_data.get("url"),
|
url=validated_data.get("url"), issue_id=validated_data.get("issue_id")
|
||||||
issue_id=validated_data.get("issue_id"),
|
|
||||||
).exists():
|
).exists():
|
||||||
raise serializers.ValidationError(
|
raise serializers.ValidationError(
|
||||||
{"error": "URL already exists for this Issue"}
|
{"error": "URL already exists for this Issue"}
|
||||||
@@ -301,14 +266,13 @@ class IssueAttachmentSerializer(BaseSerializer):
|
|||||||
model = IssueAttachment
|
model = IssueAttachment
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
read_only_fields = [
|
read_only_fields = [
|
||||||
"id",
|
|
||||||
"workspace",
|
|
||||||
"project",
|
|
||||||
"issue",
|
|
||||||
"created_by",
|
"created_by",
|
||||||
"updated_by",
|
"updated_by",
|
||||||
"created_at",
|
"created_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
|
"workspace",
|
||||||
|
"project",
|
||||||
|
"issue",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -317,8 +281,8 @@ class IssueCommentSerializer(BaseSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IssueComment
|
model = IssueComment
|
||||||
|
fields = "__all__"
|
||||||
read_only_fields = [
|
read_only_fields = [
|
||||||
"id",
|
|
||||||
"workspace",
|
"workspace",
|
||||||
"project",
|
"project",
|
||||||
"issue",
|
"issue",
|
||||||
@@ -327,76 +291,29 @@ class IssueCommentSerializer(BaseSerializer):
|
|||||||
"created_at",
|
"created_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
]
|
]
|
||||||
exclude = [
|
|
||||||
"comment_stripped",
|
|
||||||
"comment_json",
|
class IssueAttachmentSerializer(BaseSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = IssueAttachment
|
||||||
|
fields = "__all__"
|
||||||
|
read_only_fields = [
|
||||||
|
"id",
|
||||||
|
"created_by",
|
||||||
|
"updated_by",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
"workspace",
|
||||||
|
"project",
|
||||||
|
"issue",
|
||||||
]
|
]
|
||||||
|
|
||||||
def validate(self, data):
|
|
||||||
try:
|
|
||||||
if data.get("comment_html", None) is not None:
|
|
||||||
parsed = html.fromstring(data["comment_html"])
|
|
||||||
parsed_str = html.tostring(parsed, encoding="unicode")
|
|
||||||
data["comment_html"] = parsed_str
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
raise serializers.ValidationError(f"Invalid HTML: {str(e)}")
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
class IssueActivitySerializer(BaseSerializer):
|
class IssueActivitySerializer(BaseSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IssueActivity
|
model = IssueActivity
|
||||||
|
fields = "__all__"
|
||||||
exclude = [
|
exclude = [
|
||||||
"created_by",
|
"created_by",
|
||||||
"updated_by",
|
"udpated_by",
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class CycleIssueSerializer(BaseSerializer):
|
|
||||||
cycle = CycleSerializer(read_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
fields = [
|
|
||||||
"cycle",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class ModuleIssueSerializer(BaseSerializer):
|
|
||||||
module = ModuleSerializer(read_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
fields = [
|
|
||||||
"module",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class LabelLiteSerializer(BaseSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = Label
|
|
||||||
fields = [
|
|
||||||
"id",
|
|
||||||
"name",
|
|
||||||
"color",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class IssueExpandSerializer(BaseSerializer):
|
|
||||||
cycle = CycleLiteSerializer(source="issue_cycle.cycle", read_only=True)
|
|
||||||
module = ModuleLiteSerializer(source="issue_module.module", read_only=True)
|
|
||||||
labels = LabelLiteSerializer(read_only=True, many=True)
|
|
||||||
assignees = UserLiteSerializer(read_only=True, many=True)
|
|
||||||
state = StateLiteSerializer(read_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Issue
|
|
||||||
fields = "__all__"
|
|
||||||
read_only_fields = [
|
|
||||||
"id",
|
|
||||||
"workspace",
|
|
||||||
"project",
|
|
||||||
"created_by",
|
|
||||||
"updated_by",
|
|
||||||
"created_at",
|
|
||||||
"updated_at",
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ class ModuleSerializer(BaseSerializer):
|
|||||||
write_only=True,
|
write_only=True,
|
||||||
required=False,
|
required=False,
|
||||||
)
|
)
|
||||||
|
is_favorite = serializers.BooleanField(read_only=True)
|
||||||
total_issues = serializers.IntegerField(read_only=True)
|
total_issues = serializers.IntegerField(read_only=True)
|
||||||
cancelled_issues = serializers.IntegerField(read_only=True)
|
cancelled_issues = serializers.IntegerField(read_only=True)
|
||||||
completed_issues = serializers.IntegerField(read_only=True)
|
completed_issues = serializers.IntegerField(read_only=True)
|
||||||
@@ -32,7 +33,6 @@ class ModuleSerializer(BaseSerializer):
|
|||||||
model = Module
|
model = Module
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
read_only_fields = [
|
read_only_fields = [
|
||||||
"id",
|
|
||||||
"workspace",
|
"workspace",
|
||||||
"project",
|
"project",
|
||||||
"created_by",
|
"created_by",
|
||||||
@@ -52,11 +52,10 @@ class ModuleSerializer(BaseSerializer):
|
|||||||
and data.get("target_date", None) is not None
|
and data.get("target_date", None) is not None
|
||||||
and data.get("start_date", None) > data.get("target_date", None)
|
and data.get("start_date", None) > data.get("target_date", None)
|
||||||
):
|
):
|
||||||
raise serializers.ValidationError(
|
raise serializers.ValidationError("Start date cannot exceed target date")
|
||||||
"Start date cannot exceed target date"
|
|
||||||
)
|
|
||||||
|
|
||||||
if data.get("members", []):
|
if data.get("members", []):
|
||||||
|
print(data.get("members"))
|
||||||
data["members"] = ProjectMember.objects.filter(
|
data["members"] = ProjectMember.objects.filter(
|
||||||
project_id=self.context.get("project_id"),
|
project_id=self.context.get("project_id"),
|
||||||
member_id__in=data["members"],
|
member_id__in=data["members"],
|
||||||
@@ -67,18 +66,18 @@ class ModuleSerializer(BaseSerializer):
|
|||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
members = validated_data.pop("members", None)
|
members = validated_data.pop("members", None)
|
||||||
|
|
||||||
project_id = self.context["project_id"]
|
project = self.context["project"]
|
||||||
workspace_id = self.context["workspace_id"]
|
|
||||||
|
module = Module.objects.create(**validated_data, project=project)
|
||||||
|
|
||||||
module = Module.objects.create(**validated_data, project_id=project_id)
|
|
||||||
if members is not None:
|
if members is not None:
|
||||||
ModuleMember.objects.bulk_create(
|
ModuleMember.objects.bulk_create(
|
||||||
[
|
[
|
||||||
ModuleMember(
|
ModuleMember(
|
||||||
module=module,
|
module=module,
|
||||||
member_id=str(member),
|
member=member,
|
||||||
project_id=project_id,
|
project=project,
|
||||||
workspace_id=workspace_id,
|
workspace=project.workspace,
|
||||||
created_by=module.created_by,
|
created_by=module.created_by,
|
||||||
updated_by=module.updated_by,
|
updated_by=module.updated_by,
|
||||||
)
|
)
|
||||||
@@ -99,7 +98,7 @@ class ModuleSerializer(BaseSerializer):
|
|||||||
[
|
[
|
||||||
ModuleMember(
|
ModuleMember(
|
||||||
module=instance,
|
module=instance,
|
||||||
member_id=str(member),
|
member=member,
|
||||||
project=instance.project,
|
project=instance.project,
|
||||||
workspace=instance.project.workspace,
|
workspace=instance.project.workspace,
|
||||||
created_by=instance.created_by,
|
created_by=instance.created_by,
|
||||||
@@ -148,16 +147,9 @@ class ModuleLinkSerializer(BaseSerializer):
|
|||||||
# Validation if url already exists
|
# Validation if url already exists
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
if ModuleLink.objects.filter(
|
if ModuleLink.objects.filter(
|
||||||
url=validated_data.get("url"),
|
url=validated_data.get("url"), module_id=validated_data.get("module_id")
|
||||||
module_id=validated_data.get("module_id"),
|
|
||||||
).exists():
|
).exists():
|
||||||
raise serializers.ValidationError(
|
raise serializers.ValidationError(
|
||||||
{"error": "URL already exists for this Issue"}
|
{"error": "URL already exists for this Issue"}
|
||||||
)
|
)
|
||||||
return ModuleLink.objects.create(**validated_data)
|
return ModuleLink.objects.create(**validated_data)
|
||||||
|
|
||||||
|
|
||||||
class ModuleLiteSerializer(BaseSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = Module
|
|
||||||
fields = "__all__"
|
|
||||||
@@ -2,17 +2,12 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from plane.db.models import (
|
from plane.db.models import Project, ProjectIdentifier, WorkspaceMember, State, Estimate
|
||||||
Project,
|
|
||||||
ProjectIdentifier,
|
|
||||||
WorkspaceMember,
|
|
||||||
State,
|
|
||||||
Estimate,
|
|
||||||
)
|
|
||||||
from .base import BaseSerializer
|
from .base import BaseSerializer
|
||||||
|
|
||||||
|
|
||||||
class ProjectSerializer(BaseSerializer):
|
class ProjectSerializer(BaseSerializer):
|
||||||
|
|
||||||
total_members = serializers.IntegerField(read_only=True)
|
total_members = serializers.IntegerField(read_only=True)
|
||||||
total_cycles = serializers.IntegerField(read_only=True)
|
total_cycles = serializers.IntegerField(read_only=True)
|
||||||
total_modules = serializers.IntegerField(read_only=True)
|
total_modules = serializers.IntegerField(read_only=True)
|
||||||
@@ -25,13 +20,8 @@ class ProjectSerializer(BaseSerializer):
|
|||||||
model = Project
|
model = Project
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
read_only_fields = [
|
read_only_fields = [
|
||||||
"id",
|
|
||||||
"emoji",
|
|
||||||
"workspace",
|
"workspace",
|
||||||
"created_at",
|
"id",
|
||||||
"updated_at",
|
|
||||||
"created_by",
|
|
||||||
"updated_by",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
@@ -64,16 +54,12 @@ class ProjectSerializer(BaseSerializer):
|
|||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
identifier = validated_data.get("identifier", "").strip().upper()
|
identifier = validated_data.get("identifier", "").strip().upper()
|
||||||
if identifier == "":
|
if identifier == "":
|
||||||
raise serializers.ValidationError(
|
raise serializers.ValidationError(detail="Project Identifier is required")
|
||||||
detail="Project Identifier is required"
|
|
||||||
)
|
|
||||||
|
|
||||||
if ProjectIdentifier.objects.filter(
|
if ProjectIdentifier.objects.filter(
|
||||||
name=identifier, workspace_id=self.context["workspace_id"]
|
name=identifier, workspace_id=self.context["workspace_id"]
|
||||||
).exists():
|
).exists():
|
||||||
raise serializers.ValidationError(
|
raise serializers.ValidationError(detail="Project Identifier is taken")
|
||||||
detail="Project Identifier is taken"
|
|
||||||
)
|
|
||||||
|
|
||||||
project = Project.objects.create(
|
project = Project.objects.create(
|
||||||
**validated_data, workspace_id=self.context["workspace_id"]
|
**validated_data, workspace_id=self.context["workspace_id"]
|
||||||
@@ -98,4 +84,4 @@ class ProjectLiteSerializer(BaseSerializer):
|
|||||||
"emoji",
|
"emoji",
|
||||||
"description",
|
"description",
|
||||||
]
|
]
|
||||||
read_only_fields = fields
|
read_only_fields = fields
|
||||||
@@ -7,20 +7,15 @@ class StateSerializer(BaseSerializer):
|
|||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
# If the default is being provided then make all other states default False
|
# If the default is being provided then make all other states default False
|
||||||
if data.get("default", False):
|
if data.get("default", False):
|
||||||
State.objects.filter(
|
State.objects.filter(project_id=self.context.get("project_id")).update(
|
||||||
project_id=self.context.get("project_id")
|
default=False
|
||||||
).update(default=False)
|
)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = State
|
model = State
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
read_only_fields = [
|
read_only_fields = [
|
||||||
"id",
|
|
||||||
"created_by",
|
|
||||||
"updated_by",
|
|
||||||
"created_at",
|
|
||||||
"updated_at",
|
|
||||||
"workspace",
|
"workspace",
|
||||||
"project",
|
"project",
|
||||||
]
|
]
|
||||||
@@ -35,4 +30,4 @@ class StateLiteSerializer(BaseSerializer):
|
|||||||
"color",
|
"color",
|
||||||
"group",
|
"group",
|
||||||
]
|
]
|
||||||
read_only_fields = fields
|
read_only_fields = fields
|
||||||
@@ -11,6 +11,10 @@ class UserLiteSerializer(BaseSerializer):
|
|||||||
"first_name",
|
"first_name",
|
||||||
"last_name",
|
"last_name",
|
||||||
"avatar",
|
"avatar",
|
||||||
|
"is_bot",
|
||||||
"display_name",
|
"display_name",
|
||||||
]
|
]
|
||||||
read_only_fields = fields
|
read_only_fields = [
|
||||||
|
"id",
|
||||||
|
"is_bot",
|
||||||
|
]
|
||||||
@@ -5,7 +5,6 @@ from .base import BaseSerializer
|
|||||||
|
|
||||||
class WorkspaceLiteSerializer(BaseSerializer):
|
class WorkspaceLiteSerializer(BaseSerializer):
|
||||||
"""Lite serializer with only required fields"""
|
"""Lite serializer with only required fields"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Workspace
|
model = Workspace
|
||||||
fields = [
|
fields = [
|
||||||
@@ -13,4 +12,4 @@ class WorkspaceLiteSerializer(BaseSerializer):
|
|||||||
"slug",
|
"slug",
|
||||||
"id",
|
"id",
|
||||||
]
|
]
|
||||||
read_only_fields = fields
|
read_only_fields = fields
|
||||||
@@ -12,4 +12,4 @@ urlpatterns = [
|
|||||||
*cycle_patterns,
|
*cycle_patterns,
|
||||||
*module_patterns,
|
*module_patterns,
|
||||||
*inbox_patterns,
|
*inbox_patterns,
|
||||||
]
|
]
|
||||||
@@ -13,7 +13,7 @@ urlpatterns = [
|
|||||||
name="cycles",
|
name="cycles",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:pk>/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/",
|
||||||
CycleAPIEndpoint.as_view(),
|
CycleAPIEndpoint.as_view(),
|
||||||
name="cycles",
|
name="cycles",
|
||||||
),
|
),
|
||||||
@@ -23,7 +23,7 @@ urlpatterns = [
|
|||||||
name="cycle-issues",
|
name="cycle-issues",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/cycle-issues/<uuid:issue_id>/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/cycle-issues/<uuid:pk>/",
|
||||||
CycleIssueAPIEndpoint.as_view(),
|
CycleIssueAPIEndpoint.as_view(),
|
||||||
name="cycle-issues",
|
name="cycle-issues",
|
||||||
),
|
),
|
||||||
@@ -32,4 +32,4 @@ urlpatterns = [
|
|||||||
TransferCycleIssueAPIEndpoint.as_view(),
|
TransferCycleIssueAPIEndpoint.as_view(),
|
||||||
name="transfer-issues",
|
name="transfer-issues",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
@@ -5,13 +5,13 @@ from plane.api.views import InboxIssueAPIEndpoint
|
|||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/<uuid:inbox_id>/inbox-issues/",
|
||||||
InboxIssueAPIEndpoint.as_view(),
|
InboxIssueAPIEndpoint.as_view(),
|
||||||
name="inbox-issue",
|
name="inbox-issue",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/<uuid:issue_id>/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/<uuid:inbox_id>/inbox-issues/<uuid:pk>/",
|
||||||
InboxIssueAPIEndpoint.as_view(),
|
InboxIssueAPIEndpoint.as_view(),
|
||||||
name="inbox-issue",
|
name="inbox-issue",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
@@ -15,27 +15,27 @@ urlpatterns = [
|
|||||||
name="issue",
|
name="issue",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:pk>/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/",
|
||||||
IssueAPIEndpoint.as_view(),
|
IssueAPIEndpoint.as_view(),
|
||||||
name="issue",
|
name="issue",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/labels/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-labels/",
|
||||||
LabelAPIEndpoint.as_view(),
|
LabelAPIEndpoint.as_view(),
|
||||||
name="label",
|
name="label",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/labels/<uuid:pk>/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-labels/<uuid:pk>/",
|
||||||
LabelAPIEndpoint.as_view(),
|
LabelAPIEndpoint.as_view(),
|
||||||
name="label",
|
name="label",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/links/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-links/",
|
||||||
IssueLinkAPIEndpoint.as_view(),
|
IssueLinkAPIEndpoint.as_view(),
|
||||||
name="link",
|
name="link",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/links/<uuid:pk>/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-links/<uuid:pk>/",
|
||||||
IssueLinkAPIEndpoint.as_view(),
|
IssueLinkAPIEndpoint.as_view(),
|
||||||
name="link",
|
name="link",
|
||||||
),
|
),
|
||||||
@@ -50,12 +50,12 @@ urlpatterns = [
|
|||||||
name="comment",
|
name="comment",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/activities/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/activites/",
|
||||||
IssueActivityAPIEndpoint.as_view(),
|
IssueActivityAPIEndpoint.as_view(),
|
||||||
name="activity",
|
name="activity",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/activities/<uuid:pk>/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/activites/<uuid:pk>/",
|
||||||
IssueActivityAPIEndpoint.as_view(),
|
IssueActivityAPIEndpoint.as_view(),
|
||||||
name="activity",
|
name="activity",
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ urlpatterns = [
|
|||||||
name="module-issues",
|
name="module-issues",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/module-issues/<uuid:issue_id>/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/module-issues/<uuid:pk>/",
|
||||||
ModuleIssueAPIEndpoint.as_view(),
|
ModuleIssueAPIEndpoint.as_view(),
|
||||||
name="module-issues",
|
name="module-issues",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
@@ -3,14 +3,14 @@ from django.urls import path
|
|||||||
from plane.api.views import ProjectAPIEndpoint
|
from plane.api.views import ProjectAPIEndpoint
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/",
|
"workspaces/<str:slug>/projects/",
|
||||||
ProjectAPIEndpoint.as_view(),
|
ProjectAPIEndpoint.as_view(),
|
||||||
name="project",
|
name="project",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/",
|
"workspaces/<str:slug>/projects/<uuid:pk>/",
|
||||||
ProjectAPIEndpoint.as_view(),
|
ProjectAPIEndpoint.as_view(),
|
||||||
name="project",
|
name="project",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
@@ -8,9 +8,4 @@ urlpatterns = [
|
|||||||
StateAPIEndpoint.as_view(),
|
StateAPIEndpoint.as_view(),
|
||||||
name="states",
|
name="states",
|
||||||
),
|
),
|
||||||
path(
|
]
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/states/<uuid:state_id>/",
|
|
||||||
StateAPIEndpoint.as_view(),
|
|
||||||
name="states",
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -18,4 +18,4 @@ from .cycle import (
|
|||||||
|
|
||||||
from .module import ModuleAPIEndpoint, ModuleIssueAPIEndpoint
|
from .module import ModuleAPIEndpoint, ModuleIssueAPIEndpoint
|
||||||
|
|
||||||
from .inbox import InboxIssueAPIEndpoint
|
from .inbox import InboxIssueAPIEndpoint
|
||||||
@@ -7,6 +7,7 @@ from django.conf import settings
|
|||||||
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.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
|
|
||||||
# Third party imports
|
# Third party imports
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
@@ -35,35 +36,28 @@ class TimezoneMixin:
|
|||||||
else:
|
else:
|
||||||
timezone.deactivate()
|
timezone.deactivate()
|
||||||
|
|
||||||
|
|
||||||
class WebhookMixin:
|
class WebhookMixin:
|
||||||
webhook_event = None
|
webhook_event = None
|
||||||
bulk = False
|
|
||||||
|
|
||||||
def finalize_response(self, request, response, *args, **kwargs):
|
def finalize_response(self, request, response, *args, **kwargs):
|
||||||
response = super().finalize_response(
|
response = super().finalize_response(request, response, *args, **kwargs)
|
||||||
request, response, *args, **kwargs
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check for the case should webhook be sent
|
|
||||||
if (
|
if (
|
||||||
self.webhook_event
|
self.webhook_event
|
||||||
and self.request.method in ["POST", "PATCH", "DELETE"]
|
and self.request.method in ["POST", "PATCH", "DELETE"]
|
||||||
and response.status_code in [200, 201, 204]
|
and response.status_code in [200, 201, 204]
|
||||||
):
|
):
|
||||||
# Push the object to delay
|
|
||||||
send_webhook.delay(
|
send_webhook.delay(
|
||||||
event=self.webhook_event,
|
event=self.webhook_event,
|
||||||
payload=response.data,
|
event_data=json.dumps(response.data, cls=DjangoJSONEncoder),
|
||||||
kw=self.kwargs,
|
|
||||||
action=self.request.method,
|
action=self.request.method,
|
||||||
slug=self.workspace_slug,
|
slug=self.workspace_slug,
|
||||||
bulk=self.bulk,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
|
class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
|
||||||
authentication_classes = [
|
authentication_classes = [
|
||||||
APIKeyAuthentication,
|
APIKeyAuthentication,
|
||||||
@@ -106,14 +100,15 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if isinstance(e, ObjectDoesNotExist):
|
if isinstance(e, ObjectDoesNotExist):
|
||||||
|
model_name = str(exc).split(" matching query does not exist.")[0]
|
||||||
return Response(
|
return Response(
|
||||||
{"error": f"The required object does not exist."},
|
{"error": f"{model_name} does not exist."},
|
||||||
status=status.HTTP_404_NOT_FOUND,
|
status=status.HTTP_404_NOT_FOUND,
|
||||||
)
|
)
|
||||||
|
|
||||||
if isinstance(e, KeyError):
|
if isinstance(e, KeyError):
|
||||||
return Response(
|
return Response(
|
||||||
{"error": f" The required key does not exist."},
|
{"error": f"key {e} does not exist"},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -141,18 +136,16 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
|
|||||||
|
|
||||||
def finalize_response(self, request, response, *args, **kwargs):
|
def finalize_response(self, request, response, *args, **kwargs):
|
||||||
# Call super to get the default response
|
# Call super to get the default response
|
||||||
response = super().finalize_response(
|
response = super().finalize_response(request, response, *args, **kwargs)
|
||||||
request, response, *args, **kwargs
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add custom headers if they exist in the request META
|
# Add custom headers if they exist in the request META
|
||||||
ratelimit_remaining = request.META.get("X-RateLimit-Remaining")
|
ratelimit_remaining = request.META.get('X-RateLimit-Remaining')
|
||||||
if ratelimit_remaining is not None:
|
if ratelimit_remaining is not None:
|
||||||
response["X-RateLimit-Remaining"] = ratelimit_remaining
|
response['X-RateLimit-Remaining'] = ratelimit_remaining
|
||||||
|
|
||||||
ratelimit_reset = request.META.get("X-RateLimit-Reset")
|
ratelimit_reset = request.META.get('X-RateLimit-Reset')
|
||||||
if ratelimit_reset is not None:
|
if ratelimit_reset is not None:
|
||||||
response["X-RateLimit-Reset"] = ratelimit_reset
|
response['X-RateLimit-Reset'] = ratelimit_reset
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@@ -167,17 +160,13 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
|
|||||||
@property
|
@property
|
||||||
def fields(self):
|
def fields(self):
|
||||||
fields = [
|
fields = [
|
||||||
field
|
field for field in self.request.GET.get("fields", "").split(",") if field
|
||||||
for field in self.request.GET.get("fields", "").split(",")
|
|
||||||
if field
|
|
||||||
]
|
]
|
||||||
return fields if fields else None
|
return fields if fields else None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def expand(self):
|
def expand(self):
|
||||||
expand = [
|
expand = [
|
||||||
expand
|
expand for expand in self.request.GET.get("expand", "").split(",") if expand
|
||||||
for expand in self.request.GET.get("expand", "").split(",")
|
|
||||||
if expand
|
|
||||||
]
|
]
|
||||||
return expand if expand else None
|
return expand if expand else None
|
||||||
@@ -12,17 +12,12 @@ from rest_framework import status
|
|||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from .base import BaseAPIView, WebhookMixin
|
from .base import BaseAPIView, WebhookMixin
|
||||||
from plane.db.models import (
|
from plane.db.models import Cycle, Issue, CycleIssue, IssueLink, IssueAttachment
|
||||||
Cycle,
|
|
||||||
Issue,
|
|
||||||
CycleIssue,
|
|
||||||
IssueLink,
|
|
||||||
IssueAttachment,
|
|
||||||
)
|
|
||||||
from plane.app.permissions import ProjectEntityPermission
|
from plane.app.permissions import ProjectEntityPermission
|
||||||
from plane.api.serializers import (
|
from plane.api.serializers import (
|
||||||
CycleSerializer,
|
CycleSerializer,
|
||||||
CycleIssueSerializer,
|
CycleIssueSerializer,
|
||||||
|
IssueSerializer,
|
||||||
)
|
)
|
||||||
from plane.bgtasks.issue_activites_task import issue_activity
|
from plane.bgtasks.issue_activites_task import issue_activity
|
||||||
|
|
||||||
@@ -108,9 +103,7 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(total_estimates=Sum("issue_cycle__issue__estimate_point"))
|
||||||
total_estimates=Sum("issue_cycle__issue__estimate_point")
|
|
||||||
)
|
|
||||||
.annotate(
|
.annotate(
|
||||||
completed_estimates=Sum(
|
completed_estimates=Sum(
|
||||||
"issue_cycle__issue__estimate_point",
|
"issue_cycle__issue__estimate_point",
|
||||||
@@ -149,6 +142,7 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
)
|
)
|
||||||
queryset = self.get_queryset()
|
queryset = self.get_queryset()
|
||||||
cycle_view = request.GET.get("cycle_view", "all")
|
cycle_view = request.GET.get("cycle_view", "all")
|
||||||
|
queryset = queryset.order_by("-is_favorite", "-created_at")
|
||||||
|
|
||||||
# Current Cycle
|
# Current Cycle
|
||||||
if cycle_view == "current":
|
if cycle_view == "current":
|
||||||
@@ -209,8 +203,7 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
# Incomplete Cycles
|
# Incomplete Cycles
|
||||||
if cycle_view == "incomplete":
|
if cycle_view == "incomplete":
|
||||||
queryset = queryset.filter(
|
queryset = queryset.filter(
|
||||||
Q(end_date__gte=timezone.now().date())
|
Q(end_date__gte=timezone.now().date()) | Q(end_date__isnull=True),
|
||||||
| Q(end_date__isnull=True),
|
|
||||||
)
|
)
|
||||||
return self.paginate(
|
return self.paginate(
|
||||||
request=request,
|
request=request,
|
||||||
@@ -243,39 +236,12 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
):
|
):
|
||||||
serializer = CycleSerializer(data=request.data)
|
serializer = CycleSerializer(data=request.data)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
if (
|
|
||||||
request.data.get("external_id")
|
|
||||||
and request.data.get("external_source")
|
|
||||||
and Cycle.objects.filter(
|
|
||||||
project_id=project_id,
|
|
||||||
workspace__slug=slug,
|
|
||||||
external_source=request.data.get("external_source"),
|
|
||||||
external_id=request.data.get("external_id"),
|
|
||||||
).exists()
|
|
||||||
):
|
|
||||||
cycle = Cycle.objects.filter(
|
|
||||||
workspace__slug=slug,
|
|
||||||
project_id=project_id,
|
|
||||||
external_source=request.data.get("external_source"),
|
|
||||||
external_id=request.data.get("external_id"),
|
|
||||||
).first()
|
|
||||||
return Response(
|
|
||||||
{
|
|
||||||
"error": "Cycle with the same external id and external source already exists",
|
|
||||||
"id": str(cycle.id),
|
|
||||||
},
|
|
||||||
status=status.HTTP_409_CONFLICT,
|
|
||||||
)
|
|
||||||
serializer.save(
|
serializer.save(
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
owned_by=request.user,
|
owned_by=request.user,
|
||||||
)
|
)
|
||||||
return Response(
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
serializer.data, status=status.HTTP_201_CREATED
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
)
|
|
||||||
return Response(
|
|
||||||
serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
@@ -285,22 +251,15 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def patch(self, request, slug, project_id, pk):
|
def patch(self, request, slug, project_id, pk):
|
||||||
cycle = Cycle.objects.get(
|
cycle = Cycle.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
|
||||||
workspace__slug=slug, project_id=project_id, pk=pk
|
|
||||||
)
|
|
||||||
|
|
||||||
request_data = request.data
|
request_data = request.data
|
||||||
|
|
||||||
if (
|
if cycle.end_date is not None and cycle.end_date < timezone.now().date():
|
||||||
cycle.end_date is not None
|
|
||||||
and cycle.end_date < timezone.now().date()
|
|
||||||
):
|
|
||||||
if "sort_order" in request_data:
|
if "sort_order" in request_data:
|
||||||
# Can only change sort order
|
# Can only change sort order
|
||||||
request_data = {
|
request_data = {
|
||||||
"sort_order": request_data.get(
|
"sort_order": request_data.get("sort_order", cycle.sort_order)
|
||||||
"sort_order", cycle.sort_order
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
return Response(
|
return Response(
|
||||||
@@ -312,36 +271,17 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
|
|
||||||
serializer = CycleSerializer(cycle, data=request.data, partial=True)
|
serializer = CycleSerializer(cycle, data=request.data, partial=True)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
if (
|
|
||||||
request.data.get("external_id")
|
|
||||||
and (cycle.external_id != request.data.get("external_id"))
|
|
||||||
and Cycle.objects.filter(
|
|
||||||
project_id=project_id,
|
|
||||||
workspace__slug=slug,
|
|
||||||
external_source=request.data.get("external_source", cycle.external_source),
|
|
||||||
external_id=request.data.get("external_id"),
|
|
||||||
).exists()
|
|
||||||
):
|
|
||||||
return Response(
|
|
||||||
{
|
|
||||||
"error": "Cycle with the same external id and external source already exists",
|
|
||||||
"id": str(cycle.id),
|
|
||||||
},
|
|
||||||
status=status.HTTP_409_CONFLICT,
|
|
||||||
)
|
|
||||||
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)
|
||||||
|
|
||||||
def delete(self, request, slug, project_id, pk):
|
def delete(self, request, slug, project_id, pk):
|
||||||
cycle_issues = list(
|
cycle_issues = list(
|
||||||
CycleIssue.objects.filter(
|
CycleIssue.objects.filter(cycle_id=self.kwargs.get("pk")).values_list(
|
||||||
cycle_id=self.kwargs.get("pk")
|
"issue", flat=True
|
||||||
).values_list("issue", flat=True)
|
)
|
||||||
)
|
|
||||||
cycle = Cycle.objects.get(
|
|
||||||
workspace__slug=slug, project_id=project_id, pk=pk
|
|
||||||
)
|
)
|
||||||
|
cycle = Cycle.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
|
||||||
|
|
||||||
issue_activity.delay(
|
issue_activity.delay(
|
||||||
type="cycle.activity.deleted",
|
type="cycle.activity.deleted",
|
||||||
@@ -353,7 +293,7 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
}
|
}
|
||||||
),
|
),
|
||||||
actor_id=str(request.user.id),
|
actor_id=str(request.user.id),
|
||||||
issue_id=None,
|
issue_id=str(pk),
|
||||||
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()),
|
||||||
@@ -365,15 +305,14 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
|
|
||||||
class CycleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
class CycleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||||
"""
|
"""
|
||||||
This viewset automatically provides `list`, `create`,
|
This viewset automatically provides `list`, `create`, `retrieve`,
|
||||||
and `destroy` actions related to cycle issues.
|
`update` and `destroy` actions related to cycle issues.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
serializer_class = CycleIssueSerializer
|
serializer_class = CycleIssueSerializer
|
||||||
model = CycleIssue
|
model = CycleIssue
|
||||||
webhook_event = "cycle_issue"
|
webhook_event = "cycle"
|
||||||
bulk = True
|
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
ProjectEntityPermission,
|
ProjectEntityPermission,
|
||||||
]
|
]
|
||||||
@@ -381,9 +320,7 @@ class CycleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return (
|
return (
|
||||||
CycleIssue.objects.annotate(
|
CycleIssue.objects.annotate(
|
||||||
sub_issues_count=Issue.issue_objects.filter(
|
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("issue_id"))
|
||||||
parent=OuterRef("issue_id")
|
|
||||||
)
|
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
@@ -406,9 +343,7 @@ class CycleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
issues = (
|
issues = (
|
||||||
Issue.issue_objects.filter(issue_cycle__cycle_id=cycle_id)
|
Issue.issue_objects.filter(issue_cycle__cycle_id=cycle_id)
|
||||||
.annotate(
|
.annotate(
|
||||||
sub_issues_count=Issue.issue_objects.filter(
|
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
|
||||||
parent=OuterRef("id")
|
|
||||||
)
|
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
@@ -430,9 +365,7 @@ class CycleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
.values("count")
|
.values("count")
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
attachment_count=IssueAttachment.objects.filter(
|
attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id"))
|
||||||
issue=OuterRef("id")
|
|
||||||
)
|
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
@@ -455,18 +388,14 @@ class CycleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
|
|
||||||
if not issues:
|
if not issues:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Issues are required"},
|
{"error": "Issues are required"}, status=status.HTTP_400_BAD_REQUEST
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
cycle = Cycle.objects.get(
|
cycle = Cycle.objects.get(
|
||||||
workspace__slug=slug, project_id=project_id, pk=cycle_id
|
workspace__slug=slug, project_id=project_id, pk=cycle_id
|
||||||
)
|
)
|
||||||
|
|
||||||
if (
|
if cycle.end_date is not None and cycle.end_date < timezone.now().date():
|
||||||
cycle.end_date is not None
|
|
||||||
and cycle.end_date < timezone.now().date()
|
|
||||||
):
|
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
"error": "The Cycle has already been completed so no new issues can be added"
|
"error": "The Cycle has already been completed so no new issues can be added"
|
||||||
@@ -528,7 +457,7 @@ class CycleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
# Capture Issue Activity
|
# Capture Issue Activity
|
||||||
issue_activity.delay(
|
issue_activity.delay(
|
||||||
type="cycle.activity.created",
|
type="cycle.activity.created",
|
||||||
requested_data=json.dumps({"cycles_list": str(issues)}),
|
requested_data=json.dumps({"cycles_list": issues}),
|
||||||
actor_id=str(self.request.user.id),
|
actor_id=str(self.request.user.id),
|
||||||
issue_id=None,
|
issue_id=None,
|
||||||
project_id=str(self.kwargs.get("project_id", None)),
|
project_id=str(self.kwargs.get("project_id", None)),
|
||||||
@@ -549,12 +478,9 @@ class CycleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
status=status.HTTP_200_OK,
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
|
|
||||||
def delete(self, request, slug, project_id, cycle_id, issue_id):
|
def delete(self, request, slug, project_id, cycle_id, pk):
|
||||||
cycle_issue = CycleIssue.objects.get(
|
cycle_issue = CycleIssue.objects.get(
|
||||||
issue_id=issue_id,
|
pk=pk, workspace__slug=slug, project_id=project_id, cycle_id=cycle_id
|
||||||
workspace__slug=slug,
|
|
||||||
project_id=project_id,
|
|
||||||
cycle_id=cycle_id,
|
|
||||||
)
|
)
|
||||||
issue_id = cycle_issue.issue_id
|
issue_id = cycle_issue.issue_id
|
||||||
cycle_issue.delete()
|
cycle_issue.delete()
|
||||||
@@ -567,7 +493,7 @@ class CycleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
}
|
}
|
||||||
),
|
),
|
||||||
actor_id=str(self.request.user.id),
|
actor_id=str(self.request.user.id),
|
||||||
issue_id=str(issue_id),
|
issue_id=str(self.kwargs.get("pk", None)),
|
||||||
project_id=str(self.kwargs.get("project_id", None)),
|
project_id=str(self.kwargs.get("project_id", None)),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
epoch=int(timezone.now().timestamp()),
|
epoch=int(timezone.now().timestamp()),
|
||||||
@@ -625,4 +551,4 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
|
|||||||
updated_cycles, ["cycle_id"], batch_size=100
|
updated_cycles, ["cycle_id"], batch_size=100
|
||||||
)
|
)
|
||||||
|
|
||||||
return Response({"message": "Success"}, status=status.HTTP_200_OK)
|
return Response({"message": "Success"}, status=status.HTTP_200_OK)
|
||||||
@@ -14,14 +14,7 @@ from rest_framework.response import Response
|
|||||||
from .base import BaseAPIView
|
from .base import BaseAPIView
|
||||||
from plane.app.permissions import ProjectLitePermission
|
from plane.app.permissions import ProjectLitePermission
|
||||||
from plane.api.serializers import InboxIssueSerializer, IssueSerializer
|
from plane.api.serializers import InboxIssueSerializer, IssueSerializer
|
||||||
from plane.db.models import (
|
from plane.db.models import InboxIssue, Issue, State, ProjectMember
|
||||||
InboxIssue,
|
|
||||||
Issue,
|
|
||||||
State,
|
|
||||||
ProjectMember,
|
|
||||||
Project,
|
|
||||||
Inbox,
|
|
||||||
)
|
|
||||||
from plane.bgtasks.issue_activites_task import issue_activity
|
from plane.bgtasks.issue_activites_task import issue_activity
|
||||||
|
|
||||||
|
|
||||||
@@ -44,41 +37,29 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
inbox = Inbox.objects.filter(
|
return self.filter_queryset(
|
||||||
workspace__slug=self.kwargs.get("slug"),
|
super()
|
||||||
project_id=self.kwargs.get("project_id"),
|
.get_queryset()
|
||||||
).first()
|
.filter(
|
||||||
|
Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True),
|
||||||
project = Project.objects.get(
|
|
||||||
workspace__slug=self.kwargs.get("slug"),
|
|
||||||
pk=self.kwargs.get("project_id"),
|
|
||||||
)
|
|
||||||
|
|
||||||
if inbox is None and not project.inbox_view:
|
|
||||||
return InboxIssue.objects.none()
|
|
||||||
|
|
||||||
return (
|
|
||||||
InboxIssue.objects.filter(
|
|
||||||
Q(snoozed_till__gte=timezone.now())
|
|
||||||
| Q(snoozed_till__isnull=True),
|
|
||||||
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"),
|
||||||
inbox_id=inbox.id,
|
inbox_id=self.kwargs.get("inbox_id"),
|
||||||
)
|
)
|
||||||
.select_related("issue", "workspace", "project")
|
.select_related("issue", "workspace", "project")
|
||||||
.order_by(self.kwargs.get("order_by", "-created_at"))
|
.order_by(self.kwargs.get("order_by", "-created_at"))
|
||||||
)
|
)
|
||||||
|
|
||||||
def get(self, request, slug, project_id, issue_id=None):
|
def get(self, request, slug, project_id, inbox_id, pk=None):
|
||||||
if issue_id:
|
if pk:
|
||||||
inbox_issue_queryset = self.get_queryset().get(issue_id=issue_id)
|
issue_queryset = self.get_queryset().get(pk=pk)
|
||||||
inbox_issue_data = InboxIssueSerializer(
|
issues_data = InboxIssueSerializer(
|
||||||
inbox_issue_queryset,
|
issue_queryset,
|
||||||
fields=self.fields,
|
fields=self.fields,
|
||||||
expand=self.expand,
|
expand=self.expand,
|
||||||
).data
|
).data
|
||||||
return Response(
|
return Response(
|
||||||
inbox_issue_data,
|
issues_data,
|
||||||
status=status.HTTP_200_OK,
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
issue_queryset = self.get_queryset()
|
issue_queryset = self.get_queryset()
|
||||||
@@ -93,29 +74,10 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
|||||||
).data,
|
).data,
|
||||||
)
|
)
|
||||||
|
|
||||||
def post(self, request, slug, project_id):
|
def post(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(
|
||||||
{"error": "Name is required"},
|
{"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
inbox = Inbox.objects.filter(
|
|
||||||
workspace__slug=slug, project_id=project_id
|
|
||||||
).first()
|
|
||||||
|
|
||||||
project = Project.objects.get(
|
|
||||||
workspace__slug=slug,
|
|
||||||
pk=project_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Inbox view
|
|
||||||
if inbox is None and not project.inbox_view:
|
|
||||||
return Response(
|
|
||||||
{
|
|
||||||
"error": "Inbox is not enabled for this project enable it through the project's api"
|
|
||||||
},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check for valid priority
|
# Check for valid priority
|
||||||
@@ -127,8 +89,7 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
|||||||
"none",
|
"none",
|
||||||
]:
|
]:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Invalid priority"},
|
{"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create or get state
|
# Create or get state
|
||||||
@@ -162,45 +123,21 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
|||||||
current_instance=None,
|
current_instance=None,
|
||||||
epoch=int(timezone.now().timestamp()),
|
epoch=int(timezone.now().timestamp()),
|
||||||
)
|
)
|
||||||
|
|
||||||
# create an inbox issue
|
# create an inbox issue
|
||||||
inbox_issue = InboxIssue.objects.create(
|
InboxIssue.objects.create(
|
||||||
inbox_id=inbox.id,
|
inbox_id=inbox_id,
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
issue=issue,
|
issue=issue,
|
||||||
source=request.data.get("source", "in-app"),
|
source=request.data.get("source", "in-app"),
|
||||||
)
|
)
|
||||||
|
|
||||||
serializer = InboxIssueSerializer(inbox_issue)
|
serializer = IssueSerializer(issue)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def patch(self, request, slug, project_id, issue_id):
|
def patch(self, request, slug, project_id, inbox_id, pk):
|
||||||
inbox = Inbox.objects.filter(
|
|
||||||
workspace__slug=slug, project_id=project_id
|
|
||||||
).first()
|
|
||||||
|
|
||||||
project = Project.objects.get(
|
|
||||||
workspace__slug=slug,
|
|
||||||
pk=project_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Inbox view
|
|
||||||
if inbox is None and not project.inbox_view:
|
|
||||||
return Response(
|
|
||||||
{
|
|
||||||
"error": "Inbox is not enabled for this project enable it through the project's api"
|
|
||||||
},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Get the inbox issue
|
|
||||||
inbox_issue = InboxIssue.objects.get(
|
inbox_issue = InboxIssue.objects.get(
|
||||||
issue_id=issue_id,
|
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
|
||||||
workspace__slug=slug,
|
|
||||||
project_id=project_id,
|
|
||||||
inbox_id=inbox.id,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get the project member
|
# Get the project member
|
||||||
project_member = ProjectMember.objects.get(
|
project_member = ProjectMember.objects.get(
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
@@ -208,7 +145,6 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
|||||||
member=request.user,
|
member=request.user,
|
||||||
is_active=True,
|
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(
|
if project_member.role <= 10 and str(inbox_issue.created_by_id) != str(
|
||||||
request.user.id
|
request.user.id
|
||||||
@@ -223,7 +159,7 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
if bool(issue_data):
|
if bool(issue_data):
|
||||||
issue = Issue.objects.get(
|
issue = Issue.objects.get(
|
||||||
pk=issue_id, workspace__slug=slug, project_id=project_id
|
pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id
|
||||||
)
|
)
|
||||||
# Only allow guests and viewers to edit name and description
|
# Only allow guests and viewers to edit name and description
|
||||||
if project_member.role <= 10:
|
if project_member.role <= 10:
|
||||||
@@ -233,14 +169,10 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
|||||||
"description_html": issue_data.get(
|
"description_html": issue_data.get(
|
||||||
"description_html", issue.description_html
|
"description_html", issue.description_html
|
||||||
),
|
),
|
||||||
"description": issue_data.get(
|
"description": issue_data.get("description", issue.description),
|
||||||
"description", issue.description
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
issue_serializer = IssueSerializer(
|
issue_serializer = IssueSerializer(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
|
||||||
@@ -251,7 +183,7 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
|||||||
type="issue.activity.updated",
|
type="issue.activity.updated",
|
||||||
requested_data=requested_data,
|
requested_data=requested_data,
|
||||||
actor_id=str(request.user.id),
|
actor_id=str(request.user.id),
|
||||||
issue_id=str(issue_id),
|
issue_id=str(issue.id),
|
||||||
project_id=str(project_id),
|
project_id=str(project_id),
|
||||||
current_instance=json.dumps(
|
current_instance=json.dumps(
|
||||||
IssueSerializer(current_instance).data,
|
IssueSerializer(current_instance).data,
|
||||||
@@ -276,14 +208,12 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
|||||||
# Update the issue state if the issue is rejected or marked as duplicate
|
# Update the issue state if the issue is rejected or marked as duplicate
|
||||||
if serializer.data["status"] in [-1, 2]:
|
if serializer.data["status"] in [-1, 2]:
|
||||||
issue = Issue.objects.get(
|
issue = Issue.objects.get(
|
||||||
pk=issue_id,
|
pk=inbox_issue.issue_id,
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
)
|
)
|
||||||
state = State.objects.filter(
|
state = State.objects.filter(
|
||||||
group="cancelled",
|
group="cancelled", workspace__slug=slug, project_id=project_id
|
||||||
workspace__slug=slug,
|
|
||||||
project_id=project_id,
|
|
||||||
).first()
|
).first()
|
||||||
if state is not None:
|
if state is not None:
|
||||||
issue.state = state
|
issue.state = state
|
||||||
@@ -292,7 +222,7 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
|||||||
# Update the issue state if it is accepted
|
# Update the issue state if it is accepted
|
||||||
if serializer.data["status"] in [1]:
|
if serializer.data["status"] in [1]:
|
||||||
issue = Issue.objects.get(
|
issue = Issue.objects.get(
|
||||||
pk=issue_id,
|
pk=inbox_issue.issue_id,
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
)
|
)
|
||||||
@@ -301,51 +231,23 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
|||||||
if issue.state.name == "Triage":
|
if issue.state.name == "Triage":
|
||||||
# Move to default state
|
# Move to default state
|
||||||
state = State.objects.filter(
|
state = State.objects.filter(
|
||||||
workspace__slug=slug,
|
workspace__slug=slug, project_id=project_id, default=True
|
||||||
project_id=project_id,
|
|
||||||
default=True,
|
|
||||||
).first()
|
).first()
|
||||||
if state is not None:
|
if state is not None:
|
||||||
issue.state = state
|
issue.state = state
|
||||||
issue.save()
|
issue.save()
|
||||||
|
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
return Response(
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
return Response(
|
return Response(
|
||||||
InboxIssueSerializer(inbox_issue).data,
|
InboxIssueSerializer(inbox_issue).data, status=status.HTTP_200_OK
|
||||||
status=status.HTTP_200_OK,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def delete(self, request, slug, project_id, issue_id):
|
def delete(self, request, slug, project_id, inbox_id, pk):
|
||||||
inbox = Inbox.objects.filter(
|
|
||||||
workspace__slug=slug, project_id=project_id
|
|
||||||
).first()
|
|
||||||
|
|
||||||
project = Project.objects.get(
|
|
||||||
workspace__slug=slug,
|
|
||||||
pk=project_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Inbox view
|
|
||||||
if inbox is None and not project.inbox_view:
|
|
||||||
return Response(
|
|
||||||
{
|
|
||||||
"error": "Inbox is not enabled for this project enable it through the project's api"
|
|
||||||
},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Get the inbox issue
|
|
||||||
inbox_issue = InboxIssue.objects.get(
|
inbox_issue = InboxIssue.objects.get(
|
||||||
issue_id=issue_id,
|
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
|
||||||
workspace__slug=slug,
|
|
||||||
project_id=project_id,
|
|
||||||
inbox_id=inbox.id,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get the project member
|
# Get the project member
|
||||||
project_member = ProjectMember.objects.get(
|
project_member = ProjectMember.objects.get(
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
@@ -354,7 +256,6 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
|||||||
is_active=True,
|
is_active=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check the inbox issue created
|
|
||||||
if project_member.role <= 10 and str(inbox_issue.created_by_id) != str(
|
if project_member.role <= 10 and str(inbox_issue.created_by_id) != str(
|
||||||
request.user.id
|
request.user.id
|
||||||
):
|
):
|
||||||
@@ -367,8 +268,8 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
|||||||
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(
|
Issue.objects.filter(
|
||||||
workspace__slug=slug, project_id=project_id, pk=issue_id
|
workspace__slug=slug, project_id=project_id, pk=inbox_issue.issue_id
|
||||||
).delete()
|
).delete()
|
||||||
|
|
||||||
inbox_issue.delete()
|
inbox_issue.delete()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
@@ -22,6 +22,7 @@ from django.utils import timezone
|
|||||||
# Third party imports
|
# Third party imports
|
||||||
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.parsers import MultiPartParser, FormParser
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from .base import BaseAPIView, WebhookMixin
|
from .base import BaseAPIView, WebhookMixin
|
||||||
@@ -40,12 +41,14 @@ from plane.db.models import (
|
|||||||
IssueComment,
|
IssueComment,
|
||||||
IssueActivity,
|
IssueActivity,
|
||||||
)
|
)
|
||||||
|
from plane.utils.issue_filters import issue_filters
|
||||||
from plane.bgtasks.issue_activites_task import issue_activity
|
from plane.bgtasks.issue_activites_task import issue_activity
|
||||||
from plane.api.serializers import (
|
from plane.api.serializers import (
|
||||||
IssueSerializer,
|
IssueSerializer,
|
||||||
LabelSerializer,
|
LabelSerializer,
|
||||||
IssueLinkSerializer,
|
IssueLinkSerializer,
|
||||||
IssueCommentSerializer,
|
IssueCommentSerializer,
|
||||||
|
IssueAttachmentSerializer,
|
||||||
IssueActivitySerializer,
|
IssueActivitySerializer,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -67,9 +70,7 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return (
|
return (
|
||||||
Issue.issue_objects.annotate(
|
Issue.issue_objects.annotate(
|
||||||
sub_issues_count=Issue.issue_objects.filter(
|
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
|
||||||
parent=OuterRef("id")
|
|
||||||
)
|
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
@@ -88,9 +89,7 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
def get(self, request, slug, project_id, pk=None):
|
def get(self, request, slug, project_id, pk=None):
|
||||||
if pk:
|
if pk:
|
||||||
issue = Issue.issue_objects.annotate(
|
issue = Issue.issue_objects.annotate(
|
||||||
sub_issues_count=Issue.issue_objects.filter(
|
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
|
||||||
parent=OuterRef("id")
|
|
||||||
)
|
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
@@ -104,20 +103,16 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
status=status.HTTP_200_OK,
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
filters = issue_filters(request.query_params, "GET")
|
||||||
# Custom ordering for priority and state
|
# Custom ordering for priority and state
|
||||||
priority_order = ["urgent", "high", "medium", "low", "none"]
|
priority_order = ["urgent", "high", "medium", "low", "none"]
|
||||||
state_order = [
|
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
|
||||||
"backlog",
|
|
||||||
"unstarted",
|
|
||||||
"started",
|
|
||||||
"completed",
|
|
||||||
"cancelled",
|
|
||||||
]
|
|
||||||
|
|
||||||
order_by_param = request.GET.get("order_by", "-created_at")
|
order_by_param = request.GET.get("order_by", "-created_at")
|
||||||
|
|
||||||
issue_queryset = (
|
issue_queryset = (
|
||||||
self.get_queryset()
|
self.get_queryset()
|
||||||
|
.filter(**filters)
|
||||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||||
.annotate(module_id=F("issue_module__module_id"))
|
.annotate(module_id=F("issue_module__module_id"))
|
||||||
.annotate(
|
.annotate(
|
||||||
@@ -127,9 +122,7 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
.values("count")
|
.values("count")
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
attachment_count=IssueAttachment.objects.filter(
|
attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id"))
|
||||||
issue=OuterRef("id")
|
|
||||||
)
|
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
@@ -139,9 +132,7 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
# Priority Ordering
|
# Priority Ordering
|
||||||
if order_by_param == "priority" or order_by_param == "-priority":
|
if order_by_param == "priority" or order_by_param == "-priority":
|
||||||
priority_order = (
|
priority_order = (
|
||||||
priority_order
|
priority_order if order_by_param == "priority" else priority_order[::-1]
|
||||||
if order_by_param == "priority"
|
|
||||||
else priority_order[::-1]
|
|
||||||
)
|
)
|
||||||
issue_queryset = issue_queryset.annotate(
|
issue_queryset = issue_queryset.annotate(
|
||||||
priority_order=Case(
|
priority_order=Case(
|
||||||
@@ -189,9 +180,7 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
else order_by_param
|
else order_by_param
|
||||||
)
|
)
|
||||||
).order_by(
|
).order_by(
|
||||||
"-max_values"
|
"-max_values" if order_by_param.startswith("-") else "max_values"
|
||||||
if order_by_param.startswith("-")
|
|
||||||
else "max_values"
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
issue_queryset = issue_queryset.order_by(order_by_param)
|
issue_queryset = issue_queryset.order_by(order_by_param)
|
||||||
@@ -220,38 +209,12 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
if (
|
|
||||||
request.data.get("external_id")
|
|
||||||
and request.data.get("external_source")
|
|
||||||
and Issue.objects.filter(
|
|
||||||
project_id=project_id,
|
|
||||||
workspace__slug=slug,
|
|
||||||
external_source=request.data.get("external_source"),
|
|
||||||
external_id=request.data.get("external_id"),
|
|
||||||
).exists()
|
|
||||||
):
|
|
||||||
issue = Issue.objects.filter(
|
|
||||||
workspace__slug=slug,
|
|
||||||
project_id=project_id,
|
|
||||||
external_id=request.data.get("external_id"),
|
|
||||||
external_source=request.data.get("external_source"),
|
|
||||||
).first()
|
|
||||||
return Response(
|
|
||||||
{
|
|
||||||
"error": "Issue with the same external id and external source already exists",
|
|
||||||
"id": str(issue.id),
|
|
||||||
},
|
|
||||||
status=status.HTTP_409_CONFLICT,
|
|
||||||
)
|
|
||||||
|
|
||||||
serializer.save()
|
serializer.save()
|
||||||
|
|
||||||
# Track the issue
|
# Track the issue
|
||||||
issue_activity.delay(
|
issue_activity.delay(
|
||||||
type="issue.activity.created",
|
type="issue.activity.created",
|
||||||
requested_data=json.dumps(
|
requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder),
|
||||||
self.request.data, cls=DjangoJSONEncoder
|
|
||||||
),
|
|
||||||
actor_id=str(request.user.id),
|
actor_id=str(request.user.id),
|
||||||
issue_id=str(serializer.data.get("id", None)),
|
issue_id=str(serializer.data.get("id", None)),
|
||||||
project_id=str(project_id),
|
project_id=str(project_id),
|
||||||
@@ -262,44 +225,13 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
def patch(self, request, slug, project_id, pk=None):
|
def patch(self, request, slug, project_id, pk=None):
|
||||||
issue = Issue.objects.get(
|
issue = Issue.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
|
||||||
workspace__slug=slug, project_id=project_id, pk=pk
|
|
||||||
)
|
|
||||||
project = Project.objects.get(pk=project_id)
|
|
||||||
current_instance = json.dumps(
|
current_instance = json.dumps(
|
||||||
IssueSerializer(issue).data, cls=DjangoJSONEncoder
|
IssueSerializer(issue).data, cls=DjangoJSONEncoder
|
||||||
)
|
)
|
||||||
requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder)
|
requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder)
|
||||||
serializer = IssueSerializer(
|
serializer = IssueSerializer(issue, data=request.data, partial=True)
|
||||||
issue,
|
|
||||||
data=request.data,
|
|
||||||
context={
|
|
||||||
"project_id": project_id,
|
|
||||||
"workspace_id": project.workspace_id,
|
|
||||||
},
|
|
||||||
partial=True,
|
|
||||||
)
|
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
if (
|
|
||||||
str(request.data.get("external_id"))
|
|
||||||
and (issue.external_id != str(request.data.get("external_id")))
|
|
||||||
and Issue.objects.filter(
|
|
||||||
project_id=project_id,
|
|
||||||
workspace__slug=slug,
|
|
||||||
external_source=request.data.get(
|
|
||||||
"external_source", issue.external_source
|
|
||||||
),
|
|
||||||
external_id=request.data.get("external_id"),
|
|
||||||
).exists()
|
|
||||||
):
|
|
||||||
return Response(
|
|
||||||
{
|
|
||||||
"error": "Issue with the same external id and external source already exists",
|
|
||||||
"id": str(issue.id),
|
|
||||||
},
|
|
||||||
status=status.HTTP_409_CONFLICT,
|
|
||||||
)
|
|
||||||
|
|
||||||
serializer.save()
|
serializer.save()
|
||||||
issue_activity.delay(
|
issue_activity.delay(
|
||||||
type="issue.activity.updated",
|
type="issue.activity.updated",
|
||||||
@@ -307,8 +239,6 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
actor_id=str(request.user.id),
|
actor_id=str(request.user.id),
|
||||||
issue_id=str(pk),
|
issue_id=str(pk),
|
||||||
project_id=str(project_id),
|
project_id=str(project_id),
|
||||||
external_id__isnull=False,
|
|
||||||
external_source__isnull=False,
|
|
||||||
current_instance=current_instance,
|
current_instance=current_instance,
|
||||||
epoch=int(timezone.now().timestamp()),
|
epoch=int(timezone.now().timestamp()),
|
||||||
)
|
)
|
||||||
@@ -316,9 +246,7 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
def delete(self, request, slug, project_id, pk=None):
|
def delete(self, request, slug, project_id, pk=None):
|
||||||
issue = Issue.objects.get(
|
issue = Issue.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
|
||||||
workspace__slug=slug, project_id=project_id, pk=pk
|
|
||||||
)
|
|
||||||
current_instance = json.dumps(
|
current_instance = json.dumps(
|
||||||
IssueSerializer(issue).data, cls=DjangoJSONEncoder
|
IssueSerializer(issue).data, cls=DjangoJSONEncoder
|
||||||
)
|
)
|
||||||
@@ -350,7 +278,7 @@ class LabelAPIEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return (
|
return (
|
||||||
Label.objects.filter(workspace__slug=self.kwargs.get("slug"))
|
Project.objects.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)
|
||||||
.select_related("project")
|
.select_related("project")
|
||||||
@@ -364,97 +292,39 @@ class LabelAPIEndpoint(BaseAPIView):
|
|||||||
try:
|
try:
|
||||||
serializer = LabelSerializer(data=request.data)
|
serializer = LabelSerializer(data=request.data)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
if (
|
|
||||||
request.data.get("external_id")
|
|
||||||
and request.data.get("external_source")
|
|
||||||
and Label.objects.filter(
|
|
||||||
project_id=project_id,
|
|
||||||
workspace__slug=slug,
|
|
||||||
external_source=request.data.get("external_source"),
|
|
||||||
external_id=request.data.get("external_id"),
|
|
||||||
).exists()
|
|
||||||
):
|
|
||||||
label = Label.objects.filter(
|
|
||||||
workspace__slug=slug,
|
|
||||||
project_id=project_id,
|
|
||||||
external_id=request.data.get("external_id"),
|
|
||||||
external_source=request.data.get("external_source"),
|
|
||||||
).first()
|
|
||||||
return Response(
|
|
||||||
{
|
|
||||||
"error": "Label with the same external id and external source already exists",
|
|
||||||
"id": str(label.id),
|
|
||||||
},
|
|
||||||
status=status.HTTP_409_CONFLICT,
|
|
||||||
)
|
|
||||||
|
|
||||||
serializer.save(project_id=project_id)
|
serializer.save(project_id=project_id)
|
||||||
return Response(
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
serializer.data, status=status.HTTP_201_CREATED
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
)
|
|
||||||
return Response(
|
|
||||||
serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
|
||||||
)
|
|
||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
label = Label.objects.filter(
|
|
||||||
workspace__slug=slug,
|
|
||||||
project_id=project_id,
|
|
||||||
name=request.data.get("name"),
|
|
||||||
).first()
|
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{"error": "Label with the same name already exists in the project"},
|
||||||
"error": "Label with the same name already exists in the project",
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
"id": str(label.id),
|
|
||||||
},
|
|
||||||
status=status.HTTP_409_CONFLICT,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def get(self, request, slug, project_id, pk=None):
|
def get(self, request, slug, project_id, pk=None):
|
||||||
if pk is None:
|
if pk:
|
||||||
return self.paginate(
|
label = self.get_queryset().get(pk=pk)
|
||||||
request=request,
|
serializer = LabelSerializer(
|
||||||
queryset=(self.get_queryset()),
|
label,
|
||||||
on_results=lambda labels: LabelSerializer(
|
fields=self.fields,
|
||||||
labels,
|
expand=self.expand,
|
||||||
many=True,
|
|
||||||
fields=self.fields,
|
|
||||||
expand=self.expand,
|
|
||||||
).data,
|
|
||||||
)
|
)
|
||||||
label = self.get_queryset().get(pk=pk)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
serializer = LabelSerializer(
|
return self.paginate(
|
||||||
label,
|
request=request,
|
||||||
fields=self.fields,
|
queryset=(self.get_queryset()),
|
||||||
expand=self.expand,
|
on_results=lambda labels: LabelSerializer(
|
||||||
|
labels,
|
||||||
|
many=True,
|
||||||
|
fields=self.fields,
|
||||||
|
expand=self.expand,
|
||||||
|
).data,
|
||||||
)
|
)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
|
||||||
|
|
||||||
def patch(self, request, slug, project_id, pk=None):
|
def patch(self, request, slug, project_id, pk=None):
|
||||||
label = self.get_queryset().get(pk=pk)
|
label = self.get_queryset().get(pk=pk)
|
||||||
serializer = LabelSerializer(label, data=request.data, partial=True)
|
serializer = LabelSerializer(label, data=request.data, partial=True)
|
||||||
if serializer.is_valid():
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
if (
|
|
||||||
str(request.data.get("external_id"))
|
|
||||||
and (label.external_id != str(request.data.get("external_id")))
|
|
||||||
and Issue.objects.filter(
|
|
||||||
project_id=project_id,
|
|
||||||
workspace__slug=slug,
|
|
||||||
external_source=request.data.get(
|
|
||||||
"external_source", label.external_source
|
|
||||||
),
|
|
||||||
external_id=request.data.get("external_id"),
|
|
||||||
).exists()
|
|
||||||
):
|
|
||||||
return Response(
|
|
||||||
{
|
|
||||||
"error": "Label with the same external id and external source already exists",
|
|
||||||
"id": str(label.id),
|
|
||||||
},
|
|
||||||
status=status.HTTP_409_CONFLICT,
|
|
||||||
)
|
|
||||||
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, pk=None):
|
def delete(self, request, slug, project_id, pk=None):
|
||||||
label = self.get_queryset().get(pk=pk)
|
label = self.get_queryset().get(pk=pk)
|
||||||
@@ -486,31 +356,25 @@ class IssueLinkAPIEndpoint(BaseAPIView):
|
|||||||
.distinct()
|
.distinct()
|
||||||
)
|
)
|
||||||
|
|
||||||
def get(self, request, slug, project_id, issue_id, pk=None):
|
def get(self, request, slug, project_id, pk=None):
|
||||||
if pk is None:
|
if pk:
|
||||||
issue_links = self.get_queryset()
|
label = self.get_queryset().get(pk=pk)
|
||||||
serializer = IssueLinkSerializer(
|
serializer = IssueLinkSerializer(
|
||||||
issue_links,
|
label,
|
||||||
fields=self.fields,
|
fields=self.fields,
|
||||||
expand=self.expand,
|
expand=self.expand,
|
||||||
)
|
)
|
||||||
return self.paginate(
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
request=request,
|
return self.paginate(
|
||||||
queryset=(self.get_queryset()),
|
request=request,
|
||||||
on_results=lambda issue_links: IssueLinkSerializer(
|
queryset=(self.get_queryset()),
|
||||||
issue_links,
|
on_results=lambda issue_links: IssueLinkSerializer(
|
||||||
many=True,
|
issue_links,
|
||||||
fields=self.fields,
|
many=True,
|
||||||
expand=self.expand,
|
fields=self.fields,
|
||||||
).data,
|
expand=self.expand,
|
||||||
)
|
).data,
|
||||||
issue_link = self.get_queryset().get(pk=pk)
|
|
||||||
serializer = IssueLinkSerializer(
|
|
||||||
issue_link,
|
|
||||||
fields=self.fields,
|
|
||||||
expand=self.expand,
|
|
||||||
)
|
)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
|
||||||
|
|
||||||
def post(self, request, slug, project_id, issue_id):
|
def post(self, request, slug, project_id, issue_id):
|
||||||
serializer = IssueLinkSerializer(data=request.data)
|
serializer = IssueLinkSerializer(data=request.data)
|
||||||
@@ -521,9 +385,7 @@ class IssueLinkAPIEndpoint(BaseAPIView):
|
|||||||
)
|
)
|
||||||
issue_activity.delay(
|
issue_activity.delay(
|
||||||
type="link.activity.created",
|
type="link.activity.created",
|
||||||
requested_data=json.dumps(
|
requested_data=json.dumps(serializer.data, cls=DjangoJSONEncoder),
|
||||||
serializer.data, cls=DjangoJSONEncoder
|
|
||||||
),
|
|
||||||
actor_id=str(self.request.user.id),
|
actor_id=str(self.request.user.id),
|
||||||
issue_id=str(self.kwargs.get("issue_id")),
|
issue_id=str(self.kwargs.get("issue_id")),
|
||||||
project_id=str(self.kwargs.get("project_id")),
|
project_id=str(self.kwargs.get("project_id")),
|
||||||
@@ -535,19 +397,14 @@ class IssueLinkAPIEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
def patch(self, request, slug, project_id, issue_id, pk):
|
def patch(self, request, slug, project_id, issue_id, pk):
|
||||||
issue_link = IssueLink.objects.get(
|
issue_link = IssueLink.objects.get(
|
||||||
workspace__slug=slug,
|
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
|
||||||
project_id=project_id,
|
|
||||||
issue_id=issue_id,
|
|
||||||
pk=pk,
|
|
||||||
)
|
)
|
||||||
requested_data = json.dumps(request.data, cls=DjangoJSONEncoder)
|
requested_data = json.dumps(request.data, cls=DjangoJSONEncoder)
|
||||||
current_instance = json.dumps(
|
current_instance = json.dumps(
|
||||||
IssueLinkSerializer(issue_link).data,
|
IssueLinkSerializer(issue_link).data,
|
||||||
cls=DjangoJSONEncoder,
|
cls=DjangoJSONEncoder,
|
||||||
)
|
)
|
||||||
serializer = IssueLinkSerializer(
|
serializer = IssueLinkSerializer(issue_link, data=request.data, partial=True)
|
||||||
issue_link, data=request.data, partial=True
|
|
||||||
)
|
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
serializer.save()
|
serializer.save()
|
||||||
issue_activity.delay(
|
issue_activity.delay(
|
||||||
@@ -564,10 +421,7 @@ class IssueLinkAPIEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
def delete(self, request, slug, project_id, issue_id, pk):
|
def delete(self, request, slug, project_id, issue_id, pk):
|
||||||
issue_link = IssueLink.objects.get(
|
issue_link = IssueLink.objects.get(
|
||||||
workspace__slug=slug,
|
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
|
||||||
project_id=project_id,
|
|
||||||
issue_id=issue_id,
|
|
||||||
pk=pk,
|
|
||||||
)
|
)
|
||||||
current_instance = json.dumps(
|
current_instance = json.dumps(
|
||||||
IssueLinkSerializer(issue_link).data,
|
IssueLinkSerializer(issue_link).data,
|
||||||
@@ -595,16 +449,14 @@ class IssueCommentAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
|
|
||||||
serializer_class = IssueCommentSerializer
|
serializer_class = IssueCommentSerializer
|
||||||
model = IssueComment
|
model = IssueComment
|
||||||
webhook_event = "issue_comment"
|
webhook_event = "issue-comment"
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
ProjectLitePermission,
|
ProjectLitePermission,
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return (
|
return (
|
||||||
IssueComment.objects.filter(
|
IssueComment.objects.filter(workspace__slug=self.kwargs.get("slug"))
|
||||||
workspace__slug=self.kwargs.get("slug")
|
|
||||||
)
|
|
||||||
.filter(project_id=self.kwargs.get("project_id"))
|
.filter(project_id=self.kwargs.get("project_id"))
|
||||||
.filter(issue_id=self.kwargs.get("issue_id"))
|
.filter(issue_id=self.kwargs.get("issue_id"))
|
||||||
.filter(project__project_projectmember__member=self.request.user)
|
.filter(project__project_projectmember__member=self.request.user)
|
||||||
@@ -656,9 +508,7 @@ class IssueCommentAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
)
|
)
|
||||||
issue_activity.delay(
|
issue_activity.delay(
|
||||||
type="comment.activity.created",
|
type="comment.activity.created",
|
||||||
requested_data=json.dumps(
|
requested_data=json.dumps(serializer.data, cls=DjangoJSONEncoder),
|
||||||
serializer.data, cls=DjangoJSONEncoder
|
|
||||||
),
|
|
||||||
actor_id=str(self.request.user.id),
|
actor_id=str(self.request.user.id),
|
||||||
issue_id=str(self.kwargs.get("issue_id")),
|
issue_id=str(self.kwargs.get("issue_id")),
|
||||||
project_id=str(self.kwargs.get("project_id")),
|
project_id=str(self.kwargs.get("project_id")),
|
||||||
@@ -670,10 +520,7 @@ class IssueCommentAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
|
|
||||||
def patch(self, request, slug, project_id, issue_id, pk):
|
def patch(self, request, slug, project_id, issue_id, pk):
|
||||||
issue_comment = IssueComment.objects.get(
|
issue_comment = IssueComment.objects.get(
|
||||||
workspace__slug=slug,
|
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
|
||||||
project_id=project_id,
|
|
||||||
issue_id=issue_id,
|
|
||||||
pk=pk,
|
|
||||||
)
|
)
|
||||||
requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder)
|
requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder)
|
||||||
current_instance = json.dumps(
|
current_instance = json.dumps(
|
||||||
@@ -699,10 +546,7 @@ class IssueCommentAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
|
|
||||||
def delete(self, request, slug, project_id, issue_id, pk):
|
def delete(self, request, slug, project_id, issue_id, pk):
|
||||||
issue_comment = IssueComment.objects.get(
|
issue_comment = IssueComment.objects.get(
|
||||||
workspace__slug=slug,
|
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
|
||||||
project_id=project_id,
|
|
||||||
issue_id=issue_id,
|
|
||||||
pk=pk,
|
|
||||||
)
|
)
|
||||||
current_instance = json.dumps(
|
current_instance = json.dumps(
|
||||||
IssueCommentSerializer(issue_comment).data,
|
IssueCommentSerializer(issue_comment).data,
|
||||||
@@ -737,13 +581,13 @@ class IssueActivityAPIEndpoint(BaseAPIView):
|
|||||||
)
|
)
|
||||||
.select_related("actor", "workspace", "issue", "project")
|
.select_related("actor", "workspace", "issue", "project")
|
||||||
).order_by(request.GET.get("order_by", "created_at"))
|
).order_by(request.GET.get("order_by", "created_at"))
|
||||||
|
|
||||||
if pk:
|
if pk:
|
||||||
issue_activities = issue_activities.get(pk=pk)
|
issue_activities = issue_activities.get(pk=pk)
|
||||||
serializer = IssueActivitySerializer(issue_activities)
|
serializer = IssueActivitySerializer(issue_activities)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
return self.paginate(
|
self.paginate(
|
||||||
request=request,
|
request=request,
|
||||||
queryset=(issue_activities),
|
queryset=(issue_activities),
|
||||||
on_results=lambda issue_activity: IssueActivitySerializer(
|
on_results=lambda issue_activity: IssueActivitySerializer(
|
||||||
|
|||||||
@@ -55,9 +55,7 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
.prefetch_related(
|
.prefetch_related(
|
||||||
Prefetch(
|
Prefetch(
|
||||||
"link_module",
|
"link_module",
|
||||||
queryset=ModuleLink.objects.select_related(
|
queryset=ModuleLink.objects.select_related("module", "created_by"),
|
||||||
"module", "created_by"
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
@@ -123,76 +121,15 @@ class ModuleAPIEndpoint(WebhookMixin, 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(workspace__slug=slug, pk=project_id)
|
||||||
serializer = ModuleSerializer(
|
serializer = ModuleSerializer(data=request.data, context={"project": project})
|
||||||
data=request.data,
|
|
||||||
context={
|
|
||||||
"project_id": project_id,
|
|
||||||
"workspace_id": project.workspace_id,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
if (
|
|
||||||
request.data.get("external_id")
|
|
||||||
and request.data.get("external_source")
|
|
||||||
and Module.objects.filter(
|
|
||||||
project_id=project_id,
|
|
||||||
workspace__slug=slug,
|
|
||||||
external_source=request.data.get("external_source"),
|
|
||||||
external_id=request.data.get("external_id"),
|
|
||||||
).exists()
|
|
||||||
):
|
|
||||||
module = Module.objects.filter(
|
|
||||||
project_id=project_id,
|
|
||||||
workspace__slug=slug,
|
|
||||||
external_source=request.data.get("external_source"),
|
|
||||||
external_id=request.data.get("external_id"),
|
|
||||||
).first()
|
|
||||||
return Response(
|
|
||||||
{
|
|
||||||
"error": "Module with the same external id and external source already exists",
|
|
||||||
"id": str(module.id),
|
|
||||||
},
|
|
||||||
status=status.HTTP_409_CONFLICT,
|
|
||||||
)
|
|
||||||
serializer.save()
|
serializer.save()
|
||||||
module = Module.objects.get(pk=serializer.data["id"])
|
module = Module.objects.get(pk=serializer.data["id"])
|
||||||
serializer = ModuleSerializer(module)
|
serializer = ModuleSerializer(module)
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
def patch(self, request, slug, project_id, pk):
|
|
||||||
module = Module.objects.get(
|
|
||||||
pk=pk, project_id=project_id, workspace__slug=slug
|
|
||||||
)
|
|
||||||
serializer = ModuleSerializer(
|
|
||||||
module,
|
|
||||||
data=request.data,
|
|
||||||
context={"project_id": project_id},
|
|
||||||
partial=True,
|
|
||||||
)
|
|
||||||
if serializer.is_valid():
|
|
||||||
if (
|
|
||||||
request.data.get("external_id")
|
|
||||||
and (module.external_id != request.data.get("external_id"))
|
|
||||||
and Module.objects.filter(
|
|
||||||
project_id=project_id,
|
|
||||||
workspace__slug=slug,
|
|
||||||
external_source=request.data.get("external_source", module.external_source),
|
|
||||||
external_id=request.data.get("external_id"),
|
|
||||||
).exists()
|
|
||||||
):
|
|
||||||
return Response(
|
|
||||||
{
|
|
||||||
"error": "Module with the same external id and external source already exists",
|
|
||||||
"id": str(module.id),
|
|
||||||
},
|
|
||||||
status=status.HTTP_409_CONFLICT,
|
|
||||||
)
|
|
||||||
serializer.save()
|
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
def get(self, request, slug, project_id, pk=None):
|
def get(self, request, slug, project_id, pk=None):
|
||||||
if pk:
|
if pk:
|
||||||
queryset = self.get_queryset().get(pk=pk)
|
queryset = self.get_queryset().get(pk=pk)
|
||||||
@@ -217,13 +154,9 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def delete(self, request, slug, project_id, pk):
|
def delete(self, request, slug, project_id, pk):
|
||||||
module = Module.objects.get(
|
module = Module.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
|
||||||
workspace__slug=slug, project_id=project_id, pk=pk
|
|
||||||
)
|
|
||||||
module_issues = list(
|
module_issues = list(
|
||||||
ModuleIssue.objects.filter(module_id=pk).values_list(
|
ModuleIssue.objects.filter(module_id=pk).values_list("issue", flat=True)
|
||||||
"issue", flat=True
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
issue_activity.delay(
|
issue_activity.delay(
|
||||||
type="module.activity.deleted",
|
type="module.activity.deleted",
|
||||||
@@ -235,7 +168,7 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
}
|
}
|
||||||
),
|
),
|
||||||
actor_id=str(request.user.id),
|
actor_id=str(request.user.id),
|
||||||
issue_id=None,
|
issue_id=str(pk),
|
||||||
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()),
|
||||||
@@ -253,8 +186,7 @@ class ModuleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
|
|
||||||
serializer_class = ModuleIssueSerializer
|
serializer_class = ModuleIssueSerializer
|
||||||
model = ModuleIssue
|
model = ModuleIssue
|
||||||
webhook_event = "module_issue"
|
webhook_event = "module"
|
||||||
bulk = True
|
|
||||||
|
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
ProjectEntityPermission,
|
ProjectEntityPermission,
|
||||||
@@ -263,9 +195,7 @@ class ModuleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return (
|
return (
|
||||||
ModuleIssue.objects.annotate(
|
ModuleIssue.objects.annotate(
|
||||||
sub_issues_count=Issue.issue_objects.filter(
|
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("issue"))
|
||||||
parent=OuterRef("issue")
|
|
||||||
)
|
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
@@ -289,9 +219,7 @@ class ModuleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
issues = (
|
issues = (
|
||||||
Issue.issue_objects.filter(issue_module__module_id=module_id)
|
Issue.issue_objects.filter(issue_module__module_id=module_id)
|
||||||
.annotate(
|
.annotate(
|
||||||
sub_issues_count=Issue.issue_objects.filter(
|
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
|
||||||
parent=OuterRef("id")
|
|
||||||
)
|
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
@@ -313,9 +241,7 @@ class ModuleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
.values("count")
|
.values("count")
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
attachment_count=IssueAttachment.objects.filter(
|
attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id"))
|
||||||
issue=OuterRef("id")
|
|
||||||
)
|
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
@@ -336,8 +262,7 @@ class ModuleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
issues = request.data.get("issues", [])
|
issues = request.data.get("issues", [])
|
||||||
if not len(issues):
|
if not len(issues):
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Issues are required"},
|
{"error": "Issues are required"}, status=status.HTTP_400_BAD_REQUEST
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
)
|
||||||
module = Module.objects.get(
|
module = Module.objects.get(
|
||||||
workspace__slug=slug, project_id=project_id, pk=module_id
|
workspace__slug=slug, project_id=project_id, pk=module_id
|
||||||
@@ -398,7 +323,7 @@ class ModuleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
# Capture Issue Activity
|
# Capture Issue Activity
|
||||||
issue_activity.delay(
|
issue_activity.delay(
|
||||||
type="module.activity.created",
|
type="module.activity.created",
|
||||||
requested_data=json.dumps({"modules_list": str(issues)}),
|
requested_data=json.dumps({"modules_list": issues}),
|
||||||
actor_id=str(self.request.user.id),
|
actor_id=str(self.request.user.id),
|
||||||
issue_id=None,
|
issue_id=None,
|
||||||
project_id=str(self.kwargs.get("project_id", None)),
|
project_id=str(self.kwargs.get("project_id", None)),
|
||||||
@@ -418,12 +343,9 @@ class ModuleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
status=status.HTTP_200_OK,
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
|
|
||||||
def delete(self, request, slug, project_id, module_id, issue_id):
|
def delete(self, request, slug, project_id, module_id, pk):
|
||||||
module_issue = ModuleIssue.objects.get(
|
module_issue = ModuleIssue.objects.get(
|
||||||
workspace__slug=slug,
|
workspace__slug=slug, project_id=project_id, module_id=module_id, pk=pk
|
||||||
project_id=project_id,
|
|
||||||
module_id=module_id,
|
|
||||||
issue_id=issue_id,
|
|
||||||
)
|
)
|
||||||
module_issue.delete()
|
module_issue.delete()
|
||||||
issue_activity.delay(
|
issue_activity.delay(
|
||||||
@@ -435,9 +357,9 @@ class ModuleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
}
|
}
|
||||||
),
|
),
|
||||||
actor_id=str(request.user.id),
|
actor_id=str(request.user.id),
|
||||||
issue_id=str(issue_id),
|
issue_id=str(pk),
|
||||||
project_id=str(project_id),
|
project_id=str(project_id),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
epoch=int(timezone.now().timestamp()),
|
epoch=int(timezone.now().timestamp()),
|
||||||
)
|
)
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
@@ -39,15 +39,9 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return (
|
return (
|
||||||
Project.objects.filter(workspace__slug=self.kwargs.get("slug"))
|
Project.objects.filter(workspace__slug=self.kwargs.get("slug"))
|
||||||
.filter(
|
.filter(Q(project_projectmember__member=self.request.user) | Q(network=2))
|
||||||
Q(project_projectmember__member=self.request.user)
|
|
||||||
| Q(network=2)
|
|
||||||
)
|
|
||||||
.select_related(
|
.select_related(
|
||||||
"workspace",
|
"workspace", "workspace__owner", "default_assignee", "project_lead"
|
||||||
"workspace__owner",
|
|
||||||
"default_assignee",
|
|
||||||
"project_lead",
|
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
is_member=Exists(
|
is_member=Exists(
|
||||||
@@ -100,8 +94,8 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
.distinct()
|
.distinct()
|
||||||
)
|
)
|
||||||
|
|
||||||
def get(self, request, slug, project_id=None):
|
def get(self, request, slug, pk=None):
|
||||||
if project_id is None:
|
if pk is None:
|
||||||
sort_order_query = ProjectMember.objects.filter(
|
sort_order_query = ProjectMember.objects.filter(
|
||||||
member=request.user,
|
member=request.user,
|
||||||
project_id=OuterRef("pk"),
|
project_id=OuterRef("pk"),
|
||||||
@@ -120,29 +114,24 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
).select_related("member"),
|
).select_related("member"),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.order_by(request.GET.get("order_by", "sort_order"))
|
.order_by("sort_order", "name")
|
||||||
)
|
)
|
||||||
return self.paginate(
|
return self.paginate(
|
||||||
request=request,
|
request=request,
|
||||||
queryset=(projects),
|
queryset=(projects),
|
||||||
on_results=lambda projects: ProjectSerializer(
|
on_results=lambda projects: ProjectSerializer(
|
||||||
projects,
|
projects, many=True, fields=self.fields, expand=self.expand,
|
||||||
many=True,
|
|
||||||
fields=self.fields,
|
|
||||||
expand=self.expand,
|
|
||||||
).data,
|
).data,
|
||||||
)
|
)
|
||||||
project = self.get_queryset().get(workspace__slug=slug, pk=project_id)
|
else:
|
||||||
serializer = ProjectSerializer(
|
project = self.get_queryset().get(workspace__slug=slug, pk=pk)
|
||||||
project,
|
serializer = ProjectSerializer(project, fields=self.fields, expand=self.expand,)
|
||||||
fields=self.fields,
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
expand=self.expand,
|
|
||||||
)
|
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
|
||||||
|
|
||||||
def post(self, request, slug):
|
def post(self, request, slug):
|
||||||
try:
|
try:
|
||||||
workspace = Workspace.objects.get(slug=slug)
|
workspace = Workspace.objects.get(slug=slug)
|
||||||
|
|
||||||
serializer = ProjectSerializer(
|
serializer = ProjectSerializer(
|
||||||
data={**request.data}, context={"workspace_id": workspace.id}
|
data={**request.data}, context={"workspace_id": workspace.id}
|
||||||
)
|
)
|
||||||
@@ -151,9 +140,7 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
|
|
||||||
# Add the user as Administrator to the project
|
# Add the user as Administrator to the project
|
||||||
project_member = ProjectMember.objects.create(
|
project_member = ProjectMember.objects.create(
|
||||||
project_id=serializer.data["id"],
|
project_id=serializer.data["id"], member=request.user, role=20
|
||||||
member=request.user,
|
|
||||||
role=20,
|
|
||||||
)
|
)
|
||||||
# Also create the issue property for the user
|
# Also create the issue property for the user
|
||||||
_ = IssueProperty.objects.create(
|
_ = IssueProperty.objects.create(
|
||||||
@@ -226,15 +213,9 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
project = (
|
project = self.get_queryset().filter(pk=serializer.data["id"]).first()
|
||||||
self.get_queryset()
|
|
||||||
.filter(pk=serializer.data["id"])
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
serializer = ProjectSerializer(project)
|
serializer = ProjectSerializer(project)
|
||||||
return Response(
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
serializer.data, status=status.HTTP_201_CREATED
|
|
||||||
)
|
|
||||||
return Response(
|
return Response(
|
||||||
serializer.errors,
|
serializer.errors,
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
@@ -247,8 +228,7 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
)
|
)
|
||||||
except Workspace.DoesNotExist as e:
|
except Workspace.DoesNotExist as e:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Workspace does not exist"},
|
{"error": "Workspace does not exist"}, status=status.HTTP_404_NOT_FOUND
|
||||||
status=status.HTTP_404_NOT_FOUND,
|
|
||||||
)
|
)
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
return Response(
|
return Response(
|
||||||
@@ -256,10 +236,10 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
status=status.HTTP_410_GONE,
|
status=status.HTTP_410_GONE,
|
||||||
)
|
)
|
||||||
|
|
||||||
def patch(self, request, slug, project_id=None):
|
def patch(self, request, slug, pk=None):
|
||||||
try:
|
try:
|
||||||
workspace = Workspace.objects.get(slug=slug)
|
workspace = Workspace.objects.get(slug=slug)
|
||||||
project = Project.objects.get(pk=project_id)
|
project = Project.objects.get(pk=pk)
|
||||||
|
|
||||||
serializer = ProjectSerializer(
|
serializer = ProjectSerializer(
|
||||||
project,
|
project,
|
||||||
@@ -272,9 +252,7 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
serializer.save()
|
serializer.save()
|
||||||
if serializer.data["inbox_view"]:
|
if serializer.data["inbox_view"]:
|
||||||
Inbox.objects.get_or_create(
|
Inbox.objects.get_or_create(
|
||||||
name=f"{project.name} Inbox",
|
name=f"{project.name} Inbox", project=project, is_default=True
|
||||||
project=project,
|
|
||||||
is_default=True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create the triage state in Backlog group
|
# Create the triage state in Backlog group
|
||||||
@@ -282,20 +260,14 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
name="Triage",
|
name="Triage",
|
||||||
group="backlog",
|
group="backlog",
|
||||||
description="Default state for managing all Inbox Issues",
|
description="Default state for managing all Inbox Issues",
|
||||||
project_id=project_id,
|
project_id=pk,
|
||||||
color="#ff7700",
|
color="#ff7700",
|
||||||
)
|
)
|
||||||
|
|
||||||
project = (
|
project = self.get_queryset().filter(pk=serializer.data["id"]).first()
|
||||||
self.get_queryset()
|
|
||||||
.filter(pk=serializer.data["id"])
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
serializer = ProjectSerializer(project)
|
serializer = ProjectSerializer(project)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
return Response(
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
|
||||||
)
|
|
||||||
except IntegrityError as e:
|
except IntegrityError as e:
|
||||||
if "already exists" in str(e):
|
if "already exists" in str(e):
|
||||||
return Response(
|
return Response(
|
||||||
@@ -304,16 +276,10 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
)
|
)
|
||||||
except (Project.DoesNotExist, Workspace.DoesNotExist):
|
except (Project.DoesNotExist, Workspace.DoesNotExist):
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Project does not exist"},
|
{"error": "Project does not exist"}, status=status.HTTP_404_NOT_FOUND
|
||||||
status=status.HTTP_404_NOT_FOUND,
|
|
||||||
)
|
)
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
return Response(
|
return Response(
|
||||||
{"identifier": "The project identifier is already taken"},
|
{"identifier": "The project identifier is already taken"},
|
||||||
status=status.HTTP_410_GONE,
|
status=status.HTTP_410_GONE,
|
||||||
)
|
)
|
||||||
|
|
||||||
def delete(self, request, slug, project_id):
|
|
||||||
project = Project.objects.get(pk=project_id, workspace__slug=slug)
|
|
||||||
project.delete()
|
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
|
||||||
@@ -23,8 +23,10 @@ class StateAPIEndpoint(BaseAPIView):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return (
|
return self.filter_queryset(
|
||||||
State.objects.filter(workspace__slug=self.kwargs.get("slug"))
|
super()
|
||||||
|
.get_queryset()
|
||||||
|
.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(~Q(name="Triage"))
|
.filter(~Q(name="Triage"))
|
||||||
@@ -34,41 +36,15 @@ class StateAPIEndpoint(BaseAPIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def post(self, request, slug, project_id):
|
def post(self, request, slug, project_id):
|
||||||
serializer = StateSerializer(
|
serializer = StateSerializer(data=request.data, context={"project_id": project_id})
|
||||||
data=request.data, context={"project_id": project_id}
|
|
||||||
)
|
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
if (
|
|
||||||
request.data.get("external_id")
|
|
||||||
and request.data.get("external_source")
|
|
||||||
and State.objects.filter(
|
|
||||||
project_id=project_id,
|
|
||||||
workspace__slug=slug,
|
|
||||||
external_source=request.data.get("external_source"),
|
|
||||||
external_id=request.data.get("external_id"),
|
|
||||||
).exists()
|
|
||||||
):
|
|
||||||
state = State.objects.filter(
|
|
||||||
workspace__slug=slug,
|
|
||||||
project_id=project_id,
|
|
||||||
external_id=request.data.get("external_id"),
|
|
||||||
external_source=request.data.get("external_source"),
|
|
||||||
).first()
|
|
||||||
return Response(
|
|
||||||
{
|
|
||||||
"error": "State with the same external id and external source already exists",
|
|
||||||
"id": str(state.id),
|
|
||||||
},
|
|
||||||
status=status.HTTP_409_CONFLICT,
|
|
||||||
)
|
|
||||||
|
|
||||||
serializer.save(project_id=project_id)
|
serializer.save(project_id=project_id)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
def get(self, request, slug, project_id, state_id=None):
|
def get(self, request, slug, project_id, pk=None):
|
||||||
if state_id:
|
if pk:
|
||||||
serializer = StateSerializer(self.get_queryset().get(pk=state_id))
|
serializer = StateSerializer(self.get_queryset().get(pk=pk))
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
return self.paginate(
|
return self.paginate(
|
||||||
request=request,
|
request=request,
|
||||||
@@ -81,57 +57,33 @@ class StateAPIEndpoint(BaseAPIView):
|
|||||||
).data,
|
).data,
|
||||||
)
|
)
|
||||||
|
|
||||||
def delete(self, request, slug, project_id, state_id):
|
def delete(self, request, slug, project_id, pk):
|
||||||
state = State.objects.get(
|
state = State.objects.get(
|
||||||
~Q(name="Triage"),
|
~Q(name="Triage"),
|
||||||
pk=state_id,
|
pk=pk,
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
)
|
)
|
||||||
|
|
||||||
if state.default:
|
if state.default:
|
||||||
return Response(
|
return Response({"error": "Default state cannot be deleted"}, status=False)
|
||||||
{"error": "Default state cannot be deleted"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check for any issues in the state
|
# Check for any issues in the state
|
||||||
issue_exist = Issue.issue_objects.filter(state=state_id).exists()
|
issue_exist = Issue.issue_objects.filter(state=pk).exists()
|
||||||
|
|
||||||
if issue_exist:
|
if issue_exist:
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{"error": "The state is not empty, only empty states can be deleted"},
|
||||||
"error": "The state is not empty, only empty states can be deleted"
|
|
||||||
},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
state.delete()
|
state.delete()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
def patch(self, request, slug, project_id, state_id=None):
|
def patch(self, request, slug, project_id, pk=None):
|
||||||
state = State.objects.get(
|
state = State.objects.filter(workspace__slug=slug, project_id=project_id, pk=pk)
|
||||||
workspace__slug=slug, project_id=project_id, pk=state_id
|
|
||||||
)
|
|
||||||
serializer = StateSerializer(state, data=request.data, partial=True)
|
serializer = StateSerializer(state, data=request.data, partial=True)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
if (
|
|
||||||
str(request.data.get("external_id"))
|
|
||||||
and (state.external_id != str(request.data.get("external_id")))
|
|
||||||
and State.objects.filter(
|
|
||||||
project_id=project_id,
|
|
||||||
workspace__slug=slug,
|
|
||||||
external_source=request.data.get("external_source", state.external_source),
|
|
||||||
external_id=request.data.get("external_id"),
|
|
||||||
).exists()
|
|
||||||
):
|
|
||||||
return Response(
|
|
||||||
{
|
|
||||||
"error": "State with the same external id and external source already exists",
|
|
||||||
"id": str(state.id),
|
|
||||||
},
|
|
||||||
status=status.HTTP_409_CONFLICT,
|
|
||||||
)
|
|
||||||
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)
|
||||||
@@ -25,10 +25,7 @@ class APIKeyAuthentication(authentication.BaseAuthentication):
|
|||||||
def validate_api_token(self, token):
|
def validate_api_token(self, token):
|
||||||
try:
|
try:
|
||||||
api_token = APIToken.objects.get(
|
api_token = APIToken.objects.get(
|
||||||
Q(
|
Q(Q(expired_at__gt=timezone.now()) | Q(expired_at__isnull=True)),
|
||||||
Q(expired_at__gt=timezone.now())
|
|
||||||
| Q(expired_at__isnull=True)
|
|
||||||
),
|
|
||||||
token=token,
|
token=token,
|
||||||
is_active=True,
|
is_active=True,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
from .workspace import (
|
from .workspace import (
|
||||||
WorkSpaceBasePermission,
|
WorkSpaceBasePermission,
|
||||||
WorkspaceOwnerPermission,
|
WorkspaceOwnerPermission,
|
||||||
@@ -12,3 +13,5 @@ from .project import (
|
|||||||
ProjectMemberPermission,
|
ProjectMemberPermission,
|
||||||
ProjectLitePermission,
|
ProjectLitePermission,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ class WorkspaceViewerPermission(BasePermission):
|
|||||||
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__gte=10,
|
||||||
is_active=True,
|
is_active=True,
|
||||||
).exists()
|
).exists()
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -17,7 +17,6 @@ from .workspace import (
|
|||||||
WorkspaceThemeSerializer,
|
WorkspaceThemeSerializer,
|
||||||
WorkspaceMemberAdminSerializer,
|
WorkspaceMemberAdminSerializer,
|
||||||
WorkspaceMemberMeSerializer,
|
WorkspaceMemberMeSerializer,
|
||||||
WorkspaceUserPropertiesSerializer,
|
|
||||||
)
|
)
|
||||||
from .project import (
|
from .project import (
|
||||||
ProjectSerializer,
|
ProjectSerializer,
|
||||||
@@ -32,20 +31,14 @@ from .project import (
|
|||||||
ProjectDeployBoardSerializer,
|
ProjectDeployBoardSerializer,
|
||||||
ProjectMemberAdminSerializer,
|
ProjectMemberAdminSerializer,
|
||||||
ProjectPublicMemberSerializer,
|
ProjectPublicMemberSerializer,
|
||||||
ProjectMemberRoleSerializer,
|
|
||||||
)
|
)
|
||||||
from .state import StateSerializer, StateLiteSerializer
|
from .state import StateSerializer, StateLiteSerializer
|
||||||
from .view import (
|
from .view import GlobalViewSerializer, IssueViewSerializer, IssueViewFavoriteSerializer
|
||||||
GlobalViewSerializer,
|
|
||||||
IssueViewSerializer,
|
|
||||||
IssueViewFavoriteSerializer,
|
|
||||||
)
|
|
||||||
from .cycle import (
|
from .cycle import (
|
||||||
CycleSerializer,
|
CycleSerializer,
|
||||||
CycleIssueSerializer,
|
CycleIssueSerializer,
|
||||||
CycleFavoriteSerializer,
|
CycleFavoriteSerializer,
|
||||||
CycleWriteSerializer,
|
CycleWriteSerializer,
|
||||||
CycleUserPropertiesSerializer,
|
|
||||||
)
|
)
|
||||||
from .asset import FileAssetSerializer
|
from .asset import FileAssetSerializer
|
||||||
from .issue import (
|
from .issue import (
|
||||||
@@ -76,7 +69,6 @@ from .module import (
|
|||||||
ModuleIssueSerializer,
|
ModuleIssueSerializer,
|
||||||
ModuleLinkSerializer,
|
ModuleLinkSerializer,
|
||||||
ModuleFavoriteSerializer,
|
ModuleFavoriteSerializer,
|
||||||
ModuleUserPropertiesSerializer,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
from .api import APITokenSerializer, APITokenReadSerializer
|
from .api import APITokenSerializer, APITokenReadSerializer
|
||||||
@@ -93,33 +85,20 @@ from .integration import (
|
|||||||
|
|
||||||
from .importer import ImporterSerializer
|
from .importer import ImporterSerializer
|
||||||
|
|
||||||
from .page import (
|
from .page import PageSerializer, PageLogSerializer, SubPageSerializer, PageFavoriteSerializer
|
||||||
PageSerializer,
|
|
||||||
PageLogSerializer,
|
|
||||||
SubPageSerializer,
|
|
||||||
PageFavoriteSerializer,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .estimate import (
|
from .estimate import (
|
||||||
EstimateSerializer,
|
EstimateSerializer,
|
||||||
EstimatePointSerializer,
|
EstimatePointSerializer,
|
||||||
EstimateReadSerializer,
|
EstimateReadSerializer,
|
||||||
WorkspaceEstimateSerializer,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
from .inbox import (
|
from .inbox import InboxSerializer, InboxIssueSerializer, IssueStateInboxSerializer
|
||||||
InboxSerializer,
|
|
||||||
InboxIssueSerializer,
|
|
||||||
IssueStateInboxSerializer,
|
|
||||||
InboxIssueLiteSerializer,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .analytic import AnalyticViewSerializer
|
from .analytic import AnalyticViewSerializer
|
||||||
|
|
||||||
from .notification import NotificationSerializer, UserNotificationPreferenceSerializer
|
from .notification import NotificationSerializer
|
||||||
|
|
||||||
from .exporter import ExporterHistorySerializer
|
from .exporter import ExporterHistorySerializer
|
||||||
|
|
||||||
from .webhook import WebhookSerializer, WebhookLogSerializer
|
from .webhook import WebhookSerializer, WebhookLogSerializer
|
||||||
|
|
||||||
from .dashboard import DashboardSerializer, WidgetSerializer
|
|
||||||
@@ -3,6 +3,7 @@ from plane.db.models import APIToken, APIActivityLog
|
|||||||
|
|
||||||
|
|
||||||
class APITokenSerializer(BaseSerializer):
|
class APITokenSerializer(BaseSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = APIToken
|
model = APIToken
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
@@ -17,12 +18,14 @@ class APITokenSerializer(BaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class APITokenReadSerializer(BaseSerializer):
|
class APITokenReadSerializer(BaseSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = APIToken
|
model = APIToken
|
||||||
exclude = ("token",)
|
exclude = ('token',)
|
||||||
|
|
||||||
|
|
||||||
class APIActivityLogSerializer(BaseSerializer):
|
class APIActivityLogSerializer(BaseSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = APIActivityLog
|
model = APIActivityLog
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
|||||||
@@ -4,17 +4,16 @@ from rest_framework import serializers
|
|||||||
class BaseSerializer(serializers.ModelSerializer):
|
class BaseSerializer(serializers.ModelSerializer):
|
||||||
id = serializers.PrimaryKeyRelatedField(read_only=True)
|
id = serializers.PrimaryKeyRelatedField(read_only=True)
|
||||||
|
|
||||||
|
|
||||||
class DynamicBaseSerializer(BaseSerializer):
|
class DynamicBaseSerializer(BaseSerializer):
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
# If 'fields' is provided in the arguments, remove it and store it separately.
|
# If 'fields' is provided in the arguments, remove it and store it separately.
|
||||||
# This is done so as not to pass this custom argument up to the superclass.
|
# This is done so as not to pass this custom argument up to the superclass.
|
||||||
fields = kwargs.pop("fields", [])
|
fields = kwargs.pop("fields", None)
|
||||||
self.expand = kwargs.pop("expand", []) or []
|
|
||||||
fields = self.expand
|
|
||||||
|
|
||||||
# Call the initialization of the superclass.
|
# Call the initialization of the superclass.
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
# If 'fields' was provided, filter the fields of the serializer accordingly.
|
# If 'fields' was provided, filter the fields of the serializer accordingly.
|
||||||
if fields is not None:
|
if fields is not None:
|
||||||
self.fields = self._filter_fields(fields)
|
self.fields = self._filter_fields(fields)
|
||||||
@@ -32,7 +31,7 @@ class DynamicBaseSerializer(BaseSerializer):
|
|||||||
# loop through its keys and values.
|
# loop through its keys and values.
|
||||||
if isinstance(field_name, dict):
|
if isinstance(field_name, dict):
|
||||||
for key, value in field_name.items():
|
for key, value in field_name.items():
|
||||||
# If the value of this nested field is a list,
|
# If the value of this nested field is a list,
|
||||||
# perform a recursive filter on it.
|
# perform a recursive filter on it.
|
||||||
if isinstance(value, list):
|
if isinstance(value, list):
|
||||||
self._filter_fields(self.fields[key], value)
|
self._filter_fields(self.fields[key], value)
|
||||||
@@ -48,101 +47,12 @@ class DynamicBaseSerializer(BaseSerializer):
|
|||||||
elif isinstance(item, dict):
|
elif isinstance(item, dict):
|
||||||
allowed.append(list(item.keys())[0])
|
allowed.append(list(item.keys())[0])
|
||||||
|
|
||||||
for field in allowed:
|
# Convert the current serializer's fields and the allowed fields to sets.
|
||||||
if field not in self.fields:
|
existing = set(self.fields)
|
||||||
from . import (
|
allowed = set(allowed)
|
||||||
WorkspaceLiteSerializer,
|
|
||||||
ProjectLiteSerializer,
|
|
||||||
UserLiteSerializer,
|
|
||||||
StateLiteSerializer,
|
|
||||||
IssueSerializer,
|
|
||||||
LabelSerializer,
|
|
||||||
CycleIssueSerializer,
|
|
||||||
IssueFlatSerializer,
|
|
||||||
IssueRelationSerializer,
|
|
||||||
InboxIssueLiteSerializer
|
|
||||||
)
|
|
||||||
|
|
||||||
# Expansion mapper
|
# Remove fields from the serializer that aren't in the 'allowed' list.
|
||||||
expansion = {
|
for field_name in (existing - allowed):
|
||||||
"user": UserLiteSerializer,
|
self.fields.pop(field_name)
|
||||||
"workspace": WorkspaceLiteSerializer,
|
|
||||||
"project": ProjectLiteSerializer,
|
|
||||||
"default_assignee": UserLiteSerializer,
|
|
||||||
"project_lead": UserLiteSerializer,
|
|
||||||
"state": StateLiteSerializer,
|
|
||||||
"created_by": UserLiteSerializer,
|
|
||||||
"issue": IssueSerializer,
|
|
||||||
"actor": UserLiteSerializer,
|
|
||||||
"owned_by": UserLiteSerializer,
|
|
||||||
"members": UserLiteSerializer,
|
|
||||||
"assignees": UserLiteSerializer,
|
|
||||||
"labels": LabelSerializer,
|
|
||||||
"issue_cycle": CycleIssueSerializer,
|
|
||||||
"parent": IssueSerializer,
|
|
||||||
"issue_relation": IssueRelationSerializer,
|
|
||||||
"issue_inbox" : InboxIssueLiteSerializer,
|
|
||||||
}
|
|
||||||
|
|
||||||
self.fields[field] = expansion[field](many=True if field in ["members", "assignees", "labels", "issue_cycle", "issue_relation", "issue_inbox"] else False)
|
|
||||||
|
|
||||||
return self.fields
|
return self.fields
|
||||||
|
|
||||||
def to_representation(self, instance):
|
|
||||||
response = super().to_representation(instance)
|
|
||||||
|
|
||||||
# Ensure 'expand' is iterable before processing
|
|
||||||
if self.expand:
|
|
||||||
for expand in self.expand:
|
|
||||||
if expand in self.fields:
|
|
||||||
# Import all the expandable serializers
|
|
||||||
from . import (
|
|
||||||
WorkspaceLiteSerializer,
|
|
||||||
ProjectLiteSerializer,
|
|
||||||
UserLiteSerializer,
|
|
||||||
StateLiteSerializer,
|
|
||||||
IssueSerializer,
|
|
||||||
LabelSerializer,
|
|
||||||
CycleIssueSerializer,
|
|
||||||
IssueRelationSerializer,
|
|
||||||
InboxIssueLiteSerializer
|
|
||||||
)
|
|
||||||
|
|
||||||
# Expansion mapper
|
|
||||||
expansion = {
|
|
||||||
"user": UserLiteSerializer,
|
|
||||||
"workspace": WorkspaceLiteSerializer,
|
|
||||||
"project": ProjectLiteSerializer,
|
|
||||||
"default_assignee": UserLiteSerializer,
|
|
||||||
"project_lead": UserLiteSerializer,
|
|
||||||
"state": StateLiteSerializer,
|
|
||||||
"created_by": UserLiteSerializer,
|
|
||||||
"issue": IssueSerializer,
|
|
||||||
"actor": UserLiteSerializer,
|
|
||||||
"owned_by": UserLiteSerializer,
|
|
||||||
"members": UserLiteSerializer,
|
|
||||||
"assignees": UserLiteSerializer,
|
|
||||||
"labels": LabelSerializer,
|
|
||||||
"issue_cycle": CycleIssueSerializer,
|
|
||||||
"parent": IssueSerializer,
|
|
||||||
"issue_relation": IssueRelationSerializer,
|
|
||||||
"issue_inbox" : InboxIssueLiteSerializer,
|
|
||||||
}
|
|
||||||
# Check if field in expansion then expand the field
|
|
||||||
if expand in expansion:
|
|
||||||
if isinstance(response.get(expand), list):
|
|
||||||
exp_serializer = expansion[expand](
|
|
||||||
getattr(instance, expand), many=True
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
exp_serializer = expansion[expand](
|
|
||||||
getattr(instance, expand)
|
|
||||||
)
|
|
||||||
response[expand] = exp_serializer.data
|
|
||||||
else:
|
|
||||||
# You might need to handle this case differently
|
|
||||||
response[expand] = getattr(
|
|
||||||
instance, f"{expand}_id", None
|
|
||||||
)
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|||||||
@@ -7,12 +7,7 @@ from .user import UserLiteSerializer
|
|||||||
from .issue import IssueStateSerializer
|
from .issue import IssueStateSerializer
|
||||||
from .workspace import WorkspaceLiteSerializer
|
from .workspace import WorkspaceLiteSerializer
|
||||||
from .project import ProjectLiteSerializer
|
from .project import ProjectLiteSerializer
|
||||||
from plane.db.models import (
|
from plane.db.models import Cycle, CycleIssue, CycleFavorite
|
||||||
Cycle,
|
|
||||||
CycleIssue,
|
|
||||||
CycleFavorite,
|
|
||||||
CycleUserProperties,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class CycleWriteSerializer(BaseSerializer):
|
class CycleWriteSerializer(BaseSerializer):
|
||||||
@@ -22,9 +17,7 @@ class CycleWriteSerializer(BaseSerializer):
|
|||||||
and data.get("end_date", None) is not None
|
and data.get("end_date", None) is not None
|
||||||
and data.get("start_date", None) > data.get("end_date", None)
|
and data.get("start_date", None) > data.get("end_date", None)
|
||||||
):
|
):
|
||||||
raise serializers.ValidationError(
|
raise serializers.ValidationError("Start date cannot exceed end date")
|
||||||
"Start date cannot exceed end date"
|
|
||||||
)
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -33,6 +26,7 @@ class CycleWriteSerializer(BaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class CycleSerializer(BaseSerializer):
|
class CycleSerializer(BaseSerializer):
|
||||||
|
owned_by = UserLiteSerializer(read_only=True)
|
||||||
is_favorite = serializers.BooleanField(read_only=True)
|
is_favorite = serializers.BooleanField(read_only=True)
|
||||||
total_issues = serializers.IntegerField(read_only=True)
|
total_issues = serializers.IntegerField(read_only=True)
|
||||||
cancelled_issues = serializers.IntegerField(read_only=True)
|
cancelled_issues = serializers.IntegerField(read_only=True)
|
||||||
@@ -44,11 +38,8 @@ class CycleSerializer(BaseSerializer):
|
|||||||
total_estimates = serializers.IntegerField(read_only=True)
|
total_estimates = serializers.IntegerField(read_only=True)
|
||||||
completed_estimates = serializers.IntegerField(read_only=True)
|
completed_estimates = serializers.IntegerField(read_only=True)
|
||||||
started_estimates = serializers.IntegerField(read_only=True)
|
started_estimates = serializers.IntegerField(read_only=True)
|
||||||
workspace_detail = WorkspaceLiteSerializer(
|
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
|
||||||
read_only=True, source="workspace"
|
|
||||||
)
|
|
||||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||||
status = serializers.CharField(read_only=True)
|
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
if (
|
if (
|
||||||
@@ -56,9 +47,7 @@ class CycleSerializer(BaseSerializer):
|
|||||||
and data.get("end_date", None) is not None
|
and data.get("end_date", None) is not None
|
||||||
and data.get("start_date", None) > data.get("end_date", None)
|
and data.get("start_date", None) > data.get("end_date", None)
|
||||||
):
|
):
|
||||||
raise serializers.ValidationError(
|
raise serializers.ValidationError("Start date cannot exceed end date")
|
||||||
"Start date cannot exceed end date"
|
|
||||||
)
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def get_assignees(self, obj):
|
def get_assignees(self, obj):
|
||||||
@@ -116,14 +105,3 @@ class CycleFavoriteSerializer(BaseSerializer):
|
|||||||
"project",
|
"project",
|
||||||
"user",
|
"user",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class CycleUserPropertiesSerializer(BaseSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = CycleUserProperties
|
|
||||||
fields = "__all__"
|
|
||||||
read_only_fields = [
|
|
||||||
"workspace",
|
|
||||||
"project",
|
|
||||||
"cycle" "user",
|
|
||||||
]
|
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
# Module imports
|
|
||||||
from .base import BaseSerializer
|
|
||||||
from plane.db.models import Dashboard, Widget
|
|
||||||
|
|
||||||
# Third party frameworks
|
|
||||||
from rest_framework import serializers
|
|
||||||
|
|
||||||
|
|
||||||
class DashboardSerializer(BaseSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = Dashboard
|
|
||||||
fields = "__all__"
|
|
||||||
|
|
||||||
|
|
||||||
class WidgetSerializer(BaseSerializer):
|
|
||||||
is_visible = serializers.BooleanField(read_only=True)
|
|
||||||
widget_filters = serializers.JSONField(read_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Widget
|
|
||||||
fields = [
|
|
||||||
"id",
|
|
||||||
"key",
|
|
||||||
"is_visible",
|
|
||||||
"widget_filters"
|
|
||||||
]
|
|
||||||
@@ -2,18 +2,11 @@
|
|||||||
from .base import BaseSerializer
|
from .base import BaseSerializer
|
||||||
|
|
||||||
from plane.db.models import Estimate, EstimatePoint
|
from plane.db.models import Estimate, EstimatePoint
|
||||||
from plane.app.serializers import (
|
from plane.app.serializers import WorkspaceLiteSerializer, ProjectLiteSerializer
|
||||||
WorkspaceLiteSerializer,
|
|
||||||
ProjectLiteSerializer,
|
|
||||||
)
|
|
||||||
|
|
||||||
from rest_framework import serializers
|
|
||||||
|
|
||||||
|
|
||||||
class EstimateSerializer(BaseSerializer):
|
class EstimateSerializer(BaseSerializer):
|
||||||
workspace_detail = WorkspaceLiteSerializer(
|
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
|
||||||
read_only=True, source="workspace"
|
|
||||||
)
|
|
||||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -26,16 +19,6 @@ class EstimateSerializer(BaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class EstimatePointSerializer(BaseSerializer):
|
class EstimatePointSerializer(BaseSerializer):
|
||||||
def validate(self, data):
|
|
||||||
if not data:
|
|
||||||
raise serializers.ValidationError("Estimate points are required")
|
|
||||||
value = data.get("value")
|
|
||||||
if value and len(value) > 20:
|
|
||||||
raise serializers.ValidationError(
|
|
||||||
"Value can't be more than 20 characters"
|
|
||||||
)
|
|
||||||
return data
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = EstimatePoint
|
model = EstimatePoint
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
@@ -48,9 +31,7 @@ class EstimatePointSerializer(BaseSerializer):
|
|||||||
|
|
||||||
class EstimateReadSerializer(BaseSerializer):
|
class EstimateReadSerializer(BaseSerializer):
|
||||||
points = EstimatePointSerializer(read_only=True, many=True)
|
points = EstimatePointSerializer(read_only=True, many=True)
|
||||||
workspace_detail = WorkspaceLiteSerializer(
|
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
|
||||||
read_only=True, source="workspace"
|
|
||||||
)
|
|
||||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -61,18 +42,3 @@ class EstimateReadSerializer(BaseSerializer):
|
|||||||
"name",
|
"name",
|
||||||
"description",
|
"description",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class WorkspaceEstimateSerializer(BaseSerializer):
|
|
||||||
points = EstimatePointSerializer(read_only=True, many=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Estimate
|
|
||||||
fields = "__all__"
|
|
||||||
read_only_fields = [
|
|
||||||
"points",
|
|
||||||
"name",
|
|
||||||
"description",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,7 @@ from .user import UserLiteSerializer
|
|||||||
|
|
||||||
|
|
||||||
class ExporterHistorySerializer(BaseSerializer):
|
class ExporterHistorySerializer(BaseSerializer):
|
||||||
initiated_by_detail = UserLiteSerializer(
|
initiated_by_detail = UserLiteSerializer(source="initiated_by", read_only=True)
|
||||||
source="initiated_by", read_only=True
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ExporterHistory
|
model = ExporterHistory
|
||||||
|
|||||||
@@ -7,13 +7,9 @@ from plane.db.models import Importer
|
|||||||
|
|
||||||
|
|
||||||
class ImporterSerializer(BaseSerializer):
|
class ImporterSerializer(BaseSerializer):
|
||||||
initiated_by_detail = UserLiteSerializer(
|
initiated_by_detail = UserLiteSerializer(source="initiated_by", read_only=True)
|
||||||
source="initiated_by", read_only=True
|
|
||||||
)
|
|
||||||
project_detail = ProjectLiteSerializer(source="project", read_only=True)
|
project_detail = ProjectLiteSerializer(source="project", read_only=True)
|
||||||
workspace_detail = WorkspaceLiteSerializer(
|
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
|
||||||
source="workspace", read_only=True
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Importer
|
model = Importer
|
||||||
|
|||||||
@@ -46,13 +46,10 @@ class InboxIssueLiteSerializer(BaseSerializer):
|
|||||||
class IssueStateInboxSerializer(BaseSerializer):
|
class IssueStateInboxSerializer(BaseSerializer):
|
||||||
state_detail = StateLiteSerializer(read_only=True, source="state")
|
state_detail = StateLiteSerializer(read_only=True, source="state")
|
||||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||||
label_details = LabelLiteSerializer(
|
label_details = LabelLiteSerializer(read_only=True, source="labels", many=True)
|
||||||
read_only=True, source="labels", many=True
|
assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
|
||||||
)
|
|
||||||
assignee_details = UserLiteSerializer(
|
|
||||||
read_only=True, source="assignees", many=True
|
|
||||||
)
|
|
||||||
sub_issues_count = serializers.IntegerField(read_only=True)
|
sub_issues_count = serializers.IntegerField(read_only=True)
|
||||||
|
bridge_id = serializers.UUIDField(read_only=True)
|
||||||
issue_inbox = InboxIssueLiteSerializer(read_only=True, many=True)
|
issue_inbox = InboxIssueLiteSerializer(read_only=True, many=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|||||||
@@ -13,9 +13,7 @@ class IntegrationSerializer(BaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class WorkspaceIntegrationSerializer(BaseSerializer):
|
class WorkspaceIntegrationSerializer(BaseSerializer):
|
||||||
integration_detail = IntegrationSerializer(
|
integration_detail = IntegrationSerializer(read_only=True, source="integration")
|
||||||
read_only=True, source="integration"
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = WorkspaceIntegration
|
model = WorkspaceIntegration
|
||||||
|
|||||||
@@ -30,8 +30,6 @@ from plane.db.models import (
|
|||||||
CommentReaction,
|
CommentReaction,
|
||||||
IssueVote,
|
IssueVote,
|
||||||
IssueRelation,
|
IssueRelation,
|
||||||
State,
|
|
||||||
Project,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -71,26 +69,19 @@ class IssueProjectLiteSerializer(BaseSerializer):
|
|||||||
##TODO: Find a better way to write this serializer
|
##TODO: Find a better way to write this serializer
|
||||||
## Find a better approach to save manytomany?
|
## Find a better approach to save manytomany?
|
||||||
class IssueCreateSerializer(BaseSerializer):
|
class IssueCreateSerializer(BaseSerializer):
|
||||||
# ids
|
state_detail = StateSerializer(read_only=True, source="state")
|
||||||
state_id = serializers.PrimaryKeyRelatedField(
|
created_by_detail = UserLiteSerializer(read_only=True, source="created_by")
|
||||||
source="state",
|
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||||
queryset=State.objects.all(),
|
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
|
||||||
required=False,
|
|
||||||
allow_null=True,
|
assignees = serializers.ListField(
|
||||||
)
|
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
|
||||||
parent_id = serializers.PrimaryKeyRelatedField(
|
|
||||||
source="parent",
|
|
||||||
queryset=Issue.objects.all(),
|
|
||||||
required=False,
|
|
||||||
allow_null=True,
|
|
||||||
)
|
|
||||||
label_ids = serializers.ListField(
|
|
||||||
child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()),
|
|
||||||
write_only=True,
|
write_only=True,
|
||||||
required=False,
|
required=False,
|
||||||
)
|
)
|
||||||
assignee_ids = serializers.ListField(
|
|
||||||
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
|
labels = serializers.ListField(
|
||||||
|
child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()),
|
||||||
write_only=True,
|
write_only=True,
|
||||||
required=False,
|
required=False,
|
||||||
)
|
)
|
||||||
@@ -109,10 +100,8 @@ class IssueCreateSerializer(BaseSerializer):
|
|||||||
|
|
||||||
def to_representation(self, instance):
|
def to_representation(self, instance):
|
||||||
data = super().to_representation(instance)
|
data = super().to_representation(instance)
|
||||||
assignee_ids = self.initial_data.get("assignee_ids")
|
data['assignees'] = [str(assignee.id) for assignee in instance.assignees.all()]
|
||||||
data["assignee_ids"] = assignee_ids if assignee_ids else []
|
data['labels'] = [str(label.id) for label in instance.labels.all()]
|
||||||
label_ids = self.initial_data.get("label_ids")
|
|
||||||
data["label_ids"] = label_ids if label_ids else []
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
@@ -121,14 +110,12 @@ class IssueCreateSerializer(BaseSerializer):
|
|||||||
and data.get("target_date", None) is not None
|
and data.get("target_date", None) is not None
|
||||||
and data.get("start_date", None) > data.get("target_date", None)
|
and data.get("start_date", None) > data.get("target_date", None)
|
||||||
):
|
):
|
||||||
raise serializers.ValidationError(
|
raise serializers.ValidationError("Start date cannot exceed target date")
|
||||||
"Start date cannot exceed target date"
|
|
||||||
)
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
assignees = validated_data.pop("assignee_ids", None)
|
assignees = validated_data.pop("assignees", None)
|
||||||
labels = validated_data.pop("label_ids", None)
|
labels = validated_data.pop("labels", None)
|
||||||
|
|
||||||
project_id = self.context["project_id"]
|
project_id = self.context["project_id"]
|
||||||
workspace_id = self.context["workspace_id"]
|
workspace_id = self.context["workspace_id"]
|
||||||
@@ -186,8 +173,8 @@ class IssueCreateSerializer(BaseSerializer):
|
|||||||
return issue
|
return issue
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
assignees = validated_data.pop("assignee_ids", None)
|
assignees = validated_data.pop("assignees", None)
|
||||||
labels = validated_data.pop("label_ids", None)
|
labels = validated_data.pop("labels", None)
|
||||||
|
|
||||||
# Related models
|
# Related models
|
||||||
project_id = instance.project_id
|
project_id = instance.project_id
|
||||||
@@ -238,15 +225,13 @@ class IssueActivitySerializer(BaseSerializer):
|
|||||||
actor_detail = UserLiteSerializer(read_only=True, source="actor")
|
actor_detail = UserLiteSerializer(read_only=True, source="actor")
|
||||||
issue_detail = IssueFlatSerializer(read_only=True, source="issue")
|
issue_detail = IssueFlatSerializer(read_only=True, source="issue")
|
||||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||||
workspace_detail = WorkspaceLiteSerializer(
|
|
||||||
read_only=True, source="workspace"
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IssueActivity
|
model = IssueActivity
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class IssuePropertySerializer(BaseSerializer):
|
class IssuePropertySerializer(BaseSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IssueProperty
|
model = IssueProperty
|
||||||
@@ -259,17 +244,12 @@ class IssuePropertySerializer(BaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class LabelSerializer(BaseSerializer):
|
class LabelSerializer(BaseSerializer):
|
||||||
|
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
|
||||||
|
project_detail = ProjectLiteSerializer(source="project", read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Label
|
model = Label
|
||||||
fields = [
|
fields = "__all__"
|
||||||
"parent",
|
|
||||||
"name",
|
|
||||||
"color",
|
|
||||||
"id",
|
|
||||||
"project_id",
|
|
||||||
"workspace_id",
|
|
||||||
"sort_order",
|
|
||||||
]
|
|
||||||
read_only_fields = [
|
read_only_fields = [
|
||||||
"workspace",
|
"workspace",
|
||||||
"project",
|
"project",
|
||||||
@@ -287,6 +267,7 @@ class LabelLiteSerializer(BaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class IssueLabelSerializer(BaseSerializer):
|
class IssueLabelSerializer(BaseSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IssueLabel
|
model = IssueLabel
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
@@ -297,50 +278,33 @@ class IssueLabelSerializer(BaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class IssueRelationSerializer(BaseSerializer):
|
class IssueRelationSerializer(BaseSerializer):
|
||||||
id = serializers.UUIDField(source="related_issue.id", read_only=True)
|
issue_detail = IssueProjectLiteSerializer(read_only=True, source="related_issue")
|
||||||
project_id = serializers.PrimaryKeyRelatedField(
|
|
||||||
source="related_issue.project_id", read_only=True
|
|
||||||
)
|
|
||||||
sequence_id = serializers.IntegerField(
|
|
||||||
source="related_issue.sequence_id", read_only=True
|
|
||||||
)
|
|
||||||
name = serializers.CharField(source="related_issue.name", read_only=True)
|
|
||||||
relation_type = serializers.CharField(read_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IssueRelation
|
model = IssueRelation
|
||||||
fields = [
|
fields = [
|
||||||
"id",
|
"issue_detail",
|
||||||
"project_id",
|
|
||||||
"sequence_id",
|
|
||||||
"relation_type",
|
"relation_type",
|
||||||
"name",
|
"related_issue",
|
||||||
|
"issue",
|
||||||
|
"id"
|
||||||
]
|
]
|
||||||
read_only_fields = [
|
read_only_fields = [
|
||||||
"workspace",
|
"workspace",
|
||||||
"project",
|
"project",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class RelatedIssueSerializer(BaseSerializer):
|
class RelatedIssueSerializer(BaseSerializer):
|
||||||
id = serializers.UUIDField(source="issue.id", read_only=True)
|
issue_detail = IssueProjectLiteSerializer(read_only=True, source="issue")
|
||||||
project_id = serializers.PrimaryKeyRelatedField(
|
|
||||||
source="issue.project_id", read_only=True
|
|
||||||
)
|
|
||||||
sequence_id = serializers.IntegerField(
|
|
||||||
source="issue.sequence_id", read_only=True
|
|
||||||
)
|
|
||||||
name = serializers.CharField(source="issue.name", read_only=True)
|
|
||||||
relation_type = serializers.CharField(read_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IssueRelation
|
model = IssueRelation
|
||||||
fields = [
|
fields = [
|
||||||
"id",
|
"issue_detail",
|
||||||
"project_id",
|
|
||||||
"sequence_id",
|
|
||||||
"relation_type",
|
"relation_type",
|
||||||
"name",
|
"related_issue",
|
||||||
|
"issue",
|
||||||
|
"id"
|
||||||
]
|
]
|
||||||
read_only_fields = [
|
read_only_fields = [
|
||||||
"workspace",
|
"workspace",
|
||||||
@@ -435,8 +399,7 @@ class IssueLinkSerializer(BaseSerializer):
|
|||||||
# Validation if url already exists
|
# Validation if url already exists
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
if IssueLink.objects.filter(
|
if IssueLink.objects.filter(
|
||||||
url=validated_data.get("url"),
|
url=validated_data.get("url"), issue_id=validated_data.get("issue_id")
|
||||||
issue_id=validated_data.get("issue_id"),
|
|
||||||
).exists():
|
).exists():
|
||||||
raise serializers.ValidationError(
|
raise serializers.ValidationError(
|
||||||
{"error": "URL already exists for this Issue"}
|
{"error": "URL already exists for this Issue"}
|
||||||
@@ -460,8 +423,9 @@ class IssueAttachmentSerializer(BaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class IssueReactionSerializer(BaseSerializer):
|
class IssueReactionSerializer(BaseSerializer):
|
||||||
|
|
||||||
actor_detail = UserLiteSerializer(read_only=True, source="actor")
|
actor_detail = UserLiteSerializer(read_only=True, source="actor")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IssueReaction
|
model = IssueReaction
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
@@ -473,6 +437,19 @@ class IssueReactionSerializer(BaseSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class CommentReactionLiteSerializer(BaseSerializer):
|
||||||
|
actor_detail = UserLiteSerializer(read_only=True, source="actor")
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = CommentReaction
|
||||||
|
fields = [
|
||||||
|
"id",
|
||||||
|
"reaction",
|
||||||
|
"comment",
|
||||||
|
"actor_detail",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class CommentReactionSerializer(BaseSerializer):
|
class CommentReactionSerializer(BaseSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CommentReaction
|
model = CommentReaction
|
||||||
@@ -481,18 +458,12 @@ class CommentReactionSerializer(BaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class IssueVoteSerializer(BaseSerializer):
|
class IssueVoteSerializer(BaseSerializer):
|
||||||
|
|
||||||
actor_detail = UserLiteSerializer(read_only=True, source="actor")
|
actor_detail = UserLiteSerializer(read_only=True, source="actor")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IssueVote
|
model = IssueVote
|
||||||
fields = [
|
fields = ["issue", "vote", "workspace", "project", "actor", "actor_detail"]
|
||||||
"issue",
|
|
||||||
"vote",
|
|
||||||
"workspace",
|
|
||||||
"project",
|
|
||||||
"actor",
|
|
||||||
"actor_detail",
|
|
||||||
]
|
|
||||||
read_only_fields = fields
|
read_only_fields = fields
|
||||||
|
|
||||||
|
|
||||||
@@ -500,12 +471,8 @@ class IssueCommentSerializer(BaseSerializer):
|
|||||||
actor_detail = UserLiteSerializer(read_only=True, source="actor")
|
actor_detail = UserLiteSerializer(read_only=True, source="actor")
|
||||||
issue_detail = IssueFlatSerializer(read_only=True, source="issue")
|
issue_detail = IssueFlatSerializer(read_only=True, source="issue")
|
||||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||||
workspace_detail = WorkspaceLiteSerializer(
|
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
|
||||||
read_only=True, source="workspace"
|
comment_reactions = CommentReactionLiteSerializer(read_only=True, many=True)
|
||||||
)
|
|
||||||
comment_reactions = CommentReactionSerializer(
|
|
||||||
read_only=True, many=True
|
|
||||||
)
|
|
||||||
is_member = serializers.BooleanField(read_only=True)
|
is_member = serializers.BooleanField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -539,15 +506,12 @@ class IssueStateFlatSerializer(BaseSerializer):
|
|||||||
|
|
||||||
# Issue Serializer with state details
|
# Issue Serializer with state details
|
||||||
class IssueStateSerializer(DynamicBaseSerializer):
|
class IssueStateSerializer(DynamicBaseSerializer):
|
||||||
label_details = LabelLiteSerializer(
|
label_details = LabelLiteSerializer(read_only=True, source="labels", many=True)
|
||||||
read_only=True, source="labels", many=True
|
|
||||||
)
|
|
||||||
state_detail = StateLiteSerializer(read_only=True, source="state")
|
state_detail = StateLiteSerializer(read_only=True, source="state")
|
||||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||||
assignee_details = UserLiteSerializer(
|
assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
|
||||||
read_only=True, source="assignees", many=True
|
|
||||||
)
|
|
||||||
sub_issues_count = serializers.IntegerField(read_only=True)
|
sub_issues_count = serializers.IntegerField(read_only=True)
|
||||||
|
bridge_id = serializers.UUIDField(read_only=True)
|
||||||
attachment_count = serializers.IntegerField(read_only=True)
|
attachment_count = serializers.IntegerField(read_only=True)
|
||||||
link_count = serializers.IntegerField(read_only=True)
|
link_count = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
@@ -556,80 +520,40 @@ class IssueStateSerializer(DynamicBaseSerializer):
|
|||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
class IssueSerializer(DynamicBaseSerializer):
|
class IssueSerializer(BaseSerializer):
|
||||||
# ids
|
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||||
project_id = serializers.PrimaryKeyRelatedField(read_only=True)
|
state_detail = StateSerializer(read_only=True, source="state")
|
||||||
state_id = serializers.PrimaryKeyRelatedField(read_only=True)
|
parent_detail = IssueStateFlatSerializer(read_only=True, source="parent")
|
||||||
parent_id = serializers.PrimaryKeyRelatedField(read_only=True)
|
label_details = LabelSerializer(read_only=True, source="labels", many=True)
|
||||||
cycle_id = serializers.PrimaryKeyRelatedField(read_only=True)
|
assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
|
||||||
module_ids = serializers.SerializerMethodField()
|
related_issues = IssueRelationSerializer(read_only=True, source="issue_relation", many=True)
|
||||||
|
issue_relations = RelatedIssueSerializer(read_only=True, source="issue_related", many=True)
|
||||||
# Many to many
|
issue_cycle = IssueCycleDetailSerializer(read_only=True)
|
||||||
label_ids = serializers.PrimaryKeyRelatedField(
|
issue_module = IssueModuleDetailSerializer(read_only=True)
|
||||||
read_only=True, many=True, source="labels"
|
issue_link = IssueLinkSerializer(read_only=True, many=True)
|
||||||
)
|
issue_attachment = IssueAttachmentSerializer(read_only=True, many=True)
|
||||||
assignee_ids = serializers.PrimaryKeyRelatedField(
|
|
||||||
read_only=True, many=True, source="assignees"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Count items
|
|
||||||
sub_issues_count = serializers.IntegerField(read_only=True)
|
sub_issues_count = serializers.IntegerField(read_only=True)
|
||||||
attachment_count = serializers.IntegerField(read_only=True)
|
issue_reactions = IssueReactionSerializer(read_only=True, many=True)
|
||||||
link_count = serializers.IntegerField(read_only=True)
|
|
||||||
|
|
||||||
# is_subscribed
|
|
||||||
is_subscribed = serializers.BooleanField(read_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Issue
|
model = Issue
|
||||||
fields = [
|
fields = "__all__"
|
||||||
"id",
|
read_only_fields = [
|
||||||
"name",
|
"workspace",
|
||||||
"state_id",
|
"project",
|
||||||
"description_html",
|
|
||||||
"sort_order",
|
|
||||||
"completed_at",
|
|
||||||
"estimate_point",
|
|
||||||
"priority",
|
|
||||||
"start_date",
|
|
||||||
"target_date",
|
|
||||||
"sequence_id",
|
|
||||||
"project_id",
|
|
||||||
"parent_id",
|
|
||||||
"cycle_id",
|
|
||||||
"module_ids",
|
|
||||||
"label_ids",
|
|
||||||
"assignee_ids",
|
|
||||||
"sub_issues_count",
|
|
||||||
"created_at",
|
|
||||||
"updated_at",
|
|
||||||
"created_by",
|
"created_by",
|
||||||
"updated_by",
|
"updated_by",
|
||||||
"attachment_count",
|
"created_at",
|
||||||
"link_count",
|
"updated_at",
|
||||||
"is_subscribed",
|
|
||||||
"is_draft",
|
|
||||||
"archived_at",
|
|
||||||
]
|
]
|
||||||
read_only_fields = fields
|
|
||||||
|
|
||||||
def get_module_ids(self, obj):
|
|
||||||
# Access the prefetched modules and extract module IDs
|
|
||||||
return [module for module in obj.issue_module.values_list("module_id", flat=True)]
|
|
||||||
|
|
||||||
|
|
||||||
class IssueLiteSerializer(DynamicBaseSerializer):
|
class IssueLiteSerializer(DynamicBaseSerializer):
|
||||||
workspace_detail = WorkspaceLiteSerializer(
|
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
|
||||||
read_only=True, source="workspace"
|
|
||||||
)
|
|
||||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||||
state_detail = StateLiteSerializer(read_only=True, source="state")
|
state_detail = StateLiteSerializer(read_only=True, source="state")
|
||||||
label_details = LabelLiteSerializer(
|
label_details = LabelLiteSerializer(read_only=True, source="labels", many=True)
|
||||||
read_only=True, source="labels", many=True
|
assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
|
||||||
)
|
|
||||||
assignee_details = UserLiteSerializer(
|
|
||||||
read_only=True, source="assignees", many=True
|
|
||||||
)
|
|
||||||
sub_issues_count = serializers.IntegerField(read_only=True)
|
sub_issues_count = serializers.IntegerField(read_only=True)
|
||||||
cycle_id = serializers.UUIDField(read_only=True)
|
cycle_id = serializers.UUIDField(read_only=True)
|
||||||
module_id = serializers.UUIDField(read_only=True)
|
module_id = serializers.UUIDField(read_only=True)
|
||||||
@@ -656,9 +580,7 @@ class IssueLiteSerializer(DynamicBaseSerializer):
|
|||||||
class IssuePublicSerializer(BaseSerializer):
|
class IssuePublicSerializer(BaseSerializer):
|
||||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||||
state_detail = StateLiteSerializer(read_only=True, source="state")
|
state_detail = StateLiteSerializer(read_only=True, source="state")
|
||||||
reactions = IssueReactionSerializer(
|
reactions = IssueReactionSerializer(read_only=True, many=True, source="issue_reactions")
|
||||||
read_only=True, many=True, source="issue_reactions"
|
|
||||||
)
|
|
||||||
votes = IssueVoteSerializer(read_only=True, many=True)
|
votes = IssueVoteSerializer(read_only=True, many=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -681,6 +603,7 @@ class IssuePublicSerializer(BaseSerializer):
|
|||||||
read_only_fields = fields
|
read_only_fields = fields
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class IssueSubscriberSerializer(BaseSerializer):
|
class IssueSubscriberSerializer(BaseSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IssueSubscriber
|
model = IssueSubscriber
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from .base import BaseSerializer, DynamicBaseSerializer
|
from .base import BaseSerializer
|
||||||
from .user import UserLiteSerializer
|
from .user import UserLiteSerializer
|
||||||
from .project import ProjectLiteSerializer
|
from .project import ProjectLiteSerializer
|
||||||
from .workspace import WorkspaceLiteSerializer
|
from .workspace import WorkspaceLiteSerializer
|
||||||
@@ -14,7 +14,6 @@ from plane.db.models import (
|
|||||||
ModuleIssue,
|
ModuleIssue,
|
||||||
ModuleLink,
|
ModuleLink,
|
||||||
ModuleFavorite,
|
ModuleFavorite,
|
||||||
ModuleUserProperties,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -26,9 +25,7 @@ class ModuleWriteSerializer(BaseSerializer):
|
|||||||
)
|
)
|
||||||
|
|
||||||
project_detail = ProjectLiteSerializer(source="project", read_only=True)
|
project_detail = ProjectLiteSerializer(source="project", read_only=True)
|
||||||
workspace_detail = WorkspaceLiteSerializer(
|
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
|
||||||
source="workspace", read_only=True
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Module
|
model = Module
|
||||||
@@ -41,22 +38,16 @@ class ModuleWriteSerializer(BaseSerializer):
|
|||||||
"created_at",
|
"created_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
]
|
]
|
||||||
|
|
||||||
def to_representation(self, instance):
|
def to_representation(self, instance):
|
||||||
data = super().to_representation(instance)
|
data = super().to_representation(instance)
|
||||||
data["members"] = [str(member.id) for member in instance.members.all()]
|
data['members'] = [str(member.id) for member in instance.members.all()]
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
if (
|
if data.get("start_date", None) is not None and data.get("target_date", None) is not None and data.get("start_date", None) > data.get("target_date", None):
|
||||||
data.get("start_date", None) is not None
|
raise serializers.ValidationError("Start date cannot exceed target date")
|
||||||
and data.get("target_date", None) is not None
|
return data
|
||||||
and data.get("start_date", None) > data.get("target_date", None)
|
|
||||||
):
|
|
||||||
raise serializers.ValidationError(
|
|
||||||
"Start date cannot exceed target date"
|
|
||||||
)
|
|
||||||
return data
|
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
members = validated_data.pop("members", None)
|
members = validated_data.pop("members", None)
|
||||||
@@ -160,8 +151,7 @@ class ModuleLinkSerializer(BaseSerializer):
|
|||||||
# Validation if url already exists
|
# Validation if url already exists
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
if ModuleLink.objects.filter(
|
if ModuleLink.objects.filter(
|
||||||
url=validated_data.get("url"),
|
url=validated_data.get("url"), module_id=validated_data.get("module_id")
|
||||||
module_id=validated_data.get("module_id"),
|
|
||||||
).exists():
|
).exists():
|
||||||
raise serializers.ValidationError(
|
raise serializers.ValidationError(
|
||||||
{"error": "URL already exists for this Issue"}
|
{"error": "URL already exists for this Issue"}
|
||||||
@@ -169,12 +159,10 @@ class ModuleLinkSerializer(BaseSerializer):
|
|||||||
return ModuleLink.objects.create(**validated_data)
|
return ModuleLink.objects.create(**validated_data)
|
||||||
|
|
||||||
|
|
||||||
class ModuleSerializer(DynamicBaseSerializer):
|
class ModuleSerializer(BaseSerializer):
|
||||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||||
lead_detail = UserLiteSerializer(read_only=True, source="lead")
|
lead_detail = UserLiteSerializer(read_only=True, source="lead")
|
||||||
members_detail = UserLiteSerializer(
|
members_detail = UserLiteSerializer(read_only=True, many=True, source="members")
|
||||||
read_only=True, many=True, source="members"
|
|
||||||
)
|
|
||||||
link_module = ModuleLinkSerializer(read_only=True, many=True)
|
link_module = ModuleLinkSerializer(read_only=True, many=True)
|
||||||
is_favorite = serializers.BooleanField(read_only=True)
|
is_favorite = serializers.BooleanField(read_only=True)
|
||||||
total_issues = serializers.IntegerField(read_only=True)
|
total_issues = serializers.IntegerField(read_only=True)
|
||||||
@@ -208,10 +196,3 @@ class ModuleFavoriteSerializer(BaseSerializer):
|
|||||||
"project",
|
"project",
|
||||||
"user",
|
"user",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class ModuleUserPropertiesSerializer(BaseSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = ModuleUserProperties
|
|
||||||
fields = "__all__"
|
|
||||||
read_only_fields = ["workspace", "project", "module", "user"]
|
|
||||||
|
|||||||
@@ -1,21 +1,12 @@
|
|||||||
# Module imports
|
# Module imports
|
||||||
from .base import BaseSerializer
|
from .base import BaseSerializer
|
||||||
from .user import UserLiteSerializer
|
from .user import UserLiteSerializer
|
||||||
from plane.db.models import Notification, UserNotificationPreference
|
from plane.db.models import Notification
|
||||||
|
|
||||||
|
|
||||||
class NotificationSerializer(BaseSerializer):
|
class NotificationSerializer(BaseSerializer):
|
||||||
triggered_by_details = UserLiteSerializer(
|
triggered_by_details = UserLiteSerializer(read_only=True, source="triggered_by")
|
||||||
read_only=True, source="triggered_by"
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Notification
|
model = Notification
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
class UserNotificationPreferenceSerializer(BaseSerializer):
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = UserNotificationPreference
|
|
||||||
fields = "__all__"
|
|
||||||
|
|||||||
@@ -6,31 +6,19 @@ 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 (
|
from plane.db.models import Page, PageLog, PageFavorite, PageLabel, Label, Issue, Module
|
||||||
Page,
|
|
||||||
PageLog,
|
|
||||||
PageFavorite,
|
|
||||||
PageLabel,
|
|
||||||
Label,
|
|
||||||
Issue,
|
|
||||||
Module,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class PageSerializer(BaseSerializer):
|
class PageSerializer(BaseSerializer):
|
||||||
is_favorite = serializers.BooleanField(read_only=True)
|
is_favorite = serializers.BooleanField(read_only=True)
|
||||||
label_details = LabelLiteSerializer(
|
label_details = LabelLiteSerializer(read_only=True, source="labels", many=True)
|
||||||
read_only=True, source="labels", many=True
|
|
||||||
)
|
|
||||||
labels = serializers.ListField(
|
labels = serializers.ListField(
|
||||||
child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()),
|
child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()),
|
||||||
write_only=True,
|
write_only=True,
|
||||||
required=False,
|
required=False,
|
||||||
)
|
)
|
||||||
project_detail = ProjectLiteSerializer(source="project", read_only=True)
|
project_detail = ProjectLiteSerializer(source="project", read_only=True)
|
||||||
workspace_detail = WorkspaceLiteSerializer(
|
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
|
||||||
source="workspace", read_only=True
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Page
|
model = Page
|
||||||
@@ -40,10 +28,9 @@ class PageSerializer(BaseSerializer):
|
|||||||
"project",
|
"project",
|
||||||
"owned_by",
|
"owned_by",
|
||||||
]
|
]
|
||||||
|
|
||||||
def to_representation(self, instance):
|
def to_representation(self, instance):
|
||||||
data = super().to_representation(instance)
|
data = super().to_representation(instance)
|
||||||
data["labels"] = [str(label.id) for label in instance.labels.all()]
|
data['labels'] = [str(label.id) for label in instance.labels.all()]
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
@@ -107,7 +94,7 @@ class SubPageSerializer(BaseSerializer):
|
|||||||
|
|
||||||
def get_entity_details(self, obj):
|
def get_entity_details(self, obj):
|
||||||
entity_name = obj.entity_name
|
entity_name = obj.entity_name
|
||||||
if entity_name == "forward_link" or entity_name == "back_link":
|
if entity_name == 'forward_link' or entity_name == 'back_link':
|
||||||
try:
|
try:
|
||||||
page = Page.objects.get(pk=obj.entity_identifier)
|
page = Page.objects.get(pk=obj.entity_identifier)
|
||||||
return PageSerializer(page).data
|
return PageSerializer(page).data
|
||||||
@@ -117,6 +104,7 @@ class SubPageSerializer(BaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class PageLogSerializer(BaseSerializer):
|
class PageLogSerializer(BaseSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PageLog
|
model = PageLog
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
|||||||
@@ -4,10 +4,7 @@ from rest_framework import serializers
|
|||||||
# Module imports
|
# Module imports
|
||||||
from .base import BaseSerializer, DynamicBaseSerializer
|
from .base import BaseSerializer, DynamicBaseSerializer
|
||||||
from plane.app.serializers.workspace import WorkspaceLiteSerializer
|
from plane.app.serializers.workspace import WorkspaceLiteSerializer
|
||||||
from plane.app.serializers.user import (
|
from plane.app.serializers.user import UserLiteSerializer, UserAdminLiteSerializer
|
||||||
UserLiteSerializer,
|
|
||||||
UserAdminLiteSerializer,
|
|
||||||
)
|
|
||||||
from plane.db.models import (
|
from plane.db.models import (
|
||||||
Project,
|
Project,
|
||||||
ProjectMember,
|
ProjectMember,
|
||||||
@@ -20,9 +17,7 @@ from plane.db.models import (
|
|||||||
|
|
||||||
|
|
||||||
class ProjectSerializer(BaseSerializer):
|
class ProjectSerializer(BaseSerializer):
|
||||||
workspace_detail = WorkspaceLiteSerializer(
|
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
|
||||||
source="workspace", read_only=True
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Project
|
model = Project
|
||||||
@@ -34,16 +29,12 @@ class ProjectSerializer(BaseSerializer):
|
|||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
identifier = validated_data.get("identifier", "").strip().upper()
|
identifier = validated_data.get("identifier", "").strip().upper()
|
||||||
if identifier == "":
|
if identifier == "":
|
||||||
raise serializers.ValidationError(
|
raise serializers.ValidationError(detail="Project Identifier is required")
|
||||||
detail="Project Identifier is required"
|
|
||||||
)
|
|
||||||
|
|
||||||
if ProjectIdentifier.objects.filter(
|
if ProjectIdentifier.objects.filter(
|
||||||
name=identifier, workspace_id=self.context["workspace_id"]
|
name=identifier, workspace_id=self.context["workspace_id"]
|
||||||
).exists():
|
).exists():
|
||||||
raise serializers.ValidationError(
|
raise serializers.ValidationError(detail="Project Identifier is taken")
|
||||||
detail="Project Identifier is taken"
|
|
||||||
)
|
|
||||||
project = Project.objects.create(
|
project = Project.objects.create(
|
||||||
**validated_data, workspace_id=self.context["workspace_id"]
|
**validated_data, workspace_id=self.context["workspace_id"]
|
||||||
)
|
)
|
||||||
@@ -82,9 +73,7 @@ class ProjectSerializer(BaseSerializer):
|
|||||||
return project
|
return project
|
||||||
|
|
||||||
# If not same fail update
|
# If not same fail update
|
||||||
raise serializers.ValidationError(
|
raise serializers.ValidationError(detail="Project Identifier is already taken")
|
||||||
detail="Project Identifier is already taken"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ProjectLiteSerializer(BaseSerializer):
|
class ProjectLiteSerializer(BaseSerializer):
|
||||||
@@ -114,19 +103,16 @@ class ProjectListSerializer(DynamicBaseSerializer):
|
|||||||
members = serializers.SerializerMethodField()
|
members = serializers.SerializerMethodField()
|
||||||
|
|
||||||
def get_members(self, obj):
|
def get_members(self, obj):
|
||||||
project_members = getattr(obj, "members_list", None)
|
project_members = ProjectMember.objects.filter(
|
||||||
if project_members is not None:
|
project_id=obj.id,
|
||||||
# Filter members by the project ID
|
is_active=True,
|
||||||
return [
|
).values(
|
||||||
{
|
"id",
|
||||||
"id": member.id,
|
"member_id",
|
||||||
"member_id": member.member_id,
|
"member__display_name",
|
||||||
"member__display_name": member.member.display_name,
|
"member__avatar",
|
||||||
"member__avatar": member.member.avatar,
|
)
|
||||||
}
|
return list(project_members)
|
||||||
for member in project_members
|
|
||||||
]
|
|
||||||
return []
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Project
|
model = Project
|
||||||
@@ -171,12 +157,6 @@ class ProjectMemberAdminSerializer(BaseSerializer):
|
|||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
class ProjectMemberRoleSerializer(DynamicBaseSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = ProjectMember
|
|
||||||
fields = ("id", "role", "member", "project")
|
|
||||||
|
|
||||||
|
|
||||||
class ProjectMemberInviteSerializer(BaseSerializer):
|
class ProjectMemberInviteSerializer(BaseSerializer):
|
||||||
project = ProjectLiteSerializer(read_only=True)
|
project = ProjectLiteSerializer(read_only=True)
|
||||||
workspace = WorkspaceLiteSerializer(read_only=True)
|
workspace = WorkspaceLiteSerializer(read_only=True)
|
||||||
@@ -214,9 +194,7 @@ class ProjectMemberLiteSerializer(BaseSerializer):
|
|||||||
|
|
||||||
class ProjectDeployBoardSerializer(BaseSerializer):
|
class ProjectDeployBoardSerializer(BaseSerializer):
|
||||||
project_details = ProjectLiteSerializer(read_only=True, source="project")
|
project_details = ProjectLiteSerializer(read_only=True, source="project")
|
||||||
workspace_detail = WorkspaceLiteSerializer(
|
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
|
||||||
read_only=True, source="workspace"
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ProjectDeployBoard
|
model = ProjectDeployBoard
|
||||||
@@ -236,4 +214,4 @@ class ProjectPublicMemberSerializer(BaseSerializer):
|
|||||||
"workspace",
|
"workspace",
|
||||||
"project",
|
"project",
|
||||||
"member",
|
"member",
|
||||||
]
|
]
|
||||||
@@ -6,19 +6,10 @@ from plane.db.models import State
|
|||||||
|
|
||||||
|
|
||||||
class StateSerializer(BaseSerializer):
|
class StateSerializer(BaseSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = State
|
model = State
|
||||||
fields = [
|
fields = "__all__"
|
||||||
"id",
|
|
||||||
"project_id",
|
|
||||||
"workspace_id",
|
|
||||||
"name",
|
|
||||||
"color",
|
|
||||||
"group",
|
|
||||||
"default",
|
|
||||||
"description",
|
|
||||||
"sequence",
|
|
||||||
]
|
|
||||||
read_only_fields = [
|
read_only_fields = [
|
||||||
"workspace",
|
"workspace",
|
||||||
"project",
|
"project",
|
||||||
@@ -34,4 +25,4 @@ class StateLiteSerializer(BaseSerializer):
|
|||||||
"color",
|
"color",
|
||||||
"group",
|
"group",
|
||||||
]
|
]
|
||||||
read_only_fields = fields
|
read_only_fields = fields
|
||||||
@@ -26,8 +26,6 @@ class UserSerializer(BaseSerializer):
|
|||||||
"token_updated_at",
|
"token_updated_at",
|
||||||
"is_onboarded",
|
"is_onboarded",
|
||||||
"is_bot",
|
"is_bot",
|
||||||
"is_password_autoset",
|
|
||||||
"is_email_verified",
|
|
||||||
]
|
]
|
||||||
extra_kwargs = {"password": {"write_only": True}}
|
extra_kwargs = {"password": {"write_only": True}}
|
||||||
|
|
||||||
@@ -62,8 +60,6 @@ class UserMeSerializer(BaseSerializer):
|
|||||||
"theme",
|
"theme",
|
||||||
"last_workspace_id",
|
"last_workspace_id",
|
||||||
"use_case",
|
"use_case",
|
||||||
"is_password_autoset",
|
|
||||||
"is_email_verified",
|
|
||||||
]
|
]
|
||||||
read_only_fields = fields
|
read_only_fields = fields
|
||||||
|
|
||||||
@@ -84,24 +80,13 @@ class UserMeSettingsSerializer(BaseSerializer):
|
|||||||
workspace_invites = WorkspaceMemberInvite.objects.filter(
|
workspace_invites = WorkspaceMemberInvite.objects.filter(
|
||||||
email=obj.email
|
email=obj.email
|
||||||
).count()
|
).count()
|
||||||
if (
|
if obj.last_workspace_id is not None:
|
||||||
obj.last_workspace_id is not None
|
|
||||||
and Workspace.objects.filter(
|
|
||||||
pk=obj.last_workspace_id,
|
|
||||||
workspace_member__member=obj.id,
|
|
||||||
workspace_member__is_active=True,
|
|
||||||
).exists()
|
|
||||||
):
|
|
||||||
workspace = Workspace.objects.filter(
|
workspace = Workspace.objects.filter(
|
||||||
pk=obj.last_workspace_id,
|
pk=obj.last_workspace_id, workspace_member__member=obj.id
|
||||||
workspace_member__member=obj.id,
|
|
||||||
workspace_member__is_active=True,
|
|
||||||
).first()
|
).first()
|
||||||
return {
|
return {
|
||||||
"last_workspace_id": obj.last_workspace_id,
|
"last_workspace_id": obj.last_workspace_id,
|
||||||
"last_workspace_slug": workspace.slug
|
"last_workspace_slug": workspace.slug if workspace is not None else "",
|
||||||
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
|
"fallback_workspace_slug": workspace.slug
|
||||||
if workspace is not None
|
if workspace is not None
|
||||||
@@ -110,10 +95,7 @@ class UserMeSettingsSerializer(BaseSerializer):
|
|||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
fallback_workspace = (
|
fallback_workspace = (
|
||||||
Workspace.objects.filter(
|
Workspace.objects.filter(workspace_member__member_id=obj.id)
|
||||||
workspace_member__member_id=obj.id,
|
|
||||||
workspace_member__is_active=True,
|
|
||||||
)
|
|
||||||
.order_by("created_at")
|
.order_by("created_at")
|
||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
@@ -172,28 +154,14 @@ class ChangePasswordSerializer(serializers.Serializer):
|
|||||||
Serializer for password change endpoint.
|
Serializer for password change endpoint.
|
||||||
"""
|
"""
|
||||||
old_password = serializers.CharField(required=True)
|
old_password = serializers.CharField(required=True)
|
||||||
new_password = serializers.CharField(required=True, min_length=8)
|
new_password = serializers.CharField(required=True)
|
||||||
confirm_password = serializers.CharField(required=True, min_length=8)
|
|
||||||
|
|
||||||
def validate(self, data):
|
|
||||||
if data.get("old_password") == data.get("new_password"):
|
|
||||||
raise serializers.ValidationError(
|
|
||||||
{"error": "New password cannot be same as old password."}
|
|
||||||
)
|
|
||||||
|
|
||||||
if data.get("new_password") != data.get("confirm_password"):
|
|
||||||
raise serializers.ValidationError(
|
|
||||||
{
|
|
||||||
"error": "Confirm password should be same as the new password."
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
class ResetPasswordSerializer(serializers.Serializer):
|
class ResetPasswordSerializer(serializers.Serializer):
|
||||||
|
model = User
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Serializer for password change endpoint.
|
Serializer for password change endpoint.
|
||||||
"""
|
"""
|
||||||
|
new_password = serializers.CharField(required=True)
|
||||||
new_password = serializers.CharField(required=True, min_length=8)
|
confirm_password = serializers.CharField(required=True)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from .base import BaseSerializer, DynamicBaseSerializer
|
from .base import BaseSerializer
|
||||||
from .workspace import WorkspaceLiteSerializer
|
from .workspace import WorkspaceLiteSerializer
|
||||||
from .project import ProjectLiteSerializer
|
from .project import ProjectLiteSerializer
|
||||||
from plane.db.models import GlobalView, IssueView, IssueViewFavorite
|
from plane.db.models import GlobalView, IssueView, IssueViewFavorite
|
||||||
@@ -10,9 +10,7 @@ from plane.utils.issue_filters import issue_filters
|
|||||||
|
|
||||||
|
|
||||||
class GlobalViewSerializer(BaseSerializer):
|
class GlobalViewSerializer(BaseSerializer):
|
||||||
workspace_detail = WorkspaceLiteSerializer(
|
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
|
||||||
source="workspace", read_only=True
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = GlobalView
|
model = GlobalView
|
||||||
@@ -40,12 +38,10 @@ class GlobalViewSerializer(BaseSerializer):
|
|||||||
return super().update(instance, validated_data)
|
return super().update(instance, validated_data)
|
||||||
|
|
||||||
|
|
||||||
class IssueViewSerializer(DynamicBaseSerializer):
|
class IssueViewSerializer(BaseSerializer):
|
||||||
is_favorite = serializers.BooleanField(read_only=True)
|
is_favorite = serializers.BooleanField(read_only=True)
|
||||||
project_detail = ProjectLiteSerializer(source="project", read_only=True)
|
project_detail = ProjectLiteSerializer(source="project", read_only=True)
|
||||||
workspace_detail = WorkspaceLiteSerializer(
|
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
|
||||||
source="workspace", read_only=True
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IssueView
|
model = IssueView
|
||||||
|
|||||||
@@ -1,125 +1,14 @@
|
|||||||
# Python imports
|
|
||||||
import urllib
|
|
||||||
import socket
|
|
||||||
import ipaddress
|
|
||||||
from urllib.parse import urlparse
|
|
||||||
|
|
||||||
# Third party imports
|
# Third party imports
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from .base import DynamicBaseSerializer
|
from .base import DynamicBaseSerializer
|
||||||
from plane.db.models import Webhook, WebhookLog
|
from plane.db.models import Webhook, WebhookLog
|
||||||
from plane.db.models.webhook import validate_domain, validate_schema
|
from plane.db.models.webhook import validate_domain, validate_schema
|
||||||
|
|
||||||
|
|
||||||
class WebhookSerializer(DynamicBaseSerializer):
|
class WebhookSerializer(DynamicBaseSerializer):
|
||||||
url = serializers.URLField(validators=[validate_schema, validate_domain])
|
url = serializers.URLField(validators=[validate_schema, validate_domain])
|
||||||
|
|
||||||
def create(self, validated_data):
|
|
||||||
url = validated_data.get("url", None)
|
|
||||||
|
|
||||||
# Extract the hostname from the URL
|
|
||||||
hostname = urlparse(url).hostname
|
|
||||||
if not hostname:
|
|
||||||
raise serializers.ValidationError(
|
|
||||||
{"url": "Invalid URL: No hostname found."}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Resolve the hostname to IP addresses
|
|
||||||
try:
|
|
||||||
ip_addresses = socket.getaddrinfo(hostname, None)
|
|
||||||
except socket.gaierror:
|
|
||||||
raise serializers.ValidationError(
|
|
||||||
{"url": "Hostname could not be resolved."}
|
|
||||||
)
|
|
||||||
|
|
||||||
if not ip_addresses:
|
|
||||||
raise serializers.ValidationError(
|
|
||||||
{"url": "No IP addresses found for the hostname."}
|
|
||||||
)
|
|
||||||
|
|
||||||
for addr in ip_addresses:
|
|
||||||
ip = ipaddress.ip_address(addr[4][0])
|
|
||||||
if ip.is_private or ip.is_loopback:
|
|
||||||
raise serializers.ValidationError(
|
|
||||||
{"url": "URL resolves to a blocked IP address."}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Additional validation for multiple request domains and their subdomains
|
|
||||||
request = self.context.get("request")
|
|
||||||
disallowed_domains = [
|
|
||||||
"plane.so",
|
|
||||||
] # Add your disallowed domains here
|
|
||||||
if request:
|
|
||||||
request_host = request.get_host().split(":")[
|
|
||||||
0
|
|
||||||
] # Remove port if present
|
|
||||||
disallowed_domains.append(request_host)
|
|
||||||
|
|
||||||
# Check if hostname is a subdomain or exact match of any disallowed domain
|
|
||||||
if any(
|
|
||||||
hostname == domain or hostname.endswith("." + domain)
|
|
||||||
for domain in disallowed_domains
|
|
||||||
):
|
|
||||||
raise serializers.ValidationError(
|
|
||||||
{"url": "URL domain or its subdomain is not allowed."}
|
|
||||||
)
|
|
||||||
|
|
||||||
return Webhook.objects.create(**validated_data)
|
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
|
||||||
url = validated_data.get("url", None)
|
|
||||||
if url:
|
|
||||||
# Extract the hostname from the URL
|
|
||||||
hostname = urlparse(url).hostname
|
|
||||||
if not hostname:
|
|
||||||
raise serializers.ValidationError(
|
|
||||||
{"url": "Invalid URL: No hostname found."}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Resolve the hostname to IP addresses
|
|
||||||
try:
|
|
||||||
ip_addresses = socket.getaddrinfo(hostname, None)
|
|
||||||
except socket.gaierror:
|
|
||||||
raise serializers.ValidationError(
|
|
||||||
{"url": "Hostname could not be resolved."}
|
|
||||||
)
|
|
||||||
|
|
||||||
if not ip_addresses:
|
|
||||||
raise serializers.ValidationError(
|
|
||||||
{"url": "No IP addresses found for the hostname."}
|
|
||||||
)
|
|
||||||
|
|
||||||
for addr in ip_addresses:
|
|
||||||
ip = ipaddress.ip_address(addr[4][0])
|
|
||||||
if ip.is_private or ip.is_loopback:
|
|
||||||
raise serializers.ValidationError(
|
|
||||||
{"url": "URL resolves to a blocked IP address."}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Additional validation for multiple request domains and their subdomains
|
|
||||||
request = self.context.get("request")
|
|
||||||
disallowed_domains = [
|
|
||||||
"plane.so",
|
|
||||||
] # Add your disallowed domains here
|
|
||||||
if request:
|
|
||||||
request_host = request.get_host().split(":")[
|
|
||||||
0
|
|
||||||
] # Remove port if present
|
|
||||||
disallowed_domains.append(request_host)
|
|
||||||
|
|
||||||
# Check if hostname is a subdomain or exact match of any disallowed domain
|
|
||||||
if any(
|
|
||||||
hostname == domain or hostname.endswith("." + domain)
|
|
||||||
for domain in disallowed_domains
|
|
||||||
):
|
|
||||||
raise serializers.ValidationError(
|
|
||||||
{"url": "URL domain or its subdomain is not allowed."}
|
|
||||||
)
|
|
||||||
|
|
||||||
return super().update(instance, validated_data)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Webhook
|
model = Webhook
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
@@ -130,7 +19,12 @@ class WebhookSerializer(DynamicBaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class WebhookLogSerializer(DynamicBaseSerializer):
|
class WebhookLogSerializer(DynamicBaseSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = WebhookLog
|
model = WebhookLog
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
read_only_fields = ["workspace", "webhook"]
|
read_only_fields = [
|
||||||
|
"workspace",
|
||||||
|
"webhook"
|
||||||
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from .base import BaseSerializer, DynamicBaseSerializer
|
from .base import BaseSerializer
|
||||||
from .user import UserLiteSerializer, UserAdminLiteSerializer
|
from .user import UserLiteSerializer, UserAdminLiteSerializer
|
||||||
|
|
||||||
from plane.db.models import (
|
from plane.db.models import (
|
||||||
@@ -13,32 +13,14 @@ from plane.db.models import (
|
|||||||
TeamMember,
|
TeamMember,
|
||||||
WorkspaceMemberInvite,
|
WorkspaceMemberInvite,
|
||||||
WorkspaceTheme,
|
WorkspaceTheme,
|
||||||
WorkspaceUserProperties,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class WorkSpaceSerializer(DynamicBaseSerializer):
|
class WorkSpaceSerializer(BaseSerializer):
|
||||||
owner = UserLiteSerializer(read_only=True)
|
owner = UserLiteSerializer(read_only=True)
|
||||||
total_members = serializers.IntegerField(read_only=True)
|
total_members = serializers.IntegerField(read_only=True)
|
||||||
total_issues = serializers.IntegerField(read_only=True)
|
total_issues = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
def validated(self, data):
|
|
||||||
if data.get("slug") in [
|
|
||||||
"404",
|
|
||||||
"accounts",
|
|
||||||
"api",
|
|
||||||
"create-workspace",
|
|
||||||
"god-mode",
|
|
||||||
"installations",
|
|
||||||
"invitations",
|
|
||||||
"onboarding",
|
|
||||||
"profile",
|
|
||||||
"spaces",
|
|
||||||
"workspace-invitations",
|
|
||||||
"password",
|
|
||||||
]:
|
|
||||||
raise serializers.ValidationError({"slug": "Slug is not valid"})
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Workspace
|
model = Workspace
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
@@ -51,7 +33,6 @@ class WorkSpaceSerializer(DynamicBaseSerializer):
|
|||||||
"owner",
|
"owner",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class WorkspaceLiteSerializer(BaseSerializer):
|
class WorkspaceLiteSerializer(BaseSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Workspace
|
model = Workspace
|
||||||
@@ -63,7 +44,8 @@ class WorkspaceLiteSerializer(BaseSerializer):
|
|||||||
read_only_fields = fields
|
read_only_fields = fields
|
||||||
|
|
||||||
|
|
||||||
class WorkSpaceMemberSerializer(DynamicBaseSerializer):
|
|
||||||
|
class WorkSpaceMemberSerializer(BaseSerializer):
|
||||||
member = UserLiteSerializer(read_only=True)
|
member = UserLiteSerializer(read_only=True)
|
||||||
workspace = WorkspaceLiteSerializer(read_only=True)
|
workspace = WorkspaceLiteSerializer(read_only=True)
|
||||||
|
|
||||||
@@ -73,12 +55,13 @@ class WorkSpaceMemberSerializer(DynamicBaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class WorkspaceMemberMeSerializer(BaseSerializer):
|
class WorkspaceMemberMeSerializer(BaseSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = WorkspaceMember
|
model = WorkspaceMember
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
class WorkspaceMemberAdminSerializer(DynamicBaseSerializer):
|
class WorkspaceMemberAdminSerializer(BaseSerializer):
|
||||||
member = UserAdminLiteSerializer(read_only=True)
|
member = UserAdminLiteSerializer(read_only=True)
|
||||||
workspace = WorkspaceLiteSerializer(read_only=True)
|
workspace = WorkspaceLiteSerializer(read_only=True)
|
||||||
|
|
||||||
@@ -95,22 +78,10 @@ class WorkSpaceMemberInviteSerializer(BaseSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = WorkspaceMemberInvite
|
model = WorkspaceMemberInvite
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
read_only_fields = [
|
|
||||||
"id",
|
|
||||||
"email",
|
|
||||||
"token",
|
|
||||||
"workspace",
|
|
||||||
"message",
|
|
||||||
"responded_at",
|
|
||||||
"created_at",
|
|
||||||
"updated_at",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class TeamSerializer(BaseSerializer):
|
class TeamSerializer(BaseSerializer):
|
||||||
members_detail = UserLiteSerializer(
|
members_detail = UserLiteSerializer(read_only=True, source="members", many=True)
|
||||||
read_only=True, source="members", many=True
|
|
||||||
)
|
|
||||||
members = serializers.ListField(
|
members = serializers.ListField(
|
||||||
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
|
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
|
||||||
write_only=True,
|
write_only=True,
|
||||||
@@ -147,9 +118,7 @@ class TeamSerializer(BaseSerializer):
|
|||||||
members = validated_data.pop("members")
|
members = validated_data.pop("members")
|
||||||
TeamMember.objects.filter(team=instance).delete()
|
TeamMember.objects.filter(team=instance).delete()
|
||||||
team_members = [
|
team_members = [
|
||||||
TeamMember(
|
TeamMember(member=member, team=instance, workspace=instance.workspace)
|
||||||
member=member, team=instance, workspace=instance.workspace
|
|
||||||
)
|
|
||||||
for member in members
|
for member in members
|
||||||
]
|
]
|
||||||
TeamMember.objects.bulk_create(team_members, batch_size=10)
|
TeamMember.objects.bulk_create(team_members, batch_size=10)
|
||||||
@@ -165,13 +134,3 @@ class WorkspaceThemeSerializer(BaseSerializer):
|
|||||||
"workspace",
|
"workspace",
|
||||||
"actor",
|
"actor",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class WorkspaceUserPropertiesSerializer(BaseSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = WorkspaceUserProperties
|
|
||||||
fields = "__all__"
|
|
||||||
read_only_fields = [
|
|
||||||
"workspace",
|
|
||||||
"user",
|
|
||||||
]
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ from .asset import urlpatterns as asset_urls
|
|||||||
from .authentication import urlpatterns as authentication_urls
|
from .authentication import urlpatterns as authentication_urls
|
||||||
from .config import urlpatterns as configuration_urls
|
from .config import urlpatterns as configuration_urls
|
||||||
from .cycle import urlpatterns as cycle_urls
|
from .cycle import urlpatterns as cycle_urls
|
||||||
from .dashboard import urlpatterns as dashboard_urls
|
|
||||||
from .estimate import urlpatterns as estimate_urls
|
from .estimate import urlpatterns as estimate_urls
|
||||||
from .external import urlpatterns as external_urls
|
from .external import urlpatterns as external_urls
|
||||||
from .importer import urlpatterns as importer_urls
|
from .importer import urlpatterns as importer_urls
|
||||||
@@ -29,7 +28,6 @@ urlpatterns = [
|
|||||||
*authentication_urls,
|
*authentication_urls,
|
||||||
*configuration_urls,
|
*configuration_urls,
|
||||||
*cycle_urls,
|
*cycle_urls,
|
||||||
*dashboard_urls,
|
|
||||||
*estimate_urls,
|
*estimate_urls,
|
||||||
*external_urls,
|
*external_urls,
|
||||||
*importer_urls,
|
*importer_urls,
|
||||||
@@ -47,4 +45,4 @@ urlpatterns = [
|
|||||||
*workspace_urls,
|
*workspace_urls,
|
||||||
*api_urls,
|
*api_urls,
|
||||||
*webhook_urls,
|
*webhook_urls,
|
||||||
]
|
]
|
||||||
@@ -4,7 +4,6 @@ from django.urls import path
|
|||||||
from plane.app.views import (
|
from plane.app.views import (
|
||||||
FileAssetEndpoint,
|
FileAssetEndpoint,
|
||||||
UserAssetsEndpoint,
|
UserAssetsEndpoint,
|
||||||
FileAssetViewSet,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -29,13 +28,4 @@ urlpatterns = [
|
|||||||
UserAssetsEndpoint.as_view(),
|
UserAssetsEndpoint.as_view(),
|
||||||
name="user-file-assets",
|
name="user-file-assets",
|
||||||
),
|
),
|
||||||
path(
|
|
||||||
"workspaces/file-assets/<uuid:workspace_id>/<str:asset_key>/restore/",
|
|
||||||
FileAssetViewSet.as_view(
|
|
||||||
{
|
|
||||||
"post": "restore",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
name="file-assets-restore",
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -5,16 +5,18 @@ from rest_framework_simplejwt.views import TokenRefreshView
|
|||||||
|
|
||||||
from plane.app.views import (
|
from plane.app.views import (
|
||||||
# Authentication
|
# Authentication
|
||||||
|
SignUpEndpoint,
|
||||||
SignInEndpoint,
|
SignInEndpoint,
|
||||||
SignOutEndpoint,
|
SignOutEndpoint,
|
||||||
MagicGenerateEndpoint,
|
|
||||||
MagicSignInEndpoint,
|
MagicSignInEndpoint,
|
||||||
|
MagicSignInGenerateEndpoint,
|
||||||
OauthEndpoint,
|
OauthEndpoint,
|
||||||
EmailCheckEndpoint,
|
|
||||||
## End Authentication
|
## End Authentication
|
||||||
# Auth Extended
|
# Auth Extended
|
||||||
ForgotPasswordEndpoint,
|
ForgotPasswordEndpoint,
|
||||||
|
VerifyEmailEndpoint,
|
||||||
ResetPasswordEndpoint,
|
ResetPasswordEndpoint,
|
||||||
|
RequestEmailVerificationEndpoint,
|
||||||
ChangePasswordEndpoint,
|
ChangePasswordEndpoint,
|
||||||
## End Auth Extender
|
## End Auth Extender
|
||||||
# API Tokens
|
# API Tokens
|
||||||
@@ -25,21 +27,24 @@ from plane.app.views import (
|
|||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# Social Auth
|
# Social Auth
|
||||||
path("email-check/", EmailCheckEndpoint.as_view(), name="email"),
|
|
||||||
path("social-auth/", OauthEndpoint.as_view(), name="oauth"),
|
path("social-auth/", OauthEndpoint.as_view(), name="oauth"),
|
||||||
# Auth
|
# Auth
|
||||||
|
path("sign-up/", SignUpEndpoint.as_view(), name="sign-up"),
|
||||||
path("sign-in/", SignInEndpoint.as_view(), name="sign-in"),
|
path("sign-in/", SignInEndpoint.as_view(), name="sign-in"),
|
||||||
path("sign-out/", SignOutEndpoint.as_view(), name="sign-out"),
|
path("sign-out/", SignOutEndpoint.as_view(), name="sign-out"),
|
||||||
# magic sign in
|
# Magic Sign In/Up
|
||||||
path(
|
path(
|
||||||
"magic-generate/",
|
"magic-generate/", MagicSignInGenerateEndpoint.as_view(), name="magic-generate"
|
||||||
MagicGenerateEndpoint.as_view(),
|
|
||||||
name="magic-generate",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"magic-sign-in/", MagicSignInEndpoint.as_view(), name="magic-sign-in"
|
|
||||||
),
|
),
|
||||||
|
path("magic-sign-in/", MagicSignInEndpoint.as_view(), name="magic-sign-in"),
|
||||||
path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
|
path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
|
||||||
|
# Email verification
|
||||||
|
path("email-verify/", VerifyEmailEndpoint.as_view(), name="email-verify"),
|
||||||
|
path(
|
||||||
|
"request-email-verify/",
|
||||||
|
RequestEmailVerificationEndpoint.as_view(),
|
||||||
|
name="request-reset-email",
|
||||||
|
),
|
||||||
# Password Manipulation
|
# Password Manipulation
|
||||||
path(
|
path(
|
||||||
"users/me/change-password/",
|
"users/me/change-password/",
|
||||||
@@ -58,8 +63,6 @@ urlpatterns = [
|
|||||||
),
|
),
|
||||||
# API Tokens
|
# API Tokens
|
||||||
path("api-tokens/", ApiTokenEndpoint.as_view(), name="api-tokens"),
|
path("api-tokens/", ApiTokenEndpoint.as_view(), name="api-tokens"),
|
||||||
path(
|
path("api-tokens/<uuid:pk>/", ApiTokenEndpoint.as_view(), name="api-tokens"),
|
||||||
"api-tokens/<uuid:pk>/", ApiTokenEndpoint.as_view(), name="api-tokens"
|
|
||||||
),
|
|
||||||
## End API Tokens
|
## End API Tokens
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
|
|
||||||
from plane.app.views import ConfigurationEndpoint, MobileConfigurationEndpoint
|
from plane.app.views import ConfigurationEndpoint
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path(
|
path(
|
||||||
@@ -9,9 +9,4 @@ urlpatterns = [
|
|||||||
ConfigurationEndpoint.as_view(),
|
ConfigurationEndpoint.as_view(),
|
||||||
name="configuration",
|
name="configuration",
|
||||||
),
|
),
|
||||||
path(
|
]
|
||||||
"mobile-configs/",
|
|
||||||
MobileConfigurationEndpoint.as_view(),
|
|
||||||
name="configuration",
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -7,7 +7,7 @@ from plane.app.views import (
|
|||||||
CycleDateCheckEndpoint,
|
CycleDateCheckEndpoint,
|
||||||
CycleFavoriteViewSet,
|
CycleFavoriteViewSet,
|
||||||
TransferCycleIssueEndpoint,
|
TransferCycleIssueEndpoint,
|
||||||
CycleUserPropertiesEndpoint,
|
CycleIssueGroupedEndpoint,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -45,7 +45,12 @@ urlpatterns = [
|
|||||||
name="project-issue-cycle",
|
name="project-issue-cycle",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/cycle-issues/<uuid:issue_id>/",
|
"v3/workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/cycle-issues/",
|
||||||
|
CycleIssueGroupedEndpoint.as_view(),
|
||||||
|
name="project-issue-cycle",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/cycle-issues/<uuid:pk>/",
|
||||||
CycleIssueViewSet.as_view(
|
CycleIssueViewSet.as_view(
|
||||||
{
|
{
|
||||||
"get": "retrieve",
|
"get": "retrieve",
|
||||||
@@ -85,9 +90,4 @@ urlpatterns = [
|
|||||||
TransferCycleIssueEndpoint.as_view(),
|
TransferCycleIssueEndpoint.as_view(),
|
||||||
name="transfer-issues",
|
name="transfer-issues",
|
||||||
),
|
),
|
||||||
path(
|
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/user-properties/",
|
|
||||||
CycleUserPropertiesEndpoint.as_view(),
|
|
||||||
name="cycle-user-filters",
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
from django.urls import path
|
|
||||||
|
|
||||||
|
|
||||||
from plane.app.views import DashboardEndpoint, WidgetsEndpoint
|
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
path(
|
|
||||||
"workspaces/<str:slug>/dashboard/",
|
|
||||||
DashboardEndpoint.as_view(),
|
|
||||||
name="dashboard",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"workspaces/<str:slug>/dashboard/<uuid:dashboard_id>/",
|
|
||||||
DashboardEndpoint.as_view(),
|
|
||||||
name="dashboard",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"dashboard/<uuid:dashboard_id>/widgets/<uuid:widget_id>/",
|
|
||||||
WidgetsEndpoint.as_view(),
|
|
||||||
name="widgets",
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -40,7 +40,7 @@ urlpatterns = [
|
|||||||
name="inbox-issue",
|
name="inbox-issue",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/<uuid:inbox_id>/inbox-issues/<uuid:issue_id>/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/<uuid:inbox_id>/inbox-issues/<uuid:pk>/",
|
||||||
InboxIssueViewSet.as_view(
|
InboxIssueViewSet.as_view(
|
||||||
{
|
{
|
||||||
"get": "retrieve",
|
"get": "retrieve",
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ from django.urls import path
|
|||||||
|
|
||||||
from plane.app.views import (
|
from plane.app.views import (
|
||||||
IssueViewSet,
|
IssueViewSet,
|
||||||
|
IssueListEndpoint,
|
||||||
|
IssueListGroupedEndpoint,
|
||||||
LabelViewSet,
|
LabelViewSet,
|
||||||
BulkCreateIssueLabelsEndpoint,
|
BulkCreateIssueLabelsEndpoint,
|
||||||
BulkDeleteIssuesEndpoint,
|
BulkDeleteIssuesEndpoint,
|
||||||
@@ -35,6 +37,16 @@ urlpatterns = [
|
|||||||
),
|
),
|
||||||
name="project-issue",
|
name="project-issue",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"v2/workspaces/<str:slug>/projects/<uuid:project_id>/issues/",
|
||||||
|
IssueListEndpoint.as_view(),
|
||||||
|
name="project-issue",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"v3/workspaces/<str:slug>/projects/<uuid:project_id>/issues/",
|
||||||
|
IssueListGroupedEndpoint.as_view(),
|
||||||
|
name="project-issue",
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:pk>/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:pk>/",
|
||||||
IssueViewSet.as_view(
|
IssueViewSet.as_view(
|
||||||
@@ -235,7 +247,7 @@ urlpatterns = [
|
|||||||
## End Comment Reactions
|
## End Comment Reactions
|
||||||
## IssueProperty
|
## IssueProperty
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/user-properties/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-display-properties/",
|
||||||
IssueUserDisplayPropertyEndpoint.as_view(),
|
IssueUserDisplayPropertyEndpoint.as_view(),
|
||||||
name="project-issue-display-properties",
|
name="project-issue-display-properties",
|
||||||
),
|
),
|
||||||
@@ -275,17 +287,16 @@ urlpatterns = [
|
|||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-relation/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-relation/",
|
||||||
IssueRelationViewSet.as_view(
|
IssueRelationViewSet.as_view(
|
||||||
{
|
{
|
||||||
"get": "list",
|
|
||||||
"post": "create",
|
"post": "create",
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
name="issue-relation",
|
name="issue-relation",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/remove-relation/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-relation/<uuid:pk>/",
|
||||||
IssueRelationViewSet.as_view(
|
IssueRelationViewSet.as_view(
|
||||||
{
|
{
|
||||||
"post": "remove_relation",
|
"delete": "destroy",
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
name="issue-relation",
|
name="issue-relation",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from plane.app.views import (
|
|||||||
ModuleLinkViewSet,
|
ModuleLinkViewSet,
|
||||||
ModuleFavoriteViewSet,
|
ModuleFavoriteViewSet,
|
||||||
BulkImportModulesEndpoint,
|
BulkImportModulesEndpoint,
|
||||||
ModuleUserPropertiesEndpoint,
|
ModuleIssueGroupedEndpoint,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -35,26 +35,22 @@ urlpatterns = [
|
|||||||
name="project-modules",
|
name="project-modules",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/modules/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/module-issues/",
|
||||||
ModuleIssueViewSet.as_view(
|
ModuleIssueViewSet.as_view(
|
||||||
{
|
{
|
||||||
"post": "create_issue_modules",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
name="issue-module",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/issues/",
|
|
||||||
ModuleIssueViewSet.as_view(
|
|
||||||
{
|
|
||||||
"post": "create_module_issues",
|
|
||||||
"get": "list",
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
name="project-module-issues",
|
name="project-module-issues",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/issues/<uuid:issue_id>/",
|
"v3/workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/module-issues/",
|
||||||
|
ModuleIssueGroupedEndpoint.as_view(),
|
||||||
|
name="project-issue-cycle",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/module-issues/<uuid:pk>/",
|
||||||
ModuleIssueViewSet.as_view(
|
ModuleIssueViewSet.as_view(
|
||||||
{
|
{
|
||||||
"get": "retrieve",
|
"get": "retrieve",
|
||||||
@@ -111,9 +107,4 @@ urlpatterns = [
|
|||||||
BulkImportModulesEndpoint.as_view(),
|
BulkImportModulesEndpoint.as_view(),
|
||||||
name="bulk-modules-create",
|
name="bulk-modules-create",
|
||||||
),
|
),
|
||||||
path(
|
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/user-properties/",
|
|
||||||
ModuleUserPropertiesEndpoint.as_view(),
|
|
||||||
name="cycle-user-filters",
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ from plane.app.views import (
|
|||||||
NotificationViewSet,
|
NotificationViewSet,
|
||||||
UnreadNotificationEndpoint,
|
UnreadNotificationEndpoint,
|
||||||
MarkAllReadNotificationViewSet,
|
MarkAllReadNotificationViewSet,
|
||||||
UserNotificationPreferenceEndpoint,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -64,9 +63,4 @@ urlpatterns = [
|
|||||||
),
|
),
|
||||||
name="mark-all-read-notifications",
|
name="mark-all-read-notifications",
|
||||||
),
|
),
|
||||||
path(
|
|
||||||
"users/me/notification-preferences/",
|
|
||||||
UserNotificationPreferenceEndpoint.as_view(),
|
|
||||||
name="user-notification-preferences",
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ from plane.app.views import (
|
|||||||
UserProjectInvitationsViewset,
|
UserProjectInvitationsViewset,
|
||||||
ProjectPublicCoverImagesEndpoint,
|
ProjectPublicCoverImagesEndpoint,
|
||||||
ProjectDeployBoardViewSet,
|
ProjectDeployBoardViewSet,
|
||||||
UserProjectRolesEndpoint,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -66,7 +65,7 @@ urlpatterns = [
|
|||||||
name="project-member-invite",
|
name="project-member-invite",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"users/me/workspaces/<str:slug>/projects/invitations/",
|
"users/me/invitations/projects/",
|
||||||
UserProjectInvitationsViewset.as_view(
|
UserProjectInvitationsViewset.as_view(
|
||||||
{
|
{
|
||||||
"get": "list",
|
"get": "list",
|
||||||
@@ -76,12 +75,7 @@ urlpatterns = [
|
|||||||
name="user-project-invitations",
|
name="user-project-invitations",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"users/me/workspaces/<str:slug>/project-roles/",
|
"workspaces/<str:slug>/projects/join/",
|
||||||
UserProjectRolesEndpoint.as_view(),
|
|
||||||
name="user-project-roles",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/join/<uuid:pk>/",
|
|
||||||
ProjectJoinEndpoint.as_view(),
|
ProjectJoinEndpoint.as_view(),
|
||||||
name="project-join",
|
name="project-join",
|
||||||
),
|
),
|
||||||
@@ -175,4 +169,4 @@ urlpatterns = [
|
|||||||
),
|
),
|
||||||
name="project-deploy-board",
|
name="project-deploy-board",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
@@ -7,7 +7,6 @@ from plane.app.views import (
|
|||||||
UpdateUserTourCompletedEndpoint,
|
UpdateUserTourCompletedEndpoint,
|
||||||
UserActivityEndpoint,
|
UserActivityEndpoint,
|
||||||
ChangePasswordEndpoint,
|
ChangePasswordEndpoint,
|
||||||
SetUserPasswordEndpoint,
|
|
||||||
## End User
|
## End User
|
||||||
## Workspaces
|
## Workspaces
|
||||||
UserWorkSpacesEndpoint,
|
UserWorkSpacesEndpoint,
|
||||||
@@ -64,7 +63,7 @@ urlpatterns = [
|
|||||||
name="user-tour",
|
name="user-tour",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"users/me/activities/",
|
"users/workspaces/<str:slug>/activities/",
|
||||||
UserActivityEndpoint.as_view(),
|
UserActivityEndpoint.as_view(),
|
||||||
name="user-activities",
|
name="user-activities",
|
||||||
),
|
),
|
||||||
@@ -90,10 +89,5 @@ urlpatterns = [
|
|||||||
UserWorkspaceDashboardEndpoint.as_view(),
|
UserWorkspaceDashboardEndpoint.as_view(),
|
||||||
name="user-workspace-dashboard",
|
name="user-workspace-dashboard",
|
||||||
),
|
),
|
||||||
path(
|
|
||||||
"users/me/set-password/",
|
|
||||||
SetUserPasswordEndpoint.as_view(),
|
|
||||||
name="set-password",
|
|
||||||
),
|
|
||||||
## End User Graph
|
## End User Graph
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -18,10 +18,6 @@ from plane.app.views import (
|
|||||||
WorkspaceUserProfileEndpoint,
|
WorkspaceUserProfileEndpoint,
|
||||||
WorkspaceUserProfileIssuesEndpoint,
|
WorkspaceUserProfileIssuesEndpoint,
|
||||||
WorkspaceLabelsEndpoint,
|
WorkspaceLabelsEndpoint,
|
||||||
WorkspaceProjectMemberEndpoint,
|
|
||||||
WorkspaceUserPropertiesEndpoint,
|
|
||||||
WorkspaceStatesEndpoint,
|
|
||||||
WorkspaceEstimatesEndpoint,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -69,7 +65,6 @@ urlpatterns = [
|
|||||||
{
|
{
|
||||||
"delete": "destroy",
|
"delete": "destroy",
|
||||||
"get": "retrieve",
|
"get": "retrieve",
|
||||||
"patch": "partial_update",
|
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
name="workspace-invitations",
|
name="workspace-invitations",
|
||||||
@@ -96,11 +91,6 @@ urlpatterns = [
|
|||||||
WorkSpaceMemberViewSet.as_view({"get": "list"}),
|
WorkSpaceMemberViewSet.as_view({"get": "list"}),
|
||||||
name="workspace-member",
|
name="workspace-member",
|
||||||
),
|
),
|
||||||
path(
|
|
||||||
"workspaces/<str:slug>/project-members/",
|
|
||||||
WorkspaceProjectMemberEndpoint.as_view(),
|
|
||||||
name="workspace-member-roles",
|
|
||||||
),
|
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/members/<uuid:pk>/",
|
"workspaces/<str:slug>/members/<uuid:pk>/",
|
||||||
WorkSpaceMemberViewSet.as_view(
|
WorkSpaceMemberViewSet.as_view(
|
||||||
@@ -204,19 +194,4 @@ urlpatterns = [
|
|||||||
WorkspaceLabelsEndpoint.as_view(),
|
WorkspaceLabelsEndpoint.as_view(),
|
||||||
name="workspace-labels",
|
name="workspace-labels",
|
||||||
),
|
),
|
||||||
path(
|
|
||||||
"workspaces/<str:slug>/user-properties/",
|
|
||||||
WorkspaceUserPropertiesEndpoint.as_view(),
|
|
||||||
name="workspace-user-filters",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"workspaces/<str:slug>/states/",
|
|
||||||
WorkspaceStatesEndpoint.as_view(),
|
|
||||||
name="workspace-state",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"workspaces/<str:slug>/estimates/",
|
|
||||||
WorkspaceEstimatesEndpoint.as_view(),
|
|
||||||
name="workspace-estimate",
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -192,7 +192,7 @@ from plane.app.views import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# TODO: Delete this file
|
#TODO: Delete this file
|
||||||
# This url file has been deprecated use apiserver/plane/urls folder to create new urls
|
# This url file has been deprecated use apiserver/plane/urls folder to create new urls
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
@@ -204,14 +204,10 @@ urlpatterns = [
|
|||||||
path("sign-out/", SignOutEndpoint.as_view(), name="sign-out"),
|
path("sign-out/", SignOutEndpoint.as_view(), name="sign-out"),
|
||||||
# Magic Sign In/Up
|
# Magic Sign In/Up
|
||||||
path(
|
path(
|
||||||
"magic-generate/",
|
"magic-generate/", MagicSignInGenerateEndpoint.as_view(), name="magic-generate"
|
||||||
MagicSignInGenerateEndpoint.as_view(),
|
|
||||||
name="magic-generate",
|
|
||||||
),
|
),
|
||||||
path(
|
path("magic-sign-in/", MagicSignInEndpoint.as_view(), name="magic-sign-in"),
|
||||||
"magic-sign-in/", MagicSignInEndpoint.as_view(), name="magic-sign-in"
|
path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
|
||||||
),
|
|
||||||
path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
|
|
||||||
# Email verification
|
# Email verification
|
||||||
path("email-verify/", VerifyEmailEndpoint.as_view(), name="email-verify"),
|
path("email-verify/", VerifyEmailEndpoint.as_view(), name="email-verify"),
|
||||||
path(
|
path(
|
||||||
@@ -276,9 +272,7 @@ urlpatterns = [
|
|||||||
# user workspace invitations
|
# user workspace invitations
|
||||||
path(
|
path(
|
||||||
"users/me/invitations/workspaces/",
|
"users/me/invitations/workspaces/",
|
||||||
UserWorkspaceInvitationsEndpoint.as_view(
|
UserWorkspaceInvitationsEndpoint.as_view({"get": "list", "post": "create"}),
|
||||||
{"get": "list", "post": "create"}
|
|
||||||
),
|
|
||||||
name="user-workspace-invitations",
|
name="user-workspace-invitations",
|
||||||
),
|
),
|
||||||
# user workspace invitation
|
# user workspace invitation
|
||||||
@@ -317,9 +311,7 @@ urlpatterns = [
|
|||||||
# user project invitations
|
# user project invitations
|
||||||
path(
|
path(
|
||||||
"users/me/invitations/projects/",
|
"users/me/invitations/projects/",
|
||||||
UserProjectInvitationsViewset.as_view(
|
UserProjectInvitationsViewset.as_view({"get": "list", "post": "create"}),
|
||||||
{"get": "list", "post": "create"}
|
|
||||||
),
|
|
||||||
name="user-project-invitaions",
|
name="user-project-invitaions",
|
||||||
),
|
),
|
||||||
## Workspaces ##
|
## Workspaces ##
|
||||||
@@ -1246,7 +1238,7 @@ urlpatterns = [
|
|||||||
"post": "unarchive",
|
"post": "unarchive",
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
name="project-page-unarchive",
|
name="project-page-unarchive"
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-pages/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-pages/",
|
||||||
@@ -1272,22 +1264,19 @@ urlpatterns = [
|
|||||||
{
|
{
|
||||||
"post": "unlock",
|
"post": "unlock",
|
||||||
}
|
}
|
||||||
),
|
)
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/transactions/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/transactions/",
|
||||||
PageLogEndpoint.as_view(),
|
PageLogEndpoint.as_view(), name="page-transactions"
|
||||||
name="page-transactions",
|
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/transactions/<uuid:transaction>/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/transactions/<uuid:transaction>/",
|
||||||
PageLogEndpoint.as_view(),
|
PageLogEndpoint.as_view(), name="page-transactions"
|
||||||
name="page-transactions",
|
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/sub-pages/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/sub-pages/",
|
||||||
SubPagesEndpoint.as_view(),
|
SubPagesEndpoint.as_view(), name="sub-page"
|
||||||
name="sub-page",
|
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/",
|
||||||
@@ -1337,9 +1326,7 @@ urlpatterns = [
|
|||||||
## End Pages
|
## End Pages
|
||||||
# API Tokens
|
# API Tokens
|
||||||
path("api-tokens/", ApiTokenEndpoint.as_view(), name="api-tokens"),
|
path("api-tokens/", ApiTokenEndpoint.as_view(), name="api-tokens"),
|
||||||
path(
|
path("api-tokens/<uuid:pk>/", ApiTokenEndpoint.as_view(), name="api-tokens"),
|
||||||
"api-tokens/<uuid:pk>/", ApiTokenEndpoint.as_view(), name="api-tokens"
|
|
||||||
),
|
|
||||||
## End API Tokens
|
## End API Tokens
|
||||||
# Integrations
|
# Integrations
|
||||||
path(
|
path(
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ from .project import (
|
|||||||
ProjectFavoritesViewSet,
|
ProjectFavoritesViewSet,
|
||||||
ProjectPublicCoverImagesEndpoint,
|
ProjectPublicCoverImagesEndpoint,
|
||||||
ProjectDeployBoardViewSet,
|
ProjectDeployBoardViewSet,
|
||||||
UserProjectRolesEndpoint,
|
|
||||||
)
|
)
|
||||||
from .user import (
|
from .user import (
|
||||||
UserEndpoint,
|
UserEndpoint,
|
||||||
@@ -45,10 +44,6 @@ from .workspace import (
|
|||||||
WorkspaceUserProfileEndpoint,
|
WorkspaceUserProfileEndpoint,
|
||||||
WorkspaceUserProfileIssuesEndpoint,
|
WorkspaceUserProfileIssuesEndpoint,
|
||||||
WorkspaceLabelsEndpoint,
|
WorkspaceLabelsEndpoint,
|
||||||
WorkspaceProjectMemberEndpoint,
|
|
||||||
WorkspaceUserPropertiesEndpoint,
|
|
||||||
WorkspaceStatesEndpoint,
|
|
||||||
WorkspaceEstimatesEndpoint,
|
|
||||||
)
|
)
|
||||||
from .state import StateViewSet
|
from .state import StateViewSet
|
||||||
from .view import (
|
from .view import (
|
||||||
@@ -63,11 +58,13 @@ from .cycle import (
|
|||||||
CycleDateCheckEndpoint,
|
CycleDateCheckEndpoint,
|
||||||
CycleFavoriteViewSet,
|
CycleFavoriteViewSet,
|
||||||
TransferCycleIssueEndpoint,
|
TransferCycleIssueEndpoint,
|
||||||
CycleUserPropertiesEndpoint,
|
CycleIssueGroupedEndpoint,
|
||||||
)
|
)
|
||||||
from .asset import FileAssetEndpoint, UserAssetsEndpoint, FileAssetViewSet
|
from .asset import FileAssetEndpoint, UserAssetsEndpoint
|
||||||
from .issue import (
|
from .issue import (
|
||||||
IssueViewSet,
|
IssueViewSet,
|
||||||
|
IssueListEndpoint,
|
||||||
|
IssueListGroupedEndpoint,
|
||||||
WorkSpaceIssuesEndpoint,
|
WorkSpaceIssuesEndpoint,
|
||||||
IssueActivityEndpoint,
|
IssueActivityEndpoint,
|
||||||
IssueCommentViewSet,
|
IssueCommentViewSet,
|
||||||
@@ -88,19 +85,20 @@ from .issue import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from .auth_extended import (
|
from .auth_extended import (
|
||||||
|
VerifyEmailEndpoint,
|
||||||
|
RequestEmailVerificationEndpoint,
|
||||||
ForgotPasswordEndpoint,
|
ForgotPasswordEndpoint,
|
||||||
ResetPasswordEndpoint,
|
ResetPasswordEndpoint,
|
||||||
ChangePasswordEndpoint,
|
ChangePasswordEndpoint,
|
||||||
SetUserPasswordEndpoint,
|
|
||||||
EmailCheckEndpoint,
|
|
||||||
MagicGenerateEndpoint,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
from .authentication import (
|
from .authentication import (
|
||||||
|
SignUpEndpoint,
|
||||||
SignInEndpoint,
|
SignInEndpoint,
|
||||||
SignOutEndpoint,
|
SignOutEndpoint,
|
||||||
MagicSignInEndpoint,
|
MagicSignInEndpoint,
|
||||||
|
MagicSignInGenerateEndpoint,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .module import (
|
from .module import (
|
||||||
@@ -108,7 +106,7 @@ from .module import (
|
|||||||
ModuleIssueViewSet,
|
ModuleIssueViewSet,
|
||||||
ModuleLinkViewSet,
|
ModuleLinkViewSet,
|
||||||
ModuleFavoriteViewSet,
|
ModuleFavoriteViewSet,
|
||||||
ModuleUserPropertiesEndpoint,
|
ModuleIssueGroupedEndpoint,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .api import ApiTokenEndpoint
|
from .api import ApiTokenEndpoint
|
||||||
@@ -137,16 +135,13 @@ from .page import (
|
|||||||
PageFavoriteViewSet,
|
PageFavoriteViewSet,
|
||||||
PageLogEndpoint,
|
PageLogEndpoint,
|
||||||
SubPagesEndpoint,
|
SubPagesEndpoint,
|
||||||
|
CreateIssueFromBlockEndpoint,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .search import GlobalSearchEndpoint, IssueSearchEndpoint
|
from .search import GlobalSearchEndpoint, IssueSearchEndpoint
|
||||||
|
|
||||||
|
|
||||||
from .external import (
|
from .external import GPTIntegrationEndpoint, ReleaseNotesEndpoint, UnsplashEndpoint
|
||||||
GPTIntegrationEndpoint,
|
|
||||||
ReleaseNotesEndpoint,
|
|
||||||
UnsplashEndpoint,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .estimate import (
|
from .estimate import (
|
||||||
ProjectEstimatePointEndpoint,
|
ProjectEstimatePointEndpoint,
|
||||||
@@ -167,20 +162,10 @@ from .notification import (
|
|||||||
NotificationViewSet,
|
NotificationViewSet,
|
||||||
UnreadNotificationEndpoint,
|
UnreadNotificationEndpoint,
|
||||||
MarkAllReadNotificationViewSet,
|
MarkAllReadNotificationViewSet,
|
||||||
UserNotificationPreferenceEndpoint,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
from .exporter import ExportIssuesEndpoint
|
from .exporter import ExportIssuesEndpoint
|
||||||
|
|
||||||
from .config import ConfigurationEndpoint, MobileConfigurationEndpoint
|
from .config import ConfigurationEndpoint
|
||||||
|
|
||||||
from .webhook import (
|
from .webhook import WebhookEndpoint, WebhookLogsEndpoint, WebhookSecretRegenerateEndpoint
|
||||||
WebhookEndpoint,
|
|
||||||
WebhookLogsEndpoint,
|
|
||||||
WebhookSecretRegenerateEndpoint,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .dashboard import (
|
|
||||||
DashboardEndpoint,
|
|
||||||
WidgetsEndpoint
|
|
||||||
)
|
|
||||||
@@ -61,9 +61,7 @@ class AnalyticsEndpoint(BaseAPIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# If segment is present it cannot be same as x-axis
|
# If segment is present it cannot be same as x-axis
|
||||||
if segment and (
|
if segment and (segment not in valid_xaxis_segment or x_axis == segment):
|
||||||
segment not in valid_xaxis_segment or x_axis == segment
|
|
||||||
):
|
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
"error": "Both segment and x axis cannot be same and segment should be valid"
|
"error": "Both segment and x axis cannot be same and segment should be valid"
|
||||||
@@ -112,9 +110,7 @@ class AnalyticsEndpoint(BaseAPIView):
|
|||||||
if x_axis in ["assignees__id"] or segment in ["assignees__id"]:
|
if x_axis in ["assignees__id"] or segment in ["assignees__id"]:
|
||||||
assignee_details = (
|
assignee_details = (
|
||||||
Issue.issue_objects.filter(
|
Issue.issue_objects.filter(
|
||||||
workspace__slug=slug,
|
workspace__slug=slug, **filters, assignees__avatar__isnull=False
|
||||||
**filters,
|
|
||||||
assignees__avatar__isnull=False,
|
|
||||||
)
|
)
|
||||||
.order_by("assignees__id")
|
.order_by("assignees__id")
|
||||||
.distinct("assignees__id")
|
.distinct("assignees__id")
|
||||||
@@ -128,9 +124,7 @@ class AnalyticsEndpoint(BaseAPIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
cycle_details = {}
|
cycle_details = {}
|
||||||
if x_axis in ["issue_cycle__cycle_id"] or segment in [
|
if x_axis in ["issue_cycle__cycle_id"] or segment in ["issue_cycle__cycle_id"]:
|
||||||
"issue_cycle__cycle_id"
|
|
||||||
]:
|
|
||||||
cycle_details = (
|
cycle_details = (
|
||||||
Issue.issue_objects.filter(
|
Issue.issue_objects.filter(
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
@@ -192,9 +186,7 @@ class AnalyticViewViewset(BaseViewSet):
|
|||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.filter_queryset(
|
return self.filter_queryset(
|
||||||
super()
|
super().get_queryset().filter(workspace__slug=self.kwargs.get("slug"))
|
||||||
.get_queryset()
|
|
||||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -204,9 +196,7 @@ class SavedAnalyticEndpoint(BaseAPIView):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def get(self, request, slug, analytic_id):
|
def get(self, request, slug, analytic_id):
|
||||||
analytic_view = AnalyticView.objects.get(
|
analytic_view = AnalyticView.objects.get(pk=analytic_id, workspace__slug=slug)
|
||||||
pk=analytic_id, workspace__slug=slug
|
|
||||||
)
|
|
||||||
|
|
||||||
filter = analytic_view.query
|
filter = analytic_view.query
|
||||||
queryset = Issue.issue_objects.filter(**filter)
|
queryset = Issue.issue_objects.filter(**filter)
|
||||||
@@ -276,9 +266,7 @@ class ExportAnalyticsEndpoint(BaseAPIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# If segment is present it cannot be same as x-axis
|
# If segment is present it cannot be same as x-axis
|
||||||
if segment and (
|
if segment and (segment not in valid_xaxis_segment or x_axis == segment):
|
||||||
segment not in valid_xaxis_segment or x_axis == segment
|
|
||||||
):
|
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
"error": "Both segment and x axis cannot be same and segment should be valid"
|
"error": "Both segment and x axis cannot be same and segment should be valid"
|
||||||
@@ -305,9 +293,7 @@ class DefaultAnalyticsEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
def get(self, request, slug):
|
def get(self, request, slug):
|
||||||
filters = issue_filters(request.GET, "GET")
|
filters = issue_filters(request.GET, "GET")
|
||||||
base_issues = Issue.issue_objects.filter(
|
base_issues = Issue.issue_objects.filter(workspace__slug=slug, **filters)
|
||||||
workspace__slug=slug, **filters
|
|
||||||
)
|
|
||||||
|
|
||||||
total_issues = base_issues.count()
|
total_issues = base_issues.count()
|
||||||
|
|
||||||
@@ -320,9 +306,7 @@ class DefaultAnalyticsEndpoint(BaseAPIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
open_issues_groups = ["backlog", "unstarted", "started"]
|
open_issues_groups = ["backlog", "unstarted", "started"]
|
||||||
open_issues_queryset = state_groups.filter(
|
open_issues_queryset = state_groups.filter(state__group__in=open_issues_groups)
|
||||||
state__group__in=open_issues_groups
|
|
||||||
)
|
|
||||||
|
|
||||||
open_issues = open_issues_queryset.count()
|
open_issues = open_issues_queryset.count()
|
||||||
open_issues_classified = (
|
open_issues_classified = (
|
||||||
@@ -377,12 +361,10 @@ class DefaultAnalyticsEndpoint(BaseAPIView):
|
|||||||
.order_by("-count")
|
.order_by("-count")
|
||||||
)
|
)
|
||||||
|
|
||||||
open_estimate_sum = open_issues_queryset.aggregate(
|
open_estimate_sum = open_issues_queryset.aggregate(sum=Sum("estimate_point"))[
|
||||||
sum=Sum("estimate_point")
|
|
||||||
)["sum"]
|
|
||||||
total_estimate_sum = base_issues.aggregate(sum=Sum("estimate_point"))[
|
|
||||||
"sum"
|
"sum"
|
||||||
]
|
]
|
||||||
|
total_estimate_sum = base_issues.aggregate(sum=Sum("estimate_point"))["sum"]
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -71,9 +71,7 @@ class ApiTokenEndpoint(BaseAPIView):
|
|||||||
user=request.user,
|
user=request.user,
|
||||||
pk=pk,
|
pk=pk,
|
||||||
)
|
)
|
||||||
serializer = APITokenSerializer(
|
serializer = APITokenSerializer(api_token, data=request.data, partial=True)
|
||||||
api_token, 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)
|
||||||
|
|||||||
@@ -4,17 +4,13 @@ from rest_framework.response import Response
|
|||||||
from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
|
from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from .base import BaseAPIView, BaseViewSet
|
from .base import BaseAPIView
|
||||||
from plane.db.models import FileAsset, Workspace
|
from plane.db.models import FileAsset, Workspace
|
||||||
from plane.app.serializers import FileAssetSerializer
|
from plane.app.serializers import FileAssetSerializer
|
||||||
|
|
||||||
|
|
||||||
class FileAssetEndpoint(BaseAPIView):
|
class FileAssetEndpoint(BaseAPIView):
|
||||||
parser_classes = (
|
parser_classes = (MultiPartParser, FormParser, JSONParser,)
|
||||||
MultiPartParser,
|
|
||||||
FormParser,
|
|
||||||
JSONParser,
|
|
||||||
)
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
A viewset for viewing and editing task instances.
|
A viewset for viewing and editing task instances.
|
||||||
@@ -24,18 +20,10 @@ class FileAssetEndpoint(BaseAPIView):
|
|||||||
asset_key = str(workspace_id) + "/" + asset_key
|
asset_key = str(workspace_id) + "/" + asset_key
|
||||||
files = FileAsset.objects.filter(asset=asset_key)
|
files = FileAsset.objects.filter(asset=asset_key)
|
||||||
if files.exists():
|
if files.exists():
|
||||||
serializer = FileAssetSerializer(
|
serializer = FileAssetSerializer(files, context={"request": request}, many=True)
|
||||||
files, context={"request": request}, many=True
|
return Response({"data": serializer.data, "status": True}, status=status.HTTP_200_OK)
|
||||||
)
|
|
||||||
return Response(
|
|
||||||
{"data": serializer.data, "status": True},
|
|
||||||
status=status.HTTP_200_OK,
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
return Response(
|
return Response({"error": "Asset key does not exist", "status": False}, status=status.HTTP_200_OK)
|
||||||
{"error": "Asset key does not exist", "status": False},
|
|
||||||
status=status.HTTP_200_OK,
|
|
||||||
)
|
|
||||||
|
|
||||||
def post(self, request, slug):
|
def post(self, request, slug):
|
||||||
serializer = FileAssetSerializer(data=request.data)
|
serializer = FileAssetSerializer(data=request.data)
|
||||||
@@ -45,20 +33,11 @@ class FileAssetEndpoint(BaseAPIView):
|
|||||||
serializer.save(workspace_id=workspace.id)
|
serializer.save(workspace_id=workspace.id)
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
def delete(self, request, workspace_id, asset_key):
|
def patch(self, request, workspace_id, asset_key):
|
||||||
asset_key = str(workspace_id) + "/" + asset_key
|
asset_key = str(workspace_id) + "/" + asset_key
|
||||||
file_asset = FileAsset.objects.get(asset=asset_key)
|
file_asset = FileAsset.objects.get(asset=asset_key)
|
||||||
file_asset.is_deleted = True
|
file_asset.is_deleted = request.data.get("is_deleted", file_asset.is_deleted)
|
||||||
file_asset.save()
|
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
|
||||||
|
|
||||||
|
|
||||||
class FileAssetViewSet(BaseViewSet):
|
|
||||||
def restore(self, request, workspace_id, asset_key):
|
|
||||||
asset_key = str(workspace_id) + "/" + asset_key
|
|
||||||
file_asset = FileAsset.objects.get(asset=asset_key)
|
|
||||||
file_asset.is_deleted = False
|
|
||||||
file_asset.save()
|
file_asset.save()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
@@ -67,22 +46,12 @@ class UserAssetsEndpoint(BaseAPIView):
|
|||||||
parser_classes = (MultiPartParser, FormParser)
|
parser_classes = (MultiPartParser, FormParser)
|
||||||
|
|
||||||
def get(self, request, asset_key):
|
def get(self, request, asset_key):
|
||||||
files = FileAsset.objects.filter(
|
files = FileAsset.objects.filter(asset=asset_key, created_by=request.user)
|
||||||
asset=asset_key, created_by=request.user
|
|
||||||
)
|
|
||||||
if files.exists():
|
if files.exists():
|
||||||
serializer = FileAssetSerializer(
|
serializer = FileAssetSerializer(files, context={"request": request})
|
||||||
files, context={"request": request}
|
return Response({"data": serializer.data, "status": True}, status=status.HTTP_200_OK)
|
||||||
)
|
|
||||||
return Response(
|
|
||||||
{"data": serializer.data, "status": True},
|
|
||||||
status=status.HTTP_200_OK,
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
return Response(
|
return Response({"error": "Asset key does not exist", "status": False}, status=status.HTTP_200_OK)
|
||||||
{"error": "Asset key does not exist", "status": False},
|
|
||||||
status=status.HTTP_200_OK,
|
|
||||||
)
|
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
serializer = FileAssetSerializer(data=request.data)
|
serializer = FileAssetSerializer(data=request.data)
|
||||||
@@ -91,10 +60,11 @@ class UserAssetsEndpoint(BaseAPIView):
|
|||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
def delete(self, request, asset_key):
|
def delete(self, request, asset_key):
|
||||||
file_asset = FileAsset.objects.get(
|
file_asset = FileAsset.objects.get(asset=asset_key, created_by=request.user)
|
||||||
asset=asset_key, created_by=request.user
|
# Delete the file from storage
|
||||||
)
|
file_asset.asset.delete(save=False)
|
||||||
file_asset.is_deleted = True
|
# Delete the file object
|
||||||
file_asset.save()
|
file_asset.delete()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
## Python imports
|
## Python imports
|
||||||
import uuid
|
import jwt
|
||||||
import os
|
|
||||||
import json
|
|
||||||
import random
|
|
||||||
import string
|
|
||||||
|
|
||||||
## Django imports
|
## Django imports
|
||||||
from django.contrib.auth.tokens import PasswordResetTokenGenerator
|
from django.contrib.auth.tokens import PasswordResetTokenGenerator
|
||||||
@@ -12,165 +8,115 @@ from django.utils.encoding import (
|
|||||||
smart_bytes,
|
smart_bytes,
|
||||||
DjangoUnicodeDecodeError,
|
DjangoUnicodeDecodeError,
|
||||||
)
|
)
|
||||||
from django.contrib.auth.hashers import make_password
|
|
||||||
from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode
|
from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode
|
||||||
from django.core.validators import validate_email
|
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
## Third Party Imports
|
## Third Party Imports
|
||||||
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 import permissions
|
||||||
from rest_framework_simplejwt.tokens import RefreshToken
|
from rest_framework_simplejwt.tokens import RefreshToken
|
||||||
|
|
||||||
|
from sentry_sdk import capture_exception
|
||||||
|
|
||||||
## Module imports
|
## Module imports
|
||||||
from . import BaseAPIView
|
from . import BaseAPIView
|
||||||
from plane.app.serializers import (
|
from plane.app.serializers import (
|
||||||
ChangePasswordSerializer,
|
ChangePasswordSerializer,
|
||||||
ResetPasswordSerializer,
|
ResetPasswordSerializer,
|
||||||
UserSerializer,
|
|
||||||
)
|
)
|
||||||
from plane.db.models import User, WorkspaceMemberInvite
|
from plane.db.models import User
|
||||||
from plane.license.utils.instance_value import get_configuration_value
|
from plane.bgtasks.email_verification_task import email_verification
|
||||||
from plane.bgtasks.forgot_password_task import forgot_password
|
from plane.bgtasks.forgot_password_task import forgot_password
|
||||||
from plane.license.models import Instance
|
|
||||||
from plane.settings.redis import redis_instance
|
|
||||||
from plane.bgtasks.magic_link_code_task import magic_link
|
|
||||||
from plane.bgtasks.event_tracking_task import auth_events
|
|
||||||
|
|
||||||
|
|
||||||
def get_tokens_for_user(user):
|
class RequestEmailVerificationEndpoint(BaseAPIView):
|
||||||
refresh = RefreshToken.for_user(user)
|
def get(self, request):
|
||||||
return (
|
token = RefreshToken.for_user(request.user).access_token
|
||||||
str(refresh.access_token),
|
current_site = request.META.get('HTTP_ORIGIN')
|
||||||
str(refresh),
|
email_verification.delay(
|
||||||
)
|
request.user.first_name, request.user.email, token, current_site
|
||||||
|
)
|
||||||
|
return Response(
|
||||||
|
{"message": "Email sent successfully"}, status=status.HTTP_200_OK
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def generate_magic_token(email):
|
class VerifyEmailEndpoint(BaseAPIView):
|
||||||
key = "magic_" + str(email)
|
def get(self, request):
|
||||||
|
token = request.GET.get("token")
|
||||||
|
try:
|
||||||
|
payload = jwt.decode(token, settings.SECRET_KEY, algorithms="HS256")
|
||||||
|
user = User.objects.get(id=payload["user_id"])
|
||||||
|
|
||||||
## Generate a random token
|
if not user.is_email_verified:
|
||||||
token = (
|
user.is_email_verified = True
|
||||||
"".join(random.choices(string.ascii_lowercase, k=4))
|
user.save()
|
||||||
+ "-"
|
return Response(
|
||||||
+ "".join(random.choices(string.ascii_lowercase, k=4))
|
{"email": "Successfully activated"}, status=status.HTTP_200_OK
|
||||||
+ "-"
|
)
|
||||||
+ "".join(random.choices(string.ascii_lowercase, k=4))
|
except jwt.ExpiredSignatureError as _indentifier:
|
||||||
)
|
return Response(
|
||||||
|
{"email": "Activation expired"}, status=status.HTTP_400_BAD_REQUEST
|
||||||
# Initialize the redis instance
|
)
|
||||||
ri = redis_instance()
|
except jwt.exceptions.DecodeError as _indentifier:
|
||||||
|
return Response(
|
||||||
# Check if the key already exists in python
|
{"email": "Invalid token"}, status=status.HTTP_400_BAD_REQUEST
|
||||||
if ri.exists(key):
|
)
|
||||||
data = json.loads(ri.get(key))
|
|
||||||
|
|
||||||
current_attempt = data["current_attempt"] + 1
|
|
||||||
|
|
||||||
if data["current_attempt"] > 2:
|
|
||||||
return key, token, False
|
|
||||||
|
|
||||||
value = {
|
|
||||||
"current_attempt": current_attempt,
|
|
||||||
"email": email,
|
|
||||||
"token": token,
|
|
||||||
}
|
|
||||||
expiry = 600
|
|
||||||
|
|
||||||
ri.set(key, json.dumps(value), ex=expiry)
|
|
||||||
|
|
||||||
else:
|
|
||||||
value = {"current_attempt": 0, "email": email, "token": token}
|
|
||||||
expiry = 600
|
|
||||||
|
|
||||||
ri.set(key, json.dumps(value), ex=expiry)
|
|
||||||
|
|
||||||
return key, token, True
|
|
||||||
|
|
||||||
|
|
||||||
def generate_password_token(user):
|
|
||||||
uidb64 = urlsafe_base64_encode(smart_bytes(user.id))
|
|
||||||
token = PasswordResetTokenGenerator().make_token(user)
|
|
||||||
|
|
||||||
return uidb64, token
|
|
||||||
|
|
||||||
|
|
||||||
class ForgotPasswordEndpoint(BaseAPIView):
|
class ForgotPasswordEndpoint(BaseAPIView):
|
||||||
permission_classes = [
|
permission_classes = [permissions.AllowAny]
|
||||||
AllowAny,
|
|
||||||
]
|
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
email = request.data.get("email")
|
email = request.data.get("email")
|
||||||
|
|
||||||
try:
|
if User.objects.filter(email=email).exists():
|
||||||
validate_email(email)
|
user = User.objects.get(email=email)
|
||||||
except ValidationError:
|
uidb64 = urlsafe_base64_encode(smart_bytes(user.id))
|
||||||
return Response(
|
token = PasswordResetTokenGenerator().make_token(user)
|
||||||
{"error": "Please enter a valid email"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
current_site = request.META.get('HTTP_ORIGIN')
|
||||||
)
|
|
||||||
|
|
||||||
# Get the user
|
|
||||||
user = User.objects.filter(email=email).first()
|
|
||||||
if user:
|
|
||||||
# Get the reset token for user
|
|
||||||
uidb64, token = generate_password_token(user=user)
|
|
||||||
current_site = request.META.get("HTTP_ORIGIN")
|
|
||||||
# send the forgot password email
|
|
||||||
forgot_password.delay(
|
forgot_password.delay(
|
||||||
user.first_name, user.email, uidb64, token, current_site
|
user.first_name, user.email, uidb64, token, current_site
|
||||||
)
|
)
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
{"message": "Check your email to reset your password"},
|
{"message": "Check your email to reset your password"},
|
||||||
status=status.HTTP_200_OK,
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Please check the email"},
|
{"error": "Please check the email"}, status=status.HTTP_400_BAD_REQUEST
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ResetPasswordEndpoint(BaseAPIView):
|
class ResetPasswordEndpoint(BaseAPIView):
|
||||||
permission_classes = [
|
permission_classes = [permissions.AllowAny]
|
||||||
AllowAny,
|
|
||||||
]
|
|
||||||
|
|
||||||
def post(self, request, uidb64, token):
|
def post(self, request, uidb64, token):
|
||||||
try:
|
try:
|
||||||
# Decode the id from the uidb64
|
|
||||||
id = smart_str(urlsafe_base64_decode(uidb64))
|
id = smart_str(urlsafe_base64_decode(uidb64))
|
||||||
user = User.objects.get(id=id)
|
user = User.objects.get(id=id)
|
||||||
|
|
||||||
# check if the token is valid for the user
|
|
||||||
if not PasswordResetTokenGenerator().check_token(user, token):
|
if not PasswordResetTokenGenerator().check_token(user, token):
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Token is invalid"},
|
{"error": "token is not valid, please check the new one"},
|
||||||
status=status.HTTP_401_UNAUTHORIZED,
|
status=status.HTTP_401_UNAUTHORIZED,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Reset the password
|
|
||||||
serializer = ResetPasswordSerializer(data=request.data)
|
serializer = ResetPasswordSerializer(data=request.data)
|
||||||
|
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
# set_password also hashes the password that the user will get
|
# set_password also hashes the password that the user will get
|
||||||
user.set_password(serializer.data.get("new_password"))
|
user.set_password(serializer.data.get("new_password"))
|
||||||
user.is_password_autoset = False
|
|
||||||
user.save()
|
user.save()
|
||||||
|
response = {
|
||||||
# Log the user in
|
"status": "success",
|
||||||
# Generate access token for the user
|
"code": status.HTTP_200_OK,
|
||||||
access_token, refresh_token = get_tokens_for_user(user)
|
"message": "Password updated successfully",
|
||||||
data = {
|
|
||||||
"access_token": access_token,
|
|
||||||
"refresh_token": refresh_token,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Response(data, status=status.HTTP_200_OK)
|
return Response(response)
|
||||||
return Response(
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
|
||||||
)
|
|
||||||
|
|
||||||
except DjangoUnicodeDecodeError as indentifier:
|
except DjangoUnicodeDecodeError as indentifier:
|
||||||
return Response(
|
return Response(
|
||||||
@@ -182,302 +128,24 @@ class ResetPasswordEndpoint(BaseAPIView):
|
|||||||
class ChangePasswordEndpoint(BaseAPIView):
|
class ChangePasswordEndpoint(BaseAPIView):
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
serializer = ChangePasswordSerializer(data=request.data)
|
serializer = ChangePasswordSerializer(data=request.data)
|
||||||
|
|
||||||
user = User.objects.get(pk=request.user.id)
|
user = User.objects.get(pk=request.user.id)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
if not user.check_password(serializer.data.get("old_password")):
|
# Check old password
|
||||||
|
if not user.object.check_password(serializer.data.get("old_password")):
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Old password is not correct"},
|
{"old_password": ["Wrong password."]},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
# set_password also hashes the password that the user will get
|
# set_password also hashes the password that the user will get
|
||||||
user.set_password(serializer.data.get("new_password"))
|
self.object.set_password(serializer.data.get("new_password"))
|
||||||
user.is_password_autoset = False
|
self.object.save()
|
||||||
user.save()
|
response = {
|
||||||
return Response(
|
"status": "success",
|
||||||
{"message": "Password updated successfully"},
|
"code": status.HTTP_200_OK,
|
||||||
status=status.HTTP_200_OK,
|
"message": "Password updated successfully",
|
||||||
)
|
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
|
|
||||||
class SetUserPasswordEndpoint(BaseAPIView):
|
|
||||||
def post(self, request):
|
|
||||||
user = User.objects.get(pk=request.user.id)
|
|
||||||
password = request.data.get("password", False)
|
|
||||||
|
|
||||||
# If the user password is not autoset then return error
|
|
||||||
if not user.is_password_autoset:
|
|
||||||
return Response(
|
|
||||||
{
|
|
||||||
"error": "Your password is already set please change your password from profile"
|
|
||||||
},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check password validation
|
|
||||||
if not password and len(str(password)) < 8:
|
|
||||||
return Response(
|
|
||||||
{"error": "Password is not valid"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Set the user password
|
|
||||||
user.set_password(password)
|
|
||||||
user.is_password_autoset = False
|
|
||||||
user.save()
|
|
||||||
serializer = UserSerializer(user)
|
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
|
||||||
|
|
||||||
|
|
||||||
class MagicGenerateEndpoint(BaseAPIView):
|
|
||||||
permission_classes = [
|
|
||||||
AllowAny,
|
|
||||||
]
|
|
||||||
|
|
||||||
def post(self, request):
|
|
||||||
email = request.data.get("email", False)
|
|
||||||
|
|
||||||
# Check the instance registration
|
|
||||||
instance = Instance.objects.first()
|
|
||||||
if instance is None or not instance.is_setup_done:
|
|
||||||
return Response(
|
|
||||||
{"error": "Instance is not configured"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
if not email:
|
|
||||||
return Response(
|
|
||||||
{"error": "Please provide a valid email address"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Clean up the email
|
|
||||||
email = email.strip().lower()
|
|
||||||
validate_email(email)
|
|
||||||
|
|
||||||
# check if the email exists not
|
|
||||||
if not User.objects.filter(email=email).exists():
|
|
||||||
# Create a user
|
|
||||||
_ = User.objects.create(
|
|
||||||
email=email,
|
|
||||||
username=uuid.uuid4().hex,
|
|
||||||
password=make_password(uuid.uuid4().hex),
|
|
||||||
is_password_autoset=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
## Generate a random token
|
|
||||||
token = (
|
|
||||||
"".join(random.choices(string.ascii_lowercase, k=4))
|
|
||||||
+ "-"
|
|
||||||
+ "".join(random.choices(string.ascii_lowercase, k=4))
|
|
||||||
+ "-"
|
|
||||||
+ "".join(random.choices(string.ascii_lowercase, k=4))
|
|
||||||
)
|
|
||||||
|
|
||||||
ri = redis_instance()
|
|
||||||
|
|
||||||
key = "magic_" + str(email)
|
|
||||||
|
|
||||||
# Check if the key already exists in python
|
|
||||||
if ri.exists(key):
|
|
||||||
data = json.loads(ri.get(key))
|
|
||||||
|
|
||||||
current_attempt = data["current_attempt"] + 1
|
|
||||||
|
|
||||||
if data["current_attempt"] > 2:
|
|
||||||
return Response(
|
|
||||||
{
|
|
||||||
"error": "Max attempts exhausted. Please try again later."
|
|
||||||
},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
value = {
|
|
||||||
"current_attempt": current_attempt,
|
|
||||||
"email": email,
|
|
||||||
"token": token,
|
|
||||||
}
|
}
|
||||||
expiry = 600
|
|
||||||
|
|
||||||
ri.set(key, json.dumps(value), ex=expiry)
|
return Response(response)
|
||||||
|
|
||||||
else:
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
value = {"current_attempt": 0, "email": email, "token": token}
|
|
||||||
expiry = 600
|
|
||||||
|
|
||||||
ri.set(key, json.dumps(value), ex=expiry)
|
|
||||||
|
|
||||||
# If the smtp is configured send through here
|
|
||||||
current_site = request.META.get("HTTP_ORIGIN")
|
|
||||||
magic_link.delay(email, key, token, current_site)
|
|
||||||
|
|
||||||
return Response({"key": key}, status=status.HTTP_200_OK)
|
|
||||||
|
|
||||||
|
|
||||||
class EmailCheckEndpoint(BaseAPIView):
|
|
||||||
permission_classes = [
|
|
||||||
AllowAny,
|
|
||||||
]
|
|
||||||
|
|
||||||
def post(self, request):
|
|
||||||
# Check the instance registration
|
|
||||||
instance = Instance.objects.first()
|
|
||||||
if instance is None or not instance.is_setup_done:
|
|
||||||
return Response(
|
|
||||||
{"error": "Instance is not configured"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Get configuration values
|
|
||||||
ENABLE_SIGNUP, ENABLE_MAGIC_LINK_LOGIN = get_configuration_value(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"key": "ENABLE_SIGNUP",
|
|
||||||
"default": os.environ.get("ENABLE_SIGNUP"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "ENABLE_MAGIC_LINK_LOGIN",
|
|
||||||
"default": os.environ.get("ENABLE_MAGIC_LINK_LOGIN"),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
email = request.data.get("email", False)
|
|
||||||
|
|
||||||
if not email:
|
|
||||||
return Response(
|
|
||||||
{"error": "Email is required"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
# validate the email
|
|
||||||
try:
|
|
||||||
validate_email(email)
|
|
||||||
except ValidationError:
|
|
||||||
return Response(
|
|
||||||
{"error": "Email is not valid"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check if the user exists
|
|
||||||
user = User.objects.filter(email=email).first()
|
|
||||||
current_site = request.META.get("HTTP_ORIGIN")
|
|
||||||
|
|
||||||
# If new user
|
|
||||||
if user is None:
|
|
||||||
# Create the user
|
|
||||||
if (
|
|
||||||
ENABLE_SIGNUP == "0"
|
|
||||||
and not WorkspaceMemberInvite.objects.filter(
|
|
||||||
email=email,
|
|
||||||
).exists()
|
|
||||||
):
|
|
||||||
return Response(
|
|
||||||
{
|
|
||||||
"error": "New account creation is disabled. Please contact your site administrator"
|
|
||||||
},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create the user with default values
|
|
||||||
user = User.objects.create(
|
|
||||||
email=email,
|
|
||||||
username=uuid.uuid4().hex,
|
|
||||||
password=make_password(uuid.uuid4().hex),
|
|
||||||
is_password_autoset=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
if not bool(
|
|
||||||
ENABLE_MAGIC_LINK_LOGIN,
|
|
||||||
):
|
|
||||||
return Response(
|
|
||||||
{"error": "Magic link sign in is disabled."},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Send event
|
|
||||||
auth_events.delay(
|
|
||||||
user=user.id,
|
|
||||||
email=email,
|
|
||||||
user_agent=request.META.get("HTTP_USER_AGENT"),
|
|
||||||
ip=request.META.get("REMOTE_ADDR"),
|
|
||||||
event_name="SIGN_IN",
|
|
||||||
medium="MAGIC_LINK",
|
|
||||||
first_time=True,
|
|
||||||
)
|
|
||||||
key, token, current_attempt = generate_magic_token(email=email)
|
|
||||||
if not current_attempt:
|
|
||||||
return Response(
|
|
||||||
{
|
|
||||||
"error": "Max attempts exhausted. Please try again later."
|
|
||||||
},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
# Trigger the email
|
|
||||||
magic_link.delay(email, "magic_" + str(email), token, current_site)
|
|
||||||
return Response(
|
|
||||||
{
|
|
||||||
"is_password_autoset": user.is_password_autoset,
|
|
||||||
"is_existing": False,
|
|
||||||
},
|
|
||||||
status=status.HTTP_200_OK,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Existing user
|
|
||||||
else:
|
|
||||||
if user.is_password_autoset:
|
|
||||||
## Generate a random token
|
|
||||||
if not bool(ENABLE_MAGIC_LINK_LOGIN):
|
|
||||||
return Response(
|
|
||||||
{"error": "Magic link sign in is disabled."},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
auth_events.delay(
|
|
||||||
user=user.id,
|
|
||||||
email=email,
|
|
||||||
user_agent=request.META.get("HTTP_USER_AGENT"),
|
|
||||||
ip=request.META.get("REMOTE_ADDR"),
|
|
||||||
event_name="SIGN_IN",
|
|
||||||
medium="MAGIC_LINK",
|
|
||||||
first_time=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Generate magic token
|
|
||||||
key, token, current_attempt = generate_magic_token(email=email)
|
|
||||||
if not current_attempt:
|
|
||||||
return Response(
|
|
||||||
{
|
|
||||||
"error": "Max attempts exhausted. Please try again later."
|
|
||||||
},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Trigger the email
|
|
||||||
magic_link.delay(email, key, token, current_site)
|
|
||||||
return Response(
|
|
||||||
{
|
|
||||||
"is_password_autoset": user.is_password_autoset,
|
|
||||||
"is_existing": True,
|
|
||||||
},
|
|
||||||
status=status.HTTP_200_OK,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
auth_events.delay(
|
|
||||||
user=user.id,
|
|
||||||
email=email,
|
|
||||||
user_agent=request.META.get("HTTP_USER_AGENT"),
|
|
||||||
ip=request.META.get("REMOTE_ADDR"),
|
|
||||||
event_name="SIGN_IN",
|
|
||||||
medium="EMAIL",
|
|
||||||
first_time=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
# User should enter password to login
|
|
||||||
return Response(
|
|
||||||
{
|
|
||||||
"is_password_autoset": user.is_password_autoset,
|
|
||||||
"is_existing": True,
|
|
||||||
},
|
|
||||||
status=status.HTTP_200_OK,
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
# Python imports
|
# Python imports
|
||||||
import os
|
|
||||||
import uuid
|
import uuid
|
||||||
|
import random
|
||||||
|
import string
|
||||||
import json
|
import json
|
||||||
|
import requests
|
||||||
|
from requests.exceptions import RequestException
|
||||||
|
|
||||||
# Django imports
|
# Django imports
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
@@ -15,7 +18,8 @@ from rest_framework.response import Response
|
|||||||
from rest_framework.permissions import AllowAny
|
from rest_framework.permissions import AllowAny
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework_simplejwt.tokens import RefreshToken
|
from rest_framework_simplejwt.tokens import RefreshToken
|
||||||
from sentry_sdk import capture_message
|
|
||||||
|
from sentry_sdk import capture_exception, capture_message
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from . import BaseAPIView
|
from . import BaseAPIView
|
||||||
@@ -27,9 +31,7 @@ from plane.db.models import (
|
|||||||
ProjectMember,
|
ProjectMember,
|
||||||
)
|
)
|
||||||
from plane.settings.redis import redis_instance
|
from plane.settings.redis import redis_instance
|
||||||
from plane.license.models import Instance
|
from plane.bgtasks.magic_link_code_task import magic_link
|
||||||
from plane.license.utils.instance_value import get_configuration_value
|
|
||||||
from plane.bgtasks.event_tracking_task import auth_events
|
|
||||||
|
|
||||||
|
|
||||||
def get_tokens_for_user(user):
|
def get_tokens_for_user(user):
|
||||||
@@ -44,16 +46,17 @@ class SignUpEndpoint(BaseAPIView):
|
|||||||
permission_classes = (AllowAny,)
|
permission_classes = (AllowAny,)
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
# Check if the instance configuration is done
|
if not settings.ENABLE_SIGNUP:
|
||||||
instance = Instance.objects.first()
|
|
||||||
if instance is None or not instance.is_setup_done:
|
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Instance is not configured"},
|
{
|
||||||
|
"error": "New account creation is disabled. Please contact your site administrator"
|
||||||
|
},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
email = request.data.get("email", False)
|
email = request.data.get("email", False)
|
||||||
password = request.data.get("password", False)
|
password = request.data.get("password", False)
|
||||||
|
|
||||||
## Raise exception if any of the above are missing
|
## Raise exception if any of the above are missing
|
||||||
if not email or not password:
|
if not email or not password:
|
||||||
return Response(
|
return Response(
|
||||||
@@ -61,8 +64,8 @@ class SignUpEndpoint(BaseAPIView):
|
|||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Validate the email
|
|
||||||
email = email.strip().lower()
|
email = email.strip().lower()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
validate_email(email)
|
validate_email(email)
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
@@ -71,31 +74,6 @@ class SignUpEndpoint(BaseAPIView):
|
|||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
# get configuration values
|
|
||||||
# Get configuration values
|
|
||||||
(ENABLE_SIGNUP,) = get_configuration_value(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"key": "ENABLE_SIGNUP",
|
|
||||||
"default": os.environ.get("ENABLE_SIGNUP"),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
# If the sign up is not enabled and the user does not have invite disallow him from creating the account
|
|
||||||
if (
|
|
||||||
ENABLE_SIGNUP == "0"
|
|
||||||
and not WorkspaceMemberInvite.objects.filter(
|
|
||||||
email=email,
|
|
||||||
).exists()
|
|
||||||
):
|
|
||||||
return Response(
|
|
||||||
{
|
|
||||||
"error": "New account creation is disabled. Please contact your site administrator"
|
|
||||||
},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check if the user already exists
|
# Check if the user already exists
|
||||||
if User.objects.filter(email=email).exists():
|
if User.objects.filter(email=email).exists():
|
||||||
return Response(
|
return Response(
|
||||||
@@ -107,103 +85,6 @@ class SignUpEndpoint(BaseAPIView):
|
|||||||
user.set_password(password)
|
user.set_password(password)
|
||||||
|
|
||||||
# settings last actives for the user
|
# settings last actives for the user
|
||||||
user.is_password_autoset = False
|
|
||||||
user.last_active = timezone.now()
|
|
||||||
user.last_login_time = timezone.now()
|
|
||||||
user.last_login_ip = request.META.get("REMOTE_ADDR")
|
|
||||||
user.last_login_uagent = request.META.get("HTTP_USER_AGENT")
|
|
||||||
user.token_updated_at = timezone.now()
|
|
||||||
user.save()
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
class SignInEndpoint(BaseAPIView):
|
|
||||||
permission_classes = (AllowAny,)
|
|
||||||
|
|
||||||
def post(self, request):
|
|
||||||
# Check if the instance configuration is done
|
|
||||||
instance = Instance.objects.first()
|
|
||||||
if instance is None or not instance.is_setup_done:
|
|
||||||
return Response(
|
|
||||||
{"error": "Instance is not configured"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
email = request.data.get("email", False)
|
|
||||||
password = request.data.get("password", False)
|
|
||||||
|
|
||||||
## Raise exception if any of the above are missing
|
|
||||||
if not email or not password:
|
|
||||||
return Response(
|
|
||||||
{"error": "Both email and password are required"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Validate email
|
|
||||||
email = email.strip().lower()
|
|
||||||
try:
|
|
||||||
validate_email(email)
|
|
||||||
except ValidationError as e:
|
|
||||||
return Response(
|
|
||||||
{"error": "Please provide a valid email address."},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Get the user
|
|
||||||
user = User.objects.filter(email=email).first()
|
|
||||||
|
|
||||||
# Existing user
|
|
||||||
if user:
|
|
||||||
# Check user password
|
|
||||||
if not user.check_password(password):
|
|
||||||
return Response(
|
|
||||||
{
|
|
||||||
"error": "Sorry, we could not find a user with the provided credentials. Please try again."
|
|
||||||
},
|
|
||||||
status=status.HTTP_403_FORBIDDEN,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create the user
|
|
||||||
else:
|
|
||||||
(ENABLE_SIGNUP,) = get_configuration_value(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"key": "ENABLE_SIGNUP",
|
|
||||||
"default": os.environ.get("ENABLE_SIGNUP"),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
)
|
|
||||||
# Create the user
|
|
||||||
if (
|
|
||||||
ENABLE_SIGNUP == "0"
|
|
||||||
and not WorkspaceMemberInvite.objects.filter(
|
|
||||||
email=email,
|
|
||||||
).exists()
|
|
||||||
):
|
|
||||||
return Response(
|
|
||||||
{
|
|
||||||
"error": "New account creation is disabled. Please contact your site administrator"
|
|
||||||
},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
user = User.objects.create(
|
|
||||||
email=email,
|
|
||||||
username=uuid.uuid4().hex,
|
|
||||||
password=make_password(password),
|
|
||||||
is_password_autoset=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
# settings last active for the user
|
|
||||||
user.is_active = True
|
|
||||||
user.last_active = timezone.now()
|
user.last_active = timezone.now()
|
||||||
user.last_login_time = timezone.now()
|
user.last_login_time = timezone.now()
|
||||||
user.last_login_ip = request.META.get("REMOTE_ADDR")
|
user.last_login_ip = request.META.get("REMOTE_ADDR")
|
||||||
@@ -259,25 +140,186 @@ class SignInEndpoint(BaseAPIView):
|
|||||||
else 15,
|
else 15,
|
||||||
member=user,
|
member=user,
|
||||||
created_by_id=project_member_invite.created_by_id,
|
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
|
||||||
|
if settings.ANALYTICS_BASE_API:
|
||||||
|
_ = requests.post(
|
||||||
|
settings.ANALYTICS_BASE_API,
|
||||||
|
headers={
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-Auth-Token": settings.ANALYTICS_SECRET_KEY,
|
||||||
|
},
|
||||||
|
json={
|
||||||
|
"event_id": uuid.uuid4().hex,
|
||||||
|
"event_data": {
|
||||||
|
"medium": "email",
|
||||||
|
},
|
||||||
|
"user": {"email": email, "id": str(user.id)},
|
||||||
|
"device_ctx": {
|
||||||
|
"ip": request.META.get("REMOTE_ADDR"),
|
||||||
|
"user_agent": request.META.get("HTTP_USER_AGENT"),
|
||||||
|
},
|
||||||
|
"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)
|
||||||
|
|
||||||
|
|
||||||
|
class SignInEndpoint(BaseAPIView):
|
||||||
|
permission_classes = (AllowAny,)
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
email = request.data.get("email", False)
|
||||||
|
password = request.data.get("password", False)
|
||||||
|
|
||||||
|
## Raise exception if any of the above are missing
|
||||||
|
if not email or not password:
|
||||||
|
return Response(
|
||||||
|
{"error": "Both email and password are required"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
email = email.strip().lower()
|
||||||
|
|
||||||
|
try:
|
||||||
|
validate_email(email)
|
||||||
|
except ValidationError as e:
|
||||||
|
return Response(
|
||||||
|
{"error": "Please provide a valid email address."},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
user = User.objects.filter(email=email).first()
|
||||||
|
|
||||||
|
if user is None:
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"error": "Sorry, we could not find a user with the provided credentials. Please try again."
|
||||||
|
},
|
||||||
|
status=status.HTTP_403_FORBIDDEN,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Sign up Process
|
||||||
|
if not user.check_password(password):
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"error": "Sorry, we could not find a user with the provided credentials. Please try again."
|
||||||
|
},
|
||||||
|
status=status.HTTP_403_FORBIDDEN,
|
||||||
|
)
|
||||||
|
if not user.is_active:
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"error": "Your account has been deactivated. Please contact your site administrator."
|
||||||
|
},
|
||||||
|
status=status.HTTP_403_FORBIDDEN,
|
||||||
|
)
|
||||||
|
|
||||||
|
# settings last active for the user
|
||||||
|
user.last_active = timezone.now()
|
||||||
|
user.last_login_time = timezone.now()
|
||||||
|
user.last_login_ip = request.META.get("REMOTE_ADDR")
|
||||||
|
user.last_login_uagent = request.META.get("HTTP_USER_AGENT")
|
||||||
|
user.token_updated_at = timezone.now()
|
||||||
|
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
|
for project_member_invite in project_member_invites
|
||||||
],
|
],
|
||||||
ignore_conflicts=True,
|
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
|
# Delete all the invites
|
||||||
workspace_member_invites.delete()
|
workspace_member_invites.delete()
|
||||||
project_member_invites.delete()
|
project_member_invites.delete()
|
||||||
# Send event
|
try:
|
||||||
auth_events.delay(
|
# Send Analytics
|
||||||
user=user.id,
|
if settings.ANALYTICS_BASE_API:
|
||||||
email=email,
|
_ = requests.post(
|
||||||
user_agent=request.META.get("HTTP_USER_AGENT"),
|
settings.ANALYTICS_BASE_API,
|
||||||
ip=request.META.get("REMOTE_ADDR"),
|
headers={
|
||||||
event_name="SIGN_IN",
|
"Content-Type": "application/json",
|
||||||
medium="EMAIL",
|
"X-Auth-Token": settings.ANALYTICS_SECRET_KEY,
|
||||||
first_time=False,
|
},
|
||||||
)
|
json={
|
||||||
|
"event_id": uuid.uuid4().hex,
|
||||||
|
"event_data": {
|
||||||
|
"medium": "email",
|
||||||
|
},
|
||||||
|
"user": {"email": email, "id": str(user.id)},
|
||||||
|
"device_ctx": {
|
||||||
|
"ip": request.META.get("REMOTE_ADDR"),
|
||||||
|
"user_agent": request.META.get("HTTP_USER_AGENT"),
|
||||||
|
},
|
||||||
|
"event_type": "SIGN_IN",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
except RequestException as e:
|
||||||
|
capture_exception(e)
|
||||||
|
|
||||||
access_token, refresh_token = get_tokens_for_user(user)
|
access_token, refresh_token = get_tokens_for_user(user)
|
||||||
data = {
|
data = {
|
||||||
@@ -310,22 +352,79 @@ class SignOutEndpoint(BaseAPIView):
|
|||||||
return Response({"message": "success"}, status=status.HTTP_200_OK)
|
return Response({"message": "success"}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
class MagicSignInGenerateEndpoint(BaseAPIView):
|
||||||
|
permission_classes = [
|
||||||
|
AllowAny,
|
||||||
|
]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
email = request.data.get("email", False)
|
||||||
|
|
||||||
|
if not email:
|
||||||
|
return Response(
|
||||||
|
{"error": "Please provide a valid email address"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Clean up
|
||||||
|
email = email.strip().lower()
|
||||||
|
validate_email(email)
|
||||||
|
|
||||||
|
## Generate a random token
|
||||||
|
token = (
|
||||||
|
"".join(random.choices(string.ascii_lowercase, k=4))
|
||||||
|
+ "-"
|
||||||
|
+ "".join(random.choices(string.ascii_lowercase, k=4))
|
||||||
|
+ "-"
|
||||||
|
+ "".join(random.choices(string.ascii_lowercase, k=4))
|
||||||
|
)
|
||||||
|
|
||||||
|
ri = redis_instance()
|
||||||
|
|
||||||
|
key = "magic_" + str(email)
|
||||||
|
|
||||||
|
# Check if the key already exists in python
|
||||||
|
if ri.exists(key):
|
||||||
|
data = json.loads(ri.get(key))
|
||||||
|
|
||||||
|
current_attempt = data["current_attempt"] + 1
|
||||||
|
|
||||||
|
if data["current_attempt"] > 2:
|
||||||
|
return Response(
|
||||||
|
{"error": "Max attempts exhausted. Please try again later."},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
value = {
|
||||||
|
"current_attempt": current_attempt,
|
||||||
|
"email": email,
|
||||||
|
"token": token,
|
||||||
|
}
|
||||||
|
expiry = 600
|
||||||
|
|
||||||
|
ri.set(key, json.dumps(value), ex=expiry)
|
||||||
|
|
||||||
|
else:
|
||||||
|
value = {"current_attempt": 0, "email": email, "token": token}
|
||||||
|
expiry = 600
|
||||||
|
|
||||||
|
ri.set(key, json.dumps(value), ex=expiry)
|
||||||
|
|
||||||
|
|
||||||
|
current_site = request.META.get('HTTP_ORIGIN')
|
||||||
|
magic_link.delay(email, key, token, current_site)
|
||||||
|
|
||||||
|
return Response({"key": key}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
class MagicSignInEndpoint(BaseAPIView):
|
class MagicSignInEndpoint(BaseAPIView):
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
AllowAny,
|
AllowAny,
|
||||||
]
|
]
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
# Check if the instance configuration is done
|
|
||||||
instance = Instance.objects.first()
|
|
||||||
if instance is None or not instance.is_setup_done:
|
|
||||||
return Response(
|
|
||||||
{"error": "Instance is not configured"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
user_token = request.data.get("token", "").strip()
|
user_token = request.data.get("token", "").strip()
|
||||||
key = request.data.get("key", "").strip().lower()
|
key = request.data.get("key", False).strip().lower()
|
||||||
|
|
||||||
if not key or user_token == "":
|
if not key or user_token == "":
|
||||||
return Response(
|
return Response(
|
||||||
@@ -342,20 +441,71 @@ class MagicSignInEndpoint(BaseAPIView):
|
|||||||
email = data["email"]
|
email = data["email"]
|
||||||
|
|
||||||
if str(token) == str(user_token):
|
if str(token) == str(user_token):
|
||||||
user = User.objects.get(email=email)
|
if User.objects.filter(email=email).exists():
|
||||||
# Send event
|
user = User.objects.get(email=email)
|
||||||
auth_events.delay(
|
if not user.is_active:
|
||||||
user=user.id,
|
return Response(
|
||||||
email=email,
|
{
|
||||||
user_agent=request.META.get("HTTP_USER_AGENT"),
|
"error": "Your account has been deactivated. Please contact your site administrator."
|
||||||
ip=request.META.get("REMOTE_ADDR"),
|
},
|
||||||
event_name="SIGN_IN",
|
status=status.HTTP_403_FORBIDDEN,
|
||||||
medium="MAGIC_LINK",
|
)
|
||||||
first_time=False,
|
try:
|
||||||
)
|
# Send event to Jitsu for tracking
|
||||||
|
if settings.ANALYTICS_BASE_API:
|
||||||
|
_ = requests.post(
|
||||||
|
settings.ANALYTICS_BASE_API,
|
||||||
|
headers={
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-Auth-Token": settings.ANALYTICS_SECRET_KEY,
|
||||||
|
},
|
||||||
|
json={
|
||||||
|
"event_id": uuid.uuid4().hex,
|
||||||
|
"event_data": {
|
||||||
|
"medium": "code",
|
||||||
|
},
|
||||||
|
"user": {"email": email, "id": str(user.id)},
|
||||||
|
"device_ctx": {
|
||||||
|
"ip": request.META.get("REMOTE_ADDR"),
|
||||||
|
"user_agent": request.META.get("HTTP_USER_AGENT"),
|
||||||
|
},
|
||||||
|
"event_type": "SIGN_IN",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
except RequestException as e:
|
||||||
|
capture_exception(e)
|
||||||
|
else:
|
||||||
|
user = User.objects.create(
|
||||||
|
email=email,
|
||||||
|
username=uuid.uuid4().hex,
|
||||||
|
password=make_password(uuid.uuid4().hex),
|
||||||
|
is_password_autoset=True,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
# Send event to Jitsu for tracking
|
||||||
|
if settings.ANALYTICS_BASE_API:
|
||||||
|
_ = requests.post(
|
||||||
|
settings.ANALYTICS_BASE_API,
|
||||||
|
headers={
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-Auth-Token": settings.ANALYTICS_SECRET_KEY,
|
||||||
|
},
|
||||||
|
json={
|
||||||
|
"event_id": uuid.uuid4().hex,
|
||||||
|
"event_data": {
|
||||||
|
"medium": "code",
|
||||||
|
},
|
||||||
|
"user": {"email": email, "id": str(user.id)},
|
||||||
|
"device_ctx": {
|
||||||
|
"ip": request.META.get("REMOTE_ADDR"),
|
||||||
|
"user_agent": request.META.get("HTTP_USER_AGENT"),
|
||||||
|
},
|
||||||
|
"event_type": "SIGN_UP",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
except RequestException as e:
|
||||||
|
capture_exception(e)
|
||||||
|
|
||||||
user.is_active = True
|
|
||||||
user.is_email_verified = True
|
|
||||||
user.last_active = timezone.now()
|
user.last_active = timezone.now()
|
||||||
user.last_login_time = timezone.now()
|
user.last_login_time = timezone.now()
|
||||||
user.last_login_ip = request.META.get("REMOTE_ADDR")
|
user.last_login_ip = request.META.get("REMOTE_ADDR")
|
||||||
@@ -364,10 +514,8 @@ class MagicSignInEndpoint(BaseAPIView):
|
|||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
# Check if user has any accepted invites for workspace and add them to workspace
|
# Check if user has any accepted invites for workspace and add them to workspace
|
||||||
workspace_member_invites = (
|
workspace_member_invites = WorkspaceMemberInvite.objects.filter(
|
||||||
WorkspaceMemberInvite.objects.filter(
|
email=user.email, accepted=True
|
||||||
email=user.email, accepted=True
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
WorkspaceMember.objects.bulk_create(
|
WorkspaceMember.objects.bulk_create(
|
||||||
@@ -413,8 +561,7 @@ class MagicSignInEndpoint(BaseAPIView):
|
|||||||
else 15,
|
else 15,
|
||||||
member=user,
|
member=user,
|
||||||
created_by_id=project_member_invite.created_by_id,
|
created_by_id=project_member_invite.created_by_id,
|
||||||
)
|
) for project_member_invite in project_member_invites
|
||||||
for project_member_invite in project_member_invites
|
|
||||||
],
|
],
|
||||||
ignore_conflicts=True,
|
ignore_conflicts=True,
|
||||||
)
|
)
|
||||||
@@ -433,9 +580,7 @@ class MagicSignInEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{"error": "Your login code was incorrect. Please try again."},
|
||||||
"error": "Your login code was incorrect. Please try again."
|
|
||||||
},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user