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 | |||
| 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
|
||||
|
||||
exclude_patterns = [
|
||||
"bin/**",
|
||||
"**/node_modules/",
|
||||
"**/*.min.js"
|
||||
]
|
||||
|
||||
[[analyzers]]
|
||||
name = "shell"
|
||||
|
||||
|
||||
+2
-13
@@ -2,16 +2,5 @@
|
||||
*.pyc
|
||||
.env
|
||||
venv
|
||||
node_modules/
|
||||
**/node_modules/
|
||||
npm-debug.log
|
||||
.next/
|
||||
**/.next/
|
||||
.turbo/
|
||||
**/.turbo/
|
||||
build/
|
||||
**/build/
|
||||
out/
|
||||
**/out/
|
||||
dist/
|
||||
**/dist/
|
||||
node_modules
|
||||
npm-debug.log
|
||||
+6
-4
@@ -1,12 +1,14 @@
|
||||
# Database Settings
|
||||
POSTGRES_USER="plane"
|
||||
POSTGRES_PASSWORD="plane"
|
||||
POSTGRES_DB="plane"
|
||||
PGDATA="/var/lib/postgresql/data"
|
||||
PGUSER="plane"
|
||||
PGPASSWORD="plane"
|
||||
PGHOST="plane-db"
|
||||
PGDATABASE="plane"
|
||||
DATABASE_URL=postgresql://${PGUSER}:${PGPASSWORD}@${PGHOST}/${PGDATABASE}
|
||||
|
||||
# Redis Settings
|
||||
REDIS_HOST="plane-redis"
|
||||
REDIS_PORT="6379"
|
||||
REDIS_URL="redis://${REDIS_HOST}:6379/"
|
||||
|
||||
# AWS Settings
|
||||
AWS_REGION=""
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
name: Bug report
|
||||
description: Create a bug report to help us improve Plane
|
||||
title: "[bug]: "
|
||||
labels: [🐛bug]
|
||||
assignees: [srinivaspendem, pushya-plane]
|
||||
labels: [bug, need testing]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
name: Feature request
|
||||
description: Suggest a feature to improve Plane
|
||||
title: "[feature]: "
|
||||
labels: [✨feature]
|
||||
assignees: [srinivaspendem, pushya-plane]
|
||||
labels: [feature]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
||||
+117
-124
@@ -1,74 +1,120 @@
|
||||
|
||||
name: Branch Build
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch_name:
|
||||
description: "Branch Name"
|
||||
required: true
|
||||
default: "preview"
|
||||
push:
|
||||
branches:
|
||||
pull_request:
|
||||
types:
|
||||
- closed
|
||||
branches:
|
||||
- master
|
||||
- preview
|
||||
- release
|
||||
- qa
|
||||
- develop
|
||||
release:
|
||||
types: [released, prereleased]
|
||||
|
||||
env:
|
||||
TARGET_BRANCH: ${{ inputs.branch_name || github.ref_name || github.event.release.target_commitish }}
|
||||
TARGET_BRANCH: ${{ github.event.pull_request.base.ref }}
|
||||
|
||||
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
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
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:
|
||||
gh_branch_name: ${{ env.TARGET_BRANCH }}
|
||||
gh_branch_name: ${{ steps.gh_branch_clean.outputs.value }}
|
||||
|
||||
branch_build_push_frontend:
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [branch_build_setup]
|
||||
env:
|
||||
FRONTEND_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend:${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
||||
needs: [ branch_build_and_push ]
|
||||
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
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
buildkitd-flags: "--allow-insecure-entitlement security.insecure"
|
||||
uses: docker/setup-buildx-action@v2.5.0
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3.0.0
|
||||
uses: docker/login-action@v2.1.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4.1.1
|
||||
- name: Downloading Web Source Code
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: web-src-code
|
||||
|
||||
- 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:
|
||||
context: .
|
||||
file: ./web/Dockerfile.web
|
||||
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
|
||||
env:
|
||||
DOCKER_BUILDKIT: 1
|
||||
@@ -77,46 +123,28 @@ jobs:
|
||||
|
||||
branch_build_push_space:
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [branch_build_setup]
|
||||
env:
|
||||
SPACE_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-space:${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
||||
needs: [ branch_build_and_push ]
|
||||
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
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
buildkitd-flags: "--allow-insecure-entitlement security.insecure"
|
||||
uses: docker/setup-buildx-action@v2.5.0
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3.0.0
|
||||
uses: docker/login-action@v2.1.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4.1.1
|
||||
- name: Downloading Space Source Code
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: space-src-code
|
||||
|
||||
- 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:
|
||||
context: .
|
||||
file: ./space/Dockerfile.space
|
||||
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
|
||||
env:
|
||||
DOCKER_BUILDKIT: 1
|
||||
@@ -125,47 +153,29 @@ jobs:
|
||||
|
||||
branch_build_push_backend:
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [branch_build_setup]
|
||||
env:
|
||||
BACKEND_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-backend:${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
||||
needs: [ branch_build_and_push ]
|
||||
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
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
buildkitd-flags: "--allow-insecure-entitlement security.insecure"
|
||||
uses: docker/setup-buildx-action@v2.5.0
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3.0.0
|
||||
uses: docker/login-action@v2.1.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4.1.1
|
||||
- name: Downloading Backend Source Code
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: backend-src-code
|
||||
|
||||
- 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:
|
||||
context: ./apiserver
|
||||
file: ./apiserver/Dockerfile.api
|
||||
context: .
|
||||
file: ./Dockerfile.api
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
tags: ${{ env.BACKEND_TAG }}
|
||||
tags: ${{ secrets.DOCKERHUB_USERNAME }}/plane-backend-private:${{ needs.branch_build_and_push.outputs.gh_branch_name }}
|
||||
env:
|
||||
DOCKER_BUILDKIT: 1
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
@@ -173,46 +183,29 @@ jobs:
|
||||
|
||||
branch_build_push_proxy:
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [branch_build_setup]
|
||||
env:
|
||||
PROXY_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy:${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
||||
needs: [ branch_build_and_push ]
|
||||
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
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
buildkitd-flags: "--allow-insecure-entitlement security.insecure"
|
||||
uses: docker/setup-buildx-action@v2.5.0
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3.0.0
|
||||
uses: docker/login-action@v2.1.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4.1.1
|
||||
- name: Downloading Proxy Source Code
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: proxy-src-code
|
||||
|
||||
- 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:
|
||||
context: ./nginx
|
||||
file: ./nginx/Dockerfile
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
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
|
||||
env:
|
||||
DOCKER_BUILDKIT: 1
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name: Build Pull Request Contents
|
||||
|
||||
on:
|
||||
on:
|
||||
pull_request:
|
||||
types: ["opened", "synchronize"]
|
||||
|
||||
@@ -14,18 +14,16 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout Repository to Actions
|
||||
uses: actions/checkout@v3.3.0
|
||||
with:
|
||||
token: ${{ secrets.ACCESS_TOKEN }}
|
||||
|
||||
- name: Setup Node.js 18.x
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 18.x
|
||||
cache: "yarn"
|
||||
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v41
|
||||
uses: tj-actions/changed-files@v38
|
||||
with:
|
||||
files_yaml: |
|
||||
apiserver:
|
||||
@@ -46,3 +44,5 @@ jobs:
|
||||
run: |
|
||||
yarn
|
||||
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:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
pull_request:
|
||||
branches:
|
||||
- preview
|
||||
|
||||
env:
|
||||
SOURCE_BRANCH_NAME: ${{ github.ref_name }}
|
||||
- master
|
||||
types:
|
||||
- closed
|
||||
|
||||
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
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: read
|
||||
steps:
|
||||
- name: Check SOURCE_REPO
|
||||
id: check_repo
|
||||
env:
|
||||
SOURCE_REPO: ${{ secrets.SOURCE_REPO_NAME }}
|
||||
run: |
|
||||
echo "::set-output name=is_correct_repo::$(if [[ "$SOURCE_REPO" == "makeplane/plane" ]]; then echo 'true'; else echo 'false'; fi)"
|
||||
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4.1.1
|
||||
if: steps.check_repo.outputs.is_correct_repo == 'true'
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Branch Name
|
||||
if: steps.check_repo.outputs.is_correct_repo == 'true'
|
||||
run: |
|
||||
echo "SOURCE_BRANCH_NAME=${{ github.head_ref }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup GH CLI
|
||||
if: steps.check_repo.outputs.is_correct_repo == 'true'
|
||||
run: |
|
||||
type -p curl >/dev/null || (sudo apt update && sudo apt install curl -y)
|
||||
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
|
||||
@@ -31,14 +45,35 @@ jobs:
|
||||
sudo apt update
|
||||
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:
|
||||
GH_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||
run: |
|
||||
TARGET_REPO="${{ secrets.SYNC_TARGET_REPO_NAME }}"
|
||||
TARGET_BRANCH="${{ secrets.SYNC_TARGET_BRANCH_NAME }}"
|
||||
TARGET_REPO="${{ secrets.TARGET_REPO_NAME }}"
|
||||
TARGET_BRANCH="${{ secrets.TARGET_REPO_BRANCH }}"
|
||||
SOURCE_BRANCH="${{ env.SOURCE_BRANCH_NAME }}"
|
||||
|
||||
git checkout $SOURCE_BRANCH
|
||||
git remote add target-origin "https://$GH_TOKEN@github.com/$TARGET_REPO.git"
|
||||
git push target-origin $SOURCE_BRANCH:$TARGET_BRANCH
|
||||
git remote add target "https://$GH_TOKEN@github.com/$TARGET_REPO.git"
|
||||
git push target $SOURCE_BRANCH:$SOURCE_BRANCH
|
||||
|
||||
PR_TITLE="${{ github.event.pull_request.title }}"
|
||||
PR_BODY="${{ github.event.pull_request.body }}"
|
||||
|
||||
# Remove double quotes
|
||||
PR_TITLE_CLEANED="${PR_TITLE//\"/}"
|
||||
PR_BODY_CLEANED="${PR_BODY//\"/}"
|
||||
|
||||
# Construct PR_BODY_CONTENT using a here-document
|
||||
PR_BODY_CONTENT=$(cat <<EOF
|
||||
$PR_BODY_CLEANED
|
||||
EOF
|
||||
)
|
||||
|
||||
gh pr create \
|
||||
--base $TARGET_BRANCH \
|
||||
--head $SOURCE_BRANCH \
|
||||
--title "[SYNC] $PR_TITLE_CLEANED" \
|
||||
--body "$PR_BODY_CONTENT" \
|
||||
--repo $TARGET_REPO
|
||||
|
||||
@@ -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
|
||||
|
||||
```bash
|
||||
git clone https://github.com/makeplane/plane.git [folder-name]
|
||||
cd [folder-name]
|
||||
git clone https://github.com/makeplane/plane
|
||||
cd plane
|
||||
chmod +x setup.sh
|
||||
```
|
||||
|
||||
@@ -44,12 +44,33 @@ chmod +x 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
|
||||
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?
|
||||
|
||||
|
||||
@@ -79,6 +79,7 @@ COPY apiserver/manage.py manage.py
|
||||
COPY apiserver/plane plane/
|
||||
COPY apiserver/templates templates/
|
||||
|
||||
COPY apiserver/gunicorn.config.py ./
|
||||
RUN apk --no-cache add "bash~=5.2"
|
||||
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
|
||||
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
|
||||
|
||||
@@ -62,6 +76,7 @@ NEXT_PUBLIC_DEPLOY_URL="http://localhost/spaces"
|
||||
# Backend
|
||||
# Debug value for api server use it as 0 for production use
|
||||
DEBUG=0
|
||||
DJANGO_SETTINGS_MODULE="plane.settings.selfhosted" # deprecated
|
||||
|
||||
# Error logs
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
@@ -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. 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)
|
||||
|
||||
Thats it!
|
||||
|
||||
## 🍙 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
|
||||
|
||||
|
||||
+9
-10
@@ -8,16 +8,11 @@ SENTRY_DSN=""
|
||||
SENTRY_ENVIRONMENT="development"
|
||||
|
||||
# Database Settings
|
||||
POSTGRES_USER="plane"
|
||||
POSTGRES_PASSWORD="plane"
|
||||
POSTGRES_HOST="plane-db"
|
||||
POSTGRES_DB="plane"
|
||||
DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}/${POSTGRES_DB}
|
||||
|
||||
# Oauth variables
|
||||
GOOGLE_CLIENT_ID=""
|
||||
GITHUB_CLIENT_ID=""
|
||||
GITHUB_CLIENT_SECRET=""
|
||||
PGUSER="plane"
|
||||
PGPASSWORD="plane"
|
||||
PGHOST="plane-db"
|
||||
PGDATABASE="plane"
|
||||
DATABASE_URL=postgresql://${PGUSER}:${PGPASSWORD}@${PGHOST}/${PGDATABASE}
|
||||
|
||||
# Redis Settings
|
||||
REDIS_HOST="plane-redis"
|
||||
@@ -39,6 +34,9 @@ OPENAI_API_BASE="https://api.openai.com/v1" # deprecated
|
||||
OPENAI_API_KEY="sk-" # deprecated
|
||||
GPT_ENGINE="gpt-3.5-turbo" # deprecated
|
||||
|
||||
# Github
|
||||
GITHUB_CLIENT_SECRET="" # For fetching release notes
|
||||
|
||||
# Settings related to Docker
|
||||
DOCKERIZED=1 # deprecated
|
||||
|
||||
@@ -52,6 +50,7 @@ NGINX_PORT=80
|
||||
# SignUps
|
||||
ENABLE_SIGNUP="1"
|
||||
|
||||
|
||||
# Enable Email/Password Signup
|
||||
ENABLE_EMAIL_PASSWORD="1"
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ COPY manage.py manage.py
|
||||
COPY plane plane/
|
||||
COPY templates templates/
|
||||
COPY package.json package.json
|
||||
COPY gunicorn.config.py ./
|
||||
USER root
|
||||
RUN apk --no-cache add "bash~=5.2"
|
||||
COPY ./bin ./bin/
|
||||
|
||||
@@ -27,16 +27,20 @@ WORKDIR /code
|
||||
COPY requirements.txt ./requirements.txt
|
||||
ADD requirements ./requirements
|
||||
|
||||
# Install the local development settings
|
||||
RUN pip install -r requirements/local.txt --compile --no-cache-dir
|
||||
RUN pip install -r requirements.txt --compile --no-cache-dir
|
||||
|
||||
RUN addgroup -S plane && \
|
||||
adduser -S captain -G plane
|
||||
|
||||
COPY . .
|
||||
RUN chown captain.plane /code
|
||||
|
||||
RUN chown -R captain.plane /code
|
||||
RUN chmod -R +x /code/bin
|
||||
USER captain
|
||||
|
||||
# Add in Django deps and generate Django's static files
|
||||
|
||||
USER root
|
||||
|
||||
# RUN chmod +x ./bin/takeoff ./bin/worker ./bin/beat
|
||||
RUN chmod -R 777 /code
|
||||
|
||||
USER captain
|
||||
@@ -44,5 +48,5 @@ USER captain
|
||||
# Expose container port and run entry point script
|
||||
EXPOSE 8000
|
||||
|
||||
CMD [ "./bin/takeoff.local" ]
|
||||
# CMD [ "./bin/takeoff" ]
|
||||
|
||||
|
||||
@@ -26,9 +26,7 @@ def update_description():
|
||||
updated_issues.append(issue)
|
||||
|
||||
Issue.objects.bulk_update(
|
||||
updated_issues,
|
||||
["description_html", "description_stripped"],
|
||||
batch_size=100,
|
||||
updated_issues, ["description_html", "description_stripped"], batch_size=100
|
||||
)
|
||||
print("Success")
|
||||
except Exception as e:
|
||||
@@ -42,9 +40,7 @@ def update_comments():
|
||||
updated_issue_comments = []
|
||||
|
||||
for issue_comment in issue_comments:
|
||||
issue_comment.comment_html = (
|
||||
f"<p>{issue_comment.comment_stripped}</p>"
|
||||
)
|
||||
issue_comment.comment_html = f"<p>{issue_comment.comment_stripped}</p>"
|
||||
updated_issue_comments.append(issue_comment)
|
||||
|
||||
IssueComment.objects.bulk_update(
|
||||
@@ -103,9 +99,7 @@ def updated_issue_sort_order():
|
||||
issue.sort_order = issue.sequence_id * random.randint(100, 500)
|
||||
updated_issues.append(issue)
|
||||
|
||||
Issue.objects.bulk_update(
|
||||
updated_issues, ["sort_order"], batch_size=100
|
||||
)
|
||||
Issue.objects.bulk_update(updated_issues, ["sort_order"], batch_size=100)
|
||||
print("Success")
|
||||
except Exception as e:
|
||||
print(e)
|
||||
@@ -143,9 +137,7 @@ def update_project_cover_images():
|
||||
project.cover_image = project_cover_images[random.randint(0, 19)]
|
||||
updated_projects.append(project)
|
||||
|
||||
Project.objects.bulk_update(
|
||||
updated_projects, ["cover_image"], batch_size=100
|
||||
)
|
||||
Project.objects.bulk_update(updated_projects, ["cover_image"], batch_size=100)
|
||||
print("Success")
|
||||
except Exception as e:
|
||||
print(e)
|
||||
@@ -194,9 +186,7 @@ def update_label_color():
|
||||
|
||||
def create_slack_integration():
|
||||
try:
|
||||
_ = Integration.objects.create(
|
||||
provider="slack", network=2, title="Slack"
|
||||
)
|
||||
_ = Integration.objects.create(provider="slack", network=2, title="Slack")
|
||||
print("Success")
|
||||
except Exception as e:
|
||||
print(e)
|
||||
@@ -222,16 +212,12 @@ def update_integration_verified():
|
||||
|
||||
def update_start_date():
|
||||
try:
|
||||
issues = Issue.objects.filter(
|
||||
state__group__in=["started", "completed"]
|
||||
)
|
||||
issues = Issue.objects.filter(state__group__in=["started", "completed"])
|
||||
updated_issues = []
|
||||
for issue in issues:
|
||||
issue.start_date = issue.created_at.date()
|
||||
updated_issues.append(issue)
|
||||
Issue.objects.bulk_update(
|
||||
updated_issues, ["start_date"], batch_size=500
|
||||
)
|
||||
Issue.objects.bulk_update(updated_issues, ["start_date"], batch_size=500)
|
||||
print("Success")
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
Executable → Regular
-3
@@ -2,7 +2,4 @@
|
||||
set -e
|
||||
|
||||
python manage.py wait_for_db
|
||||
# Wait for migrations
|
||||
python manage.py wait_for_migrations
|
||||
# Run the processes
|
||||
celery -A plane beat -l info
|
||||
+11
-22
@@ -1,31 +1,20 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
python manage.py wait_for_db
|
||||
# Wait for migrations
|
||||
python manage.py wait_for_migrations
|
||||
python manage.py migrate
|
||||
|
||||
# Create the default bucket
|
||||
#!/bin/bash
|
||||
# Set default value for ENABLE_REGISTRATION
|
||||
ENABLE_REGISTRATION=${ENABLE_REGISTRATION:-1}
|
||||
|
||||
# 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
|
||||
# Check if ENABLE_REGISTRATION is not set to '0'
|
||||
if [ "$ENABLE_REGISTRATION" != "0" ]; then
|
||||
# Register instance
|
||||
python manage.py register_instance
|
||||
# Load the configuration variable
|
||||
python manage.py configure_instance
|
||||
fi
|
||||
|
||||
# Create the default bucket
|
||||
python manage.py create_bucket
|
||||
|
||||
exec gunicorn -w $GUNICORN_WORKERS -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:${PORT:-8000} --max-requests 1200 --max-requests-jitter 1000 --access-logfile -
|
||||
exec gunicorn -w $GUNICORN_WORKERS -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:8000 --max-requests 1200 --max-requests-jitter 1000 --access-logfile -
|
||||
|
||||
@@ -1,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
|
||||
|
||||
python manage.py wait_for_db
|
||||
# Wait for migrations
|
||||
python manage.py wait_for_migrations
|
||||
# Run the processes
|
||||
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 sys
|
||||
|
||||
if __name__ == "__main__":
|
||||
if __name__ == '__main__':
|
||||
os.environ.setdefault(
|
||||
"DJANGO_SETTINGS_MODULE", "plane.settings.production"
|
||||
)
|
||||
'DJANGO_SETTINGS_MODULE',
|
||||
'plane.settings.production')
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"name": "plane-api",
|
||||
"version": "0.15.0"
|
||||
}
|
||||
"name": "plane-api",
|
||||
"version": "0.13.2"
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
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):
|
||||
name = "plane.analytics"
|
||||
name = 'plane.analytics'
|
||||
|
||||
@@ -2,4 +2,4 @@ from django.apps import AppConfig
|
||||
|
||||
|
||||
class ApiConfig(AppConfig):
|
||||
name = "plane.api"
|
||||
name = "plane.api"
|
||||
@@ -25,10 +25,7 @@ class APIKeyAuthentication(authentication.BaseAuthentication):
|
||||
def validate_api_token(self, token):
|
||||
try:
|
||||
api_token = APIToken.objects.get(
|
||||
Q(
|
||||
Q(expired_at__gt=timezone.now())
|
||||
| Q(expired_at__isnull=True)
|
||||
),
|
||||
Q(Q(expired_at__gt=timezone.now()) | Q(expired_at__isnull=True)),
|
||||
token=token,
|
||||
is_active=True,
|
||||
)
|
||||
@@ -47,4 +44,4 @@ class APIKeyAuthentication(authentication.BaseAuthentication):
|
||||
|
||||
# Validate the API 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
|
||||
|
||||
|
||||
class ApiKeyRateThrottle(SimpleRateThrottle):
|
||||
scope = "api_key"
|
||||
rate = "60/minute"
|
||||
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")
|
||||
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}"
|
||||
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:
|
||||
now = self.timer()
|
||||
# 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)
|
||||
|
||||
@@ -36,7 +39,7 @@ class ApiKeyRateThrottle(SimpleRateThrottle):
|
||||
reset_time = int(now + self.duration)
|
||||
|
||||
# Add headers
|
||||
request.META["X-RateLimit-Remaining"] = max(0, available)
|
||||
request.META["X-RateLimit-Reset"] = reset_time
|
||||
request.META['X-RateLimit-Remaining'] = max(0, available)
|
||||
request.META['X-RateLimit-Reset'] = reset_time
|
||||
|
||||
return allowed
|
||||
return allowed
|
||||
@@ -9,13 +9,8 @@ from .issue import (
|
||||
IssueCommentSerializer,
|
||||
IssueAttachmentSerializer,
|
||||
IssueActivitySerializer,
|
||||
IssueExpandSerializer,
|
||||
)
|
||||
from .state import StateLiteSerializer, StateSerializer
|
||||
from .cycle import CycleSerializer, CycleIssueSerializer, CycleLiteSerializer
|
||||
from .module import (
|
||||
ModuleSerializer,
|
||||
ModuleIssueSerializer,
|
||||
ModuleLiteSerializer,
|
||||
)
|
||||
from .inbox import InboxIssueSerializer
|
||||
from .cycle import CycleSerializer, CycleIssueSerializer
|
||||
from .module import ModuleSerializer, ModuleIssueSerializer
|
||||
from .inbox import InboxIssueSerializer
|
||||
@@ -97,11 +97,9 @@ class BaseSerializer(serializers.ModelSerializer):
|
||||
exp_serializer = expansion[expand](
|
||||
getattr(instance, expand)
|
||||
)
|
||||
response[expand] = exp_serializer.data
|
||||
response[expand] = exp_serializer.data
|
||||
else:
|
||||
# You might need to handle this case differently
|
||||
response[expand] = getattr(
|
||||
instance, f"{expand}_id", None
|
||||
)
|
||||
response[expand] = getattr(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("start_date", None) > data.get("end_date", None)
|
||||
):
|
||||
raise serializers.ValidationError(
|
||||
"Start date cannot exceed end date"
|
||||
)
|
||||
raise serializers.ValidationError("Start date cannot exceed end date")
|
||||
return data
|
||||
|
||||
class Meta:
|
||||
model = Cycle
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"id",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"workspace",
|
||||
"project",
|
||||
"owned_by",
|
||||
@@ -53,10 +46,4 @@ class CycleIssueSerializer(BaseSerializer):
|
||||
"workspace",
|
||||
"project",
|
||||
"cycle",
|
||||
]
|
||||
|
||||
|
||||
class CycleLiteSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = Cycle
|
||||
fields = "__all__"
|
||||
]
|
||||
@@ -2,18 +2,12 @@
|
||||
from .base import BaseSerializer
|
||||
from plane.db.models import InboxIssue
|
||||
|
||||
|
||||
class InboxIssueSerializer(BaseSerializer):
|
||||
|
||||
class Meta:
|
||||
model = InboxIssue
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"id",
|
||||
"workspace",
|
||||
"project",
|
||||
"issue",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
"workspace",
|
||||
]
|
||||
@@ -1,6 +1,3 @@
|
||||
from lxml import html
|
||||
|
||||
|
||||
# Django imports
|
||||
from django.utils import timezone
|
||||
|
||||
@@ -22,10 +19,6 @@ from plane.db.models import (
|
||||
ProjectMember,
|
||||
)
|
||||
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):
|
||||
@@ -47,8 +40,8 @@ class IssueSerializer(BaseSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Issue
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"id",
|
||||
"workspace",
|
||||
"project",
|
||||
"created_by",
|
||||
@@ -56,10 +49,6 @@ class IssueSerializer(BaseSerializer):
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
exclude = [
|
||||
"description",
|
||||
"description_stripped",
|
||||
]
|
||||
|
||||
def validate(self, data):
|
||||
if (
|
||||
@@ -67,24 +56,13 @@ class IssueSerializer(BaseSerializer):
|
||||
and data.get("target_date", None) is not None
|
||||
and data.get("start_date", None) > data.get("target_date", None)
|
||||
):
|
||||
raise serializers.ValidationError(
|
||||
"Start date cannot exceed target date"
|
||||
)
|
||||
|
||||
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)}")
|
||||
raise serializers.ValidationError("Start date cannot exceed target date")
|
||||
|
||||
# Validate assignees are from project
|
||||
if data.get("assignees", []):
|
||||
print(data.get("assignees"))
|
||||
data["assignees"] = ProjectMember.objects.filter(
|
||||
project_id=self.context.get("project_id"),
|
||||
is_active=True,
|
||||
member_id__in=data["assignees"],
|
||||
).values_list("member_id", flat=True)
|
||||
|
||||
@@ -99,8 +77,7 @@ class IssueSerializer(BaseSerializer):
|
||||
if (
|
||||
data.get("state")
|
||||
and not State.objects.filter(
|
||||
project_id=self.context.get("project_id"),
|
||||
pk=data.get("state").id,
|
||||
project_id=self.context.get("project_id"), pk=data.get("state")
|
||||
).exists()
|
||||
):
|
||||
raise serializers.ValidationError(
|
||||
@@ -111,8 +88,7 @@ class IssueSerializer(BaseSerializer):
|
||||
if (
|
||||
data.get("parent")
|
||||
and not Issue.objects.filter(
|
||||
workspace_id=self.context.get("workspace_id"),
|
||||
pk=data.get("parent").id,
|
||||
workspce_id=self.context.get("workspace_id"), pk=data.get("parent")
|
||||
).exists()
|
||||
):
|
||||
raise serializers.ValidationError(
|
||||
@@ -243,13 +219,9 @@ class IssueSerializer(BaseSerializer):
|
||||
]
|
||||
if "labels" in self.fields:
|
||||
if "labels" in self.expand:
|
||||
data["labels"] = LabelSerializer(
|
||||
instance.labels.all(), many=True
|
||||
).data
|
||||
data["labels"] = LabelSerializer(instance.labels.all(), many=True).data
|
||||
else:
|
||||
data["labels"] = [
|
||||
str(label.id) for label in instance.labels.all()
|
||||
]
|
||||
data["labels"] = [str(label.id) for label in instance.labels.all()]
|
||||
|
||||
return data
|
||||
|
||||
@@ -259,13 +231,8 @@ class LabelSerializer(BaseSerializer):
|
||||
model = Label
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"id",
|
||||
"workspace",
|
||||
"project",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
|
||||
|
||||
@@ -274,21 +241,19 @@ class IssueLinkSerializer(BaseSerializer):
|
||||
model = IssueLink
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"id",
|
||||
"workspace",
|
||||
"project",
|
||||
"issue",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"issue",
|
||||
]
|
||||
|
||||
# Validation if url already exists
|
||||
def create(self, validated_data):
|
||||
if IssueLink.objects.filter(
|
||||
url=validated_data.get("url"),
|
||||
issue_id=validated_data.get("issue_id"),
|
||||
url=validated_data.get("url"), issue_id=validated_data.get("issue_id")
|
||||
).exists():
|
||||
raise serializers.ValidationError(
|
||||
{"error": "URL already exists for this Issue"}
|
||||
@@ -301,14 +266,13 @@ class IssueAttachmentSerializer(BaseSerializer):
|
||||
model = IssueAttachment
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"id",
|
||||
"workspace",
|
||||
"project",
|
||||
"issue",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"workspace",
|
||||
"project",
|
||||
"issue",
|
||||
]
|
||||
|
||||
|
||||
@@ -317,8 +281,8 @@ class IssueCommentSerializer(BaseSerializer):
|
||||
|
||||
class Meta:
|
||||
model = IssueComment
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"id",
|
||||
"workspace",
|
||||
"project",
|
||||
"issue",
|
||||
@@ -327,76 +291,29 @@ class IssueCommentSerializer(BaseSerializer):
|
||||
"created_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 Meta:
|
||||
model = IssueActivity
|
||||
fields = "__all__"
|
||||
exclude = [
|
||||
"created_by",
|
||||
"updated_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",
|
||||
"udpated_by",
|
||||
]
|
||||
|
||||
@@ -21,6 +21,7 @@ class ModuleSerializer(BaseSerializer):
|
||||
write_only=True,
|
||||
required=False,
|
||||
)
|
||||
is_favorite = serializers.BooleanField(read_only=True)
|
||||
total_issues = serializers.IntegerField(read_only=True)
|
||||
cancelled_issues = serializers.IntegerField(read_only=True)
|
||||
completed_issues = serializers.IntegerField(read_only=True)
|
||||
@@ -32,7 +33,6 @@ class ModuleSerializer(BaseSerializer):
|
||||
model = Module
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"id",
|
||||
"workspace",
|
||||
"project",
|
||||
"created_by",
|
||||
@@ -52,11 +52,10 @@ class ModuleSerializer(BaseSerializer):
|
||||
and data.get("target_date", None) is not None
|
||||
and data.get("start_date", None) > data.get("target_date", None)
|
||||
):
|
||||
raise serializers.ValidationError(
|
||||
"Start date cannot exceed target date"
|
||||
)
|
||||
raise serializers.ValidationError("Start date cannot exceed target date")
|
||||
|
||||
if data.get("members", []):
|
||||
print(data.get("members"))
|
||||
data["members"] = ProjectMember.objects.filter(
|
||||
project_id=self.context.get("project_id"),
|
||||
member_id__in=data["members"],
|
||||
@@ -67,18 +66,18 @@ class ModuleSerializer(BaseSerializer):
|
||||
def create(self, validated_data):
|
||||
members = validated_data.pop("members", None)
|
||||
|
||||
project_id = self.context["project_id"]
|
||||
workspace_id = self.context["workspace_id"]
|
||||
project = self.context["project"]
|
||||
|
||||
module = Module.objects.create(**validated_data, project=project)
|
||||
|
||||
module = Module.objects.create(**validated_data, project_id=project_id)
|
||||
if members is not None:
|
||||
ModuleMember.objects.bulk_create(
|
||||
[
|
||||
ModuleMember(
|
||||
module=module,
|
||||
member_id=str(member),
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
member=member,
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
created_by=module.created_by,
|
||||
updated_by=module.updated_by,
|
||||
)
|
||||
@@ -99,7 +98,7 @@ class ModuleSerializer(BaseSerializer):
|
||||
[
|
||||
ModuleMember(
|
||||
module=instance,
|
||||
member_id=str(member),
|
||||
member=member,
|
||||
project=instance.project,
|
||||
workspace=instance.project.workspace,
|
||||
created_by=instance.created_by,
|
||||
@@ -148,16 +147,9 @@ class ModuleLinkSerializer(BaseSerializer):
|
||||
# Validation if url already exists
|
||||
def create(self, validated_data):
|
||||
if ModuleLink.objects.filter(
|
||||
url=validated_data.get("url"),
|
||||
module_id=validated_data.get("module_id"),
|
||||
url=validated_data.get("url"), module_id=validated_data.get("module_id")
|
||||
).exists():
|
||||
raise serializers.ValidationError(
|
||||
{"error": "URL already exists for this Issue"}
|
||||
)
|
||||
return ModuleLink.objects.create(**validated_data)
|
||||
|
||||
|
||||
class ModuleLiteSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = Module
|
||||
fields = "__all__"
|
||||
return ModuleLink.objects.create(**validated_data)
|
||||
@@ -2,17 +2,12 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import (
|
||||
Project,
|
||||
ProjectIdentifier,
|
||||
WorkspaceMember,
|
||||
State,
|
||||
Estimate,
|
||||
)
|
||||
from plane.db.models import Project, ProjectIdentifier, WorkspaceMember, State, Estimate
|
||||
from .base import BaseSerializer
|
||||
|
||||
|
||||
class ProjectSerializer(BaseSerializer):
|
||||
|
||||
total_members = serializers.IntegerField(read_only=True)
|
||||
total_cycles = serializers.IntegerField(read_only=True)
|
||||
total_modules = serializers.IntegerField(read_only=True)
|
||||
@@ -25,13 +20,8 @@ class ProjectSerializer(BaseSerializer):
|
||||
model = Project
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"id",
|
||||
"emoji",
|
||||
"workspace",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"id",
|
||||
]
|
||||
|
||||
def validate(self, data):
|
||||
@@ -64,16 +54,12 @@ class ProjectSerializer(BaseSerializer):
|
||||
def create(self, validated_data):
|
||||
identifier = validated_data.get("identifier", "").strip().upper()
|
||||
if identifier == "":
|
||||
raise serializers.ValidationError(
|
||||
detail="Project Identifier is required"
|
||||
)
|
||||
raise serializers.ValidationError(detail="Project Identifier is required")
|
||||
|
||||
if ProjectIdentifier.objects.filter(
|
||||
name=identifier, workspace_id=self.context["workspace_id"]
|
||||
).exists():
|
||||
raise serializers.ValidationError(
|
||||
detail="Project Identifier is taken"
|
||||
)
|
||||
raise serializers.ValidationError(detail="Project Identifier is taken")
|
||||
|
||||
project = Project.objects.create(
|
||||
**validated_data, workspace_id=self.context["workspace_id"]
|
||||
@@ -98,4 +84,4 @@ class ProjectLiteSerializer(BaseSerializer):
|
||||
"emoji",
|
||||
"description",
|
||||
]
|
||||
read_only_fields = fields
|
||||
read_only_fields = fields
|
||||
@@ -7,20 +7,15 @@ class StateSerializer(BaseSerializer):
|
||||
def validate(self, data):
|
||||
# If the default is being provided then make all other states default False
|
||||
if data.get("default", False):
|
||||
State.objects.filter(
|
||||
project_id=self.context.get("project_id")
|
||||
).update(default=False)
|
||||
State.objects.filter(project_id=self.context.get("project_id")).update(
|
||||
default=False
|
||||
)
|
||||
return data
|
||||
|
||||
class Meta:
|
||||
model = State
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"id",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"workspace",
|
||||
"project",
|
||||
]
|
||||
@@ -35,4 +30,4 @@ class StateLiteSerializer(BaseSerializer):
|
||||
"color",
|
||||
"group",
|
||||
]
|
||||
read_only_fields = fields
|
||||
read_only_fields = fields
|
||||
@@ -11,6 +11,10 @@ class UserLiteSerializer(BaseSerializer):
|
||||
"first_name",
|
||||
"last_name",
|
||||
"avatar",
|
||||
"is_bot",
|
||||
"display_name",
|
||||
]
|
||||
read_only_fields = fields
|
||||
read_only_fields = [
|
||||
"id",
|
||||
"is_bot",
|
||||
]
|
||||
@@ -5,7 +5,6 @@ from .base import BaseSerializer
|
||||
|
||||
class WorkspaceLiteSerializer(BaseSerializer):
|
||||
"""Lite serializer with only required fields"""
|
||||
|
||||
class Meta:
|
||||
model = Workspace
|
||||
fields = [
|
||||
@@ -13,4 +12,4 @@ class WorkspaceLiteSerializer(BaseSerializer):
|
||||
"slug",
|
||||
"id",
|
||||
]
|
||||
read_only_fields = fields
|
||||
read_only_fields = fields
|
||||
@@ -12,4 +12,4 @@ urlpatterns = [
|
||||
*cycle_patterns,
|
||||
*module_patterns,
|
||||
*inbox_patterns,
|
||||
]
|
||||
]
|
||||
@@ -13,7 +13,7 @@ urlpatterns = [
|
||||
name="cycles",
|
||||
),
|
||||
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(),
|
||||
name="cycles",
|
||||
),
|
||||
@@ -23,7 +23,7 @@ urlpatterns = [
|
||||
name="cycle-issues",
|
||||
),
|
||||
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(),
|
||||
name="cycle-issues",
|
||||
),
|
||||
@@ -32,4 +32,4 @@ urlpatterns = [
|
||||
TransferCycleIssueAPIEndpoint.as_view(),
|
||||
name="transfer-issues",
|
||||
),
|
||||
]
|
||||
]
|
||||
@@ -5,13 +5,13 @@ from plane.api.views import InboxIssueAPIEndpoint
|
||||
|
||||
urlpatterns = [
|
||||
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(),
|
||||
name="inbox-issue",
|
||||
),
|
||||
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(),
|
||||
name="inbox-issue",
|
||||
),
|
||||
]
|
||||
]
|
||||
@@ -15,27 +15,27 @@ urlpatterns = [
|
||||
name="issue",
|
||||
),
|
||||
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(),
|
||||
name="issue",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/labels/",
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-labels/",
|
||||
LabelAPIEndpoint.as_view(),
|
||||
name="label",
|
||||
),
|
||||
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(),
|
||||
name="label",
|
||||
),
|
||||
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(),
|
||||
name="link",
|
||||
),
|
||||
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(),
|
||||
name="link",
|
||||
),
|
||||
@@ -50,12 +50,12 @@ urlpatterns = [
|
||||
name="comment",
|
||||
),
|
||||
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(),
|
||||
name="activity",
|
||||
),
|
||||
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(),
|
||||
name="activity",
|
||||
),
|
||||
|
||||
@@ -19,8 +19,8 @@ urlpatterns = [
|
||||
name="module-issues",
|
||||
),
|
||||
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(),
|
||||
name="module-issues",
|
||||
),
|
||||
]
|
||||
]
|
||||
@@ -3,14 +3,14 @@ from django.urls import path
|
||||
from plane.api.views import ProjectAPIEndpoint
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/",
|
||||
ProjectAPIEndpoint.as_view(),
|
||||
name="project",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/",
|
||||
"workspaces/<str:slug>/projects/<uuid:pk>/",
|
||||
ProjectAPIEndpoint.as_view(),
|
||||
name="project",
|
||||
),
|
||||
]
|
||||
]
|
||||
@@ -8,9 +8,4 @@ urlpatterns = [
|
||||
StateAPIEndpoint.as_view(),
|
||||
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 .inbox import InboxIssueAPIEndpoint
|
||||
from .inbox import InboxIssueAPIEndpoint
|
||||
@@ -7,6 +7,7 @@ from django.conf import settings
|
||||
from django.db import IntegrityError
|
||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
from django.utils import timezone
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
|
||||
# Third party imports
|
||||
from rest_framework.views import APIView
|
||||
@@ -35,35 +36,28 @@ class TimezoneMixin:
|
||||
else:
|
||||
timezone.deactivate()
|
||||
|
||||
|
||||
class WebhookMixin:
|
||||
webhook_event = None
|
||||
bulk = False
|
||||
|
||||
def finalize_response(self, request, response, *args, **kwargs):
|
||||
response = super().finalize_response(
|
||||
request, response, *args, **kwargs
|
||||
)
|
||||
response = super().finalize_response(request, response, *args, **kwargs)
|
||||
|
||||
# Check for the case should webhook be sent
|
||||
if (
|
||||
self.webhook_event
|
||||
and self.request.method in ["POST", "PATCH", "DELETE"]
|
||||
and response.status_code in [200, 201, 204]
|
||||
):
|
||||
# Push the object to delay
|
||||
send_webhook.delay(
|
||||
event=self.webhook_event,
|
||||
payload=response.data,
|
||||
kw=self.kwargs,
|
||||
event_data=json.dumps(response.data, cls=DjangoJSONEncoder),
|
||||
action=self.request.method,
|
||||
slug=self.workspace_slug,
|
||||
bulk=self.bulk,
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
|
||||
class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
|
||||
authentication_classes = [
|
||||
APIKeyAuthentication,
|
||||
@@ -106,14 +100,15 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
|
||||
)
|
||||
|
||||
if isinstance(e, ObjectDoesNotExist):
|
||||
model_name = str(exc).split(" matching query does not exist.")[0]
|
||||
return Response(
|
||||
{"error": f"The required object does not exist."},
|
||||
{"error": f"{model_name} does not exist."},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
|
||||
if isinstance(e, KeyError):
|
||||
return Response(
|
||||
{"error": f" The required key does not exist."},
|
||||
{"error": f"key {e} does not exist"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@@ -141,18 +136,16 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
|
||||
|
||||
def finalize_response(self, request, response, *args, **kwargs):
|
||||
# Call super to get the default response
|
||||
response = super().finalize_response(
|
||||
request, response, *args, **kwargs
|
||||
)
|
||||
response = super().finalize_response(request, response, *args, **kwargs)
|
||||
|
||||
# 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:
|
||||
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:
|
||||
response["X-RateLimit-Reset"] = ratelimit_reset
|
||||
response['X-RateLimit-Reset'] = ratelimit_reset
|
||||
|
||||
return response
|
||||
|
||||
@@ -167,17 +160,13 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
|
||||
@property
|
||||
def fields(self):
|
||||
fields = [
|
||||
field
|
||||
for field in self.request.GET.get("fields", "").split(",")
|
||||
if field
|
||||
field for field in self.request.GET.get("fields", "").split(",") if field
|
||||
]
|
||||
return fields if fields else None
|
||||
|
||||
@property
|
||||
def expand(self):
|
||||
expand = [
|
||||
expand
|
||||
for expand in self.request.GET.get("expand", "").split(",")
|
||||
if expand
|
||||
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
|
||||
from .base import BaseAPIView, WebhookMixin
|
||||
from plane.db.models import (
|
||||
Cycle,
|
||||
Issue,
|
||||
CycleIssue,
|
||||
IssueLink,
|
||||
IssueAttachment,
|
||||
)
|
||||
from plane.db.models import Cycle, Issue, CycleIssue, IssueLink, IssueAttachment
|
||||
from plane.app.permissions import ProjectEntityPermission
|
||||
from plane.api.serializers import (
|
||||
CycleSerializer,
|
||||
CycleIssueSerializer,
|
||||
IssueSerializer,
|
||||
)
|
||||
from plane.bgtasks.issue_activites_task import issue_activity
|
||||
|
||||
@@ -108,9 +103,7 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
total_estimates=Sum("issue_cycle__issue__estimate_point")
|
||||
)
|
||||
.annotate(total_estimates=Sum("issue_cycle__issue__estimate_point"))
|
||||
.annotate(
|
||||
completed_estimates=Sum(
|
||||
"issue_cycle__issue__estimate_point",
|
||||
@@ -149,6 +142,7 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
)
|
||||
queryset = self.get_queryset()
|
||||
cycle_view = request.GET.get("cycle_view", "all")
|
||||
queryset = queryset.order_by("-is_favorite", "-created_at")
|
||||
|
||||
# Current Cycle
|
||||
if cycle_view == "current":
|
||||
@@ -209,8 +203,7 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
# Incomplete Cycles
|
||||
if cycle_view == "incomplete":
|
||||
queryset = queryset.filter(
|
||||
Q(end_date__gte=timezone.now().date())
|
||||
| Q(end_date__isnull=True),
|
||||
Q(end_date__gte=timezone.now().date()) | Q(end_date__isnull=True),
|
||||
)
|
||||
return self.paginate(
|
||||
request=request,
|
||||
@@ -243,39 +236,12 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
):
|
||||
serializer = CycleSerializer(data=request.data)
|
||||
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",
|
||||
"cycle": str(cycle.id),
|
||||
},
|
||||
status=status.HTTP_409_CONFLICT,
|
||||
)
|
||||
serializer.save(
|
||||
project_id=project_id,
|
||||
owned_by=request.user,
|
||||
)
|
||||
return Response(
|
||||
serializer.data, status=status.HTTP_201_CREATED
|
||||
)
|
||||
return Response(
|
||||
serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
else:
|
||||
return Response(
|
||||
{
|
||||
@@ -285,22 +251,15 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
)
|
||||
|
||||
def patch(self, request, slug, project_id, pk):
|
||||
cycle = Cycle.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=pk
|
||||
)
|
||||
cycle = Cycle.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
|
||||
|
||||
request_data = request.data
|
||||
|
||||
if (
|
||||
cycle.end_date is not None
|
||||
and cycle.end_date < timezone.now().date()
|
||||
):
|
||||
if cycle.end_date is not None and cycle.end_date < timezone.now().date():
|
||||
if "sort_order" in request_data:
|
||||
# Can only change sort order
|
||||
request_data = {
|
||||
"sort_order": request_data.get(
|
||||
"sort_order", cycle.sort_order
|
||||
)
|
||||
"sort_order": request_data.get("sort_order", cycle.sort_order)
|
||||
}
|
||||
else:
|
||||
return Response(
|
||||
@@ -312,36 +271,17 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
|
||||
serializer = CycleSerializer(cycle, data=request.data, partial=True)
|
||||
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",
|
||||
"cycle_id": str(cycle.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):
|
||||
cycle_issues = list(
|
||||
CycleIssue.objects.filter(
|
||||
cycle_id=self.kwargs.get("pk")
|
||||
).values_list("issue", flat=True)
|
||||
)
|
||||
cycle = Cycle.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=pk
|
||||
CycleIssue.objects.filter(cycle_id=self.kwargs.get("pk")).values_list(
|
||||
"issue", flat=True
|
||||
)
|
||||
)
|
||||
cycle = Cycle.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
|
||||
|
||||
issue_activity.delay(
|
||||
type="cycle.activity.deleted",
|
||||
@@ -353,7 +293,7 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
}
|
||||
),
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=None,
|
||||
issue_id=str(pk),
|
||||
project_id=str(project_id),
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
@@ -365,15 +305,14 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
|
||||
class CycleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
"""
|
||||
This viewset automatically provides `list`, `create`,
|
||||
and `destroy` actions related to cycle issues.
|
||||
This viewset automatically provides `list`, `create`, `retrieve`,
|
||||
`update` and `destroy` actions related to cycle issues.
|
||||
|
||||
"""
|
||||
|
||||
serializer_class = CycleIssueSerializer
|
||||
model = CycleIssue
|
||||
webhook_event = "cycle_issue"
|
||||
bulk = True
|
||||
webhook_event = "cycle"
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
@@ -381,9 +320,7 @@ class CycleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
def get_queryset(self):
|
||||
return (
|
||||
CycleIssue.objects.annotate(
|
||||
sub_issues_count=Issue.issue_objects.filter(
|
||||
parent=OuterRef("issue_id")
|
||||
)
|
||||
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("issue_id"))
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
@@ -406,9 +343,7 @@ class CycleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
issues = (
|
||||
Issue.issue_objects.filter(issue_cycle__cycle_id=cycle_id)
|
||||
.annotate(
|
||||
sub_issues_count=Issue.issue_objects.filter(
|
||||
parent=OuterRef("id")
|
||||
)
|
||||
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
@@ -430,9 +365,7 @@ class CycleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
attachment_count=IssueAttachment.objects.filter(
|
||||
issue=OuterRef("id")
|
||||
)
|
||||
attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id"))
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
@@ -455,18 +388,14 @@ class CycleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
|
||||
if not issues:
|
||||
return Response(
|
||||
{"error": "Issues are required"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
{"error": "Issues are required"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
cycle = Cycle.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=cycle_id
|
||||
)
|
||||
|
||||
if (
|
||||
cycle.end_date is not None
|
||||
and cycle.end_date < timezone.now().date()
|
||||
):
|
||||
if cycle.end_date is not None and cycle.end_date < timezone.now().date():
|
||||
return Response(
|
||||
{
|
||||
"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
|
||||
issue_activity.delay(
|
||||
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),
|
||||
issue_id=None,
|
||||
project_id=str(self.kwargs.get("project_id", None)),
|
||||
@@ -549,12 +478,9 @@ class CycleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
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(
|
||||
issue_id=issue_id,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
cycle_id=cycle_id,
|
||||
pk=pk, workspace__slug=slug, project_id=project_id, cycle_id=cycle_id
|
||||
)
|
||||
issue_id = cycle_issue.issue_id
|
||||
cycle_issue.delete()
|
||||
@@ -567,7 +493,7 @@ class CycleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
}
|
||||
),
|
||||
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)),
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
@@ -625,4 +551,4 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
|
||||
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 plane.app.permissions import ProjectLitePermission
|
||||
from plane.api.serializers import InboxIssueSerializer, IssueSerializer
|
||||
from plane.db.models import (
|
||||
InboxIssue,
|
||||
Issue,
|
||||
State,
|
||||
ProjectMember,
|
||||
Project,
|
||||
Inbox,
|
||||
)
|
||||
from plane.db.models import InboxIssue, Issue, State, ProjectMember
|
||||
from plane.bgtasks.issue_activites_task import issue_activity
|
||||
|
||||
|
||||
@@ -44,41 +37,29 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
||||
]
|
||||
|
||||
def get_queryset(self):
|
||||
inbox = Inbox.objects.filter(
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
).first()
|
||||
|
||||
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),
|
||||
return self.filter_queryset(
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(
|
||||
Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True),
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
inbox_id=inbox.id,
|
||||
inbox_id=self.kwargs.get("inbox_id"),
|
||||
)
|
||||
.select_related("issue", "workspace", "project")
|
||||
.order_by(self.kwargs.get("order_by", "-created_at"))
|
||||
)
|
||||
|
||||
def get(self, request, slug, project_id, issue_id=None):
|
||||
if issue_id:
|
||||
inbox_issue_queryset = self.get_queryset().get(issue_id=issue_id)
|
||||
inbox_issue_data = InboxIssueSerializer(
|
||||
inbox_issue_queryset,
|
||||
def get(self, request, slug, project_id, inbox_id, pk=None):
|
||||
if pk:
|
||||
issue_queryset = self.get_queryset().get(pk=pk)
|
||||
issues_data = InboxIssueSerializer(
|
||||
issue_queryset,
|
||||
fields=self.fields,
|
||||
expand=self.expand,
|
||||
).data
|
||||
return Response(
|
||||
inbox_issue_data,
|
||||
issues_data,
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
issue_queryset = self.get_queryset()
|
||||
@@ -93,29 +74,10 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
||||
).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):
|
||||
return Response(
|
||||
{"error": "Name is required"},
|
||||
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,
|
||||
{"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
# Check for valid priority
|
||||
@@ -127,8 +89,7 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
||||
"none",
|
||||
]:
|
||||
return Response(
|
||||
{"error": "Invalid priority"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
{"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
# Create or get state
|
||||
@@ -162,45 +123,21 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
)
|
||||
|
||||
# create an inbox issue
|
||||
inbox_issue = InboxIssue.objects.create(
|
||||
inbox_id=inbox.id,
|
||||
InboxIssue.objects.create(
|
||||
inbox_id=inbox_id,
|
||||
project_id=project_id,
|
||||
issue=issue,
|
||||
source=request.data.get("source", "in-app"),
|
||||
)
|
||||
|
||||
serializer = InboxIssueSerializer(inbox_issue)
|
||||
serializer = IssueSerializer(issue)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
def patch(self, request, slug, project_id, issue_id):
|
||||
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
|
||||
def patch(self, request, slug, project_id, inbox_id, pk):
|
||||
inbox_issue = InboxIssue.objects.get(
|
||||
issue_id=issue_id,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
inbox_id=inbox.id,
|
||||
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
|
||||
)
|
||||
|
||||
# Get the project member
|
||||
project_member = ProjectMember.objects.get(
|
||||
workspace__slug=slug,
|
||||
@@ -208,7 +145,6 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
||||
member=request.user,
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
# Only project members admins and created_by users can access this endpoint
|
||||
if project_member.role <= 10 and str(inbox_issue.created_by_id) != str(
|
||||
request.user.id
|
||||
@@ -223,7 +159,7 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
||||
|
||||
if bool(issue_data):
|
||||
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
|
||||
if project_member.role <= 10:
|
||||
@@ -233,14 +169,10 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
||||
"description_html": issue_data.get(
|
||||
"description_html", issue.description_html
|
||||
),
|
||||
"description": issue_data.get(
|
||||
"description", issue.description
|
||||
),
|
||||
"description": issue_data.get("description", issue.description),
|
||||
}
|
||||
|
||||
issue_serializer = IssueSerializer(
|
||||
issue, data=issue_data, partial=True
|
||||
)
|
||||
issue_serializer = IssueSerializer(issue, data=issue_data, partial=True)
|
||||
|
||||
if issue_serializer.is_valid():
|
||||
current_instance = issue
|
||||
@@ -251,7 +183,7 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
||||
type="issue.activity.updated",
|
||||
requested_data=requested_data,
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=str(issue_id),
|
||||
issue_id=str(issue.id),
|
||||
project_id=str(project_id),
|
||||
current_instance=json.dumps(
|
||||
IssueSerializer(current_instance).data,
|
||||
@@ -276,14 +208,12 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
||||
# Update the issue state if the issue is rejected or marked as duplicate
|
||||
if serializer.data["status"] in [-1, 2]:
|
||||
issue = Issue.objects.get(
|
||||
pk=issue_id,
|
||||
pk=inbox_issue.issue_id,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
)
|
||||
state = State.objects.filter(
|
||||
group="cancelled",
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
group="cancelled", workspace__slug=slug, project_id=project_id
|
||||
).first()
|
||||
if state is not None:
|
||||
issue.state = state
|
||||
@@ -292,7 +222,7 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
||||
# Update the issue state if it is accepted
|
||||
if serializer.data["status"] in [1]:
|
||||
issue = Issue.objects.get(
|
||||
pk=issue_id,
|
||||
pk=inbox_issue.issue_id,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
)
|
||||
@@ -301,51 +231,23 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
||||
if issue.state.name == "Triage":
|
||||
# Move to default state
|
||||
state = State.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
default=True,
|
||||
workspace__slug=slug, project_id=project_id, default=True
|
||||
).first()
|
||||
if state is not None:
|
||||
issue.state = state
|
||||
issue.save()
|
||||
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(
|
||||
serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
else:
|
||||
return Response(
|
||||
InboxIssueSerializer(inbox_issue).data,
|
||||
status=status.HTTP_200_OK,
|
||||
InboxIssueSerializer(inbox_issue).data, status=status.HTTP_200_OK
|
||||
)
|
||||
|
||||
def delete(self, request, slug, project_id, issue_id):
|
||||
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
|
||||
def delete(self, request, slug, project_id, inbox_id, pk):
|
||||
inbox_issue = InboxIssue.objects.get(
|
||||
issue_id=issue_id,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
inbox_id=inbox.id,
|
||||
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
|
||||
)
|
||||
|
||||
# Get the project member
|
||||
project_member = ProjectMember.objects.get(
|
||||
workspace__slug=slug,
|
||||
@@ -354,7 +256,6 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
# Check the inbox issue created
|
||||
if project_member.role <= 10 and str(inbox_issue.created_by_id) != str(
|
||||
request.user.id
|
||||
):
|
||||
@@ -367,8 +268,8 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
||||
if inbox_issue.status in [-2, -1, 0, 2]:
|
||||
# Delete the issue also
|
||||
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()
|
||||
|
||||
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
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.parsers import MultiPartParser, FormParser
|
||||
|
||||
# Module imports
|
||||
from .base import BaseAPIView, WebhookMixin
|
||||
@@ -40,12 +41,14 @@ from plane.db.models import (
|
||||
IssueComment,
|
||||
IssueActivity,
|
||||
)
|
||||
from plane.utils.issue_filters import issue_filters
|
||||
from plane.bgtasks.issue_activites_task import issue_activity
|
||||
from plane.api.serializers import (
|
||||
IssueSerializer,
|
||||
LabelSerializer,
|
||||
IssueLinkSerializer,
|
||||
IssueCommentSerializer,
|
||||
IssueAttachmentSerializer,
|
||||
IssueActivitySerializer,
|
||||
)
|
||||
|
||||
@@ -67,9 +70,7 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
def get_queryset(self):
|
||||
return (
|
||||
Issue.issue_objects.annotate(
|
||||
sub_issues_count=Issue.issue_objects.filter(
|
||||
parent=OuterRef("id")
|
||||
)
|
||||
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
@@ -88,9 +89,7 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
def get(self, request, slug, project_id, pk=None):
|
||||
if pk:
|
||||
issue = Issue.issue_objects.annotate(
|
||||
sub_issues_count=Issue.issue_objects.filter(
|
||||
parent=OuterRef("id")
|
||||
)
|
||||
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
@@ -104,20 +103,16 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
# Custom ordering for priority and state
|
||||
priority_order = ["urgent", "high", "medium", "low", "none"]
|
||||
state_order = [
|
||||
"backlog",
|
||||
"unstarted",
|
||||
"started",
|
||||
"completed",
|
||||
"cancelled",
|
||||
]
|
||||
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
|
||||
|
||||
order_by_param = request.GET.get("order_by", "-created_at")
|
||||
|
||||
issue_queryset = (
|
||||
self.get_queryset()
|
||||
.filter(**filters)
|
||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||
.annotate(module_id=F("issue_module__module_id"))
|
||||
.annotate(
|
||||
@@ -127,9 +122,7 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
attachment_count=IssueAttachment.objects.filter(
|
||||
issue=OuterRef("id")
|
||||
)
|
||||
attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id"))
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
@@ -139,9 +132,7 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
# Priority Ordering
|
||||
if order_by_param == "priority" or order_by_param == "-priority":
|
||||
priority_order = (
|
||||
priority_order
|
||||
if order_by_param == "priority"
|
||||
else priority_order[::-1]
|
||||
priority_order if order_by_param == "priority" else priority_order[::-1]
|
||||
)
|
||||
issue_queryset = issue_queryset.annotate(
|
||||
priority_order=Case(
|
||||
@@ -189,9 +180,7 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
else order_by_param
|
||||
)
|
||||
).order_by(
|
||||
"-max_values"
|
||||
if order_by_param.startswith("-")
|
||||
else "max_values"
|
||||
"-max_values" if order_by_param.startswith("-") else "max_values"
|
||||
)
|
||||
else:
|
||||
issue_queryset = issue_queryset.order_by(order_by_param)
|
||||
@@ -220,38 +209,12 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
)
|
||||
|
||||
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",
|
||||
"issue_id": str(issue.id),
|
||||
},
|
||||
status=status.HTTP_409_CONFLICT,
|
||||
)
|
||||
|
||||
serializer.save()
|
||||
|
||||
# Track the issue
|
||||
issue_activity.delay(
|
||||
type="issue.activity.created",
|
||||
requested_data=json.dumps(
|
||||
self.request.data, cls=DjangoJSONEncoder
|
||||
),
|
||||
requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder),
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=str(serializer.data.get("id", None)),
|
||||
project_id=str(project_id),
|
||||
@@ -262,42 +225,13 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def patch(self, request, slug, project_id, pk=None):
|
||||
issue = Issue.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=pk
|
||||
)
|
||||
project = Project.objects.get(pk=project_id)
|
||||
issue = Issue.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
|
||||
current_instance = json.dumps(
|
||||
IssueSerializer(issue).data, cls=DjangoJSONEncoder
|
||||
)
|
||||
requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder)
|
||||
serializer = IssueSerializer(
|
||||
issue,
|
||||
data=request.data,
|
||||
context={
|
||||
"project_id": project_id,
|
||||
"workspace_id": project.workspace_id,
|
||||
},
|
||||
partial=True,
|
||||
)
|
||||
serializer = IssueSerializer(issue, data=request.data, partial=True)
|
||||
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",
|
||||
"issue_id": str(issue.id),
|
||||
},
|
||||
status=status.HTTP_409_CONFLICT,
|
||||
)
|
||||
|
||||
serializer.save()
|
||||
issue_activity.delay(
|
||||
type="issue.activity.updated",
|
||||
@@ -305,8 +239,6 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=str(pk),
|
||||
project_id=str(project_id),
|
||||
external_id__isnull=False,
|
||||
external_source__isnull=False,
|
||||
current_instance=current_instance,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
)
|
||||
@@ -314,9 +246,7 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def delete(self, request, slug, project_id, pk=None):
|
||||
issue = Issue.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=pk
|
||||
)
|
||||
issue = Issue.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
|
||||
current_instance = json.dumps(
|
||||
IssueSerializer(issue).data, cls=DjangoJSONEncoder
|
||||
)
|
||||
@@ -348,7 +278,7 @@ class LabelAPIEndpoint(BaseAPIView):
|
||||
|
||||
def get_queryset(self):
|
||||
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__project_projectmember__member=self.request.user)
|
||||
.select_related("project")
|
||||
@@ -363,47 +293,38 @@ class LabelAPIEndpoint(BaseAPIView):
|
||||
serializer = LabelSerializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
serializer.save(project_id=project_id)
|
||||
return Response(
|
||||
serializer.data, status=status.HTTP_201_CREATED
|
||||
)
|
||||
return Response(
|
||||
serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
except IntegrityError:
|
||||
return Response(
|
||||
{
|
||||
"error": "Label with the same name already exists in the project"
|
||||
},
|
||||
{"error": "Label with the same name already exists in the project"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
def get(self, request, slug, project_id, pk=None):
|
||||
if pk is None:
|
||||
return self.paginate(
|
||||
request=request,
|
||||
queryset=(self.get_queryset()),
|
||||
on_results=lambda labels: LabelSerializer(
|
||||
labels,
|
||||
many=True,
|
||||
fields=self.fields,
|
||||
expand=self.expand,
|
||||
).data,
|
||||
if pk:
|
||||
label = self.get_queryset().get(pk=pk)
|
||||
serializer = LabelSerializer(
|
||||
label,
|
||||
fields=self.fields,
|
||||
expand=self.expand,
|
||||
)
|
||||
label = self.get_queryset().get(pk=pk)
|
||||
serializer = LabelSerializer(
|
||||
label,
|
||||
fields=self.fields,
|
||||
expand=self.expand,
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return self.paginate(
|
||||
request=request,
|
||||
queryset=(self.get_queryset()),
|
||||
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):
|
||||
label = self.get_queryset().get(pk=pk)
|
||||
serializer = LabelSerializer(label, data=request.data, partial=True)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
def delete(self, request, slug, project_id, pk=None):
|
||||
label = self.get_queryset().get(pk=pk)
|
||||
@@ -435,31 +356,25 @@ class IssueLinkAPIEndpoint(BaseAPIView):
|
||||
.distinct()
|
||||
)
|
||||
|
||||
def get(self, request, slug, project_id, issue_id, pk=None):
|
||||
if pk is None:
|
||||
issue_links = self.get_queryset()
|
||||
def get(self, request, slug, project_id, pk=None):
|
||||
if pk:
|
||||
label = self.get_queryset().get(pk=pk)
|
||||
serializer = IssueLinkSerializer(
|
||||
issue_links,
|
||||
label,
|
||||
fields=self.fields,
|
||||
expand=self.expand,
|
||||
)
|
||||
return self.paginate(
|
||||
request=request,
|
||||
queryset=(self.get_queryset()),
|
||||
on_results=lambda issue_links: IssueLinkSerializer(
|
||||
issue_links,
|
||||
many=True,
|
||||
fields=self.fields,
|
||||
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)
|
||||
return self.paginate(
|
||||
request=request,
|
||||
queryset=(self.get_queryset()),
|
||||
on_results=lambda issue_links: IssueLinkSerializer(
|
||||
issue_links,
|
||||
many=True,
|
||||
fields=self.fields,
|
||||
expand=self.expand,
|
||||
).data,
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
def post(self, request, slug, project_id, issue_id):
|
||||
serializer = IssueLinkSerializer(data=request.data)
|
||||
@@ -470,9 +385,7 @@ class IssueLinkAPIEndpoint(BaseAPIView):
|
||||
)
|
||||
issue_activity.delay(
|
||||
type="link.activity.created",
|
||||
requested_data=json.dumps(
|
||||
serializer.data, cls=DjangoJSONEncoder
|
||||
),
|
||||
requested_data=json.dumps(serializer.data, cls=DjangoJSONEncoder),
|
||||
actor_id=str(self.request.user.id),
|
||||
issue_id=str(self.kwargs.get("issue_id")),
|
||||
project_id=str(self.kwargs.get("project_id")),
|
||||
@@ -484,19 +397,14 @@ class IssueLinkAPIEndpoint(BaseAPIView):
|
||||
|
||||
def patch(self, request, slug, project_id, issue_id, pk):
|
||||
issue_link = IssueLink.objects.get(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
issue_id=issue_id,
|
||||
pk=pk,
|
||||
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
|
||||
)
|
||||
requested_data = json.dumps(request.data, cls=DjangoJSONEncoder)
|
||||
current_instance = json.dumps(
|
||||
IssueLinkSerializer(issue_link).data,
|
||||
cls=DjangoJSONEncoder,
|
||||
)
|
||||
serializer = IssueLinkSerializer(
|
||||
issue_link, data=request.data, partial=True
|
||||
)
|
||||
serializer = IssueLinkSerializer(issue_link, data=request.data, partial=True)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
issue_activity.delay(
|
||||
@@ -513,10 +421,7 @@ class IssueLinkAPIEndpoint(BaseAPIView):
|
||||
|
||||
def delete(self, request, slug, project_id, issue_id, pk):
|
||||
issue_link = IssueLink.objects.get(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
issue_id=issue_id,
|
||||
pk=pk,
|
||||
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
|
||||
)
|
||||
current_instance = json.dumps(
|
||||
IssueLinkSerializer(issue_link).data,
|
||||
@@ -544,16 +449,14 @@ class IssueCommentAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
|
||||
serializer_class = IssueCommentSerializer
|
||||
model = IssueComment
|
||||
webhook_event = "issue_comment"
|
||||
webhook_event = "issue-comment"
|
||||
permission_classes = [
|
||||
ProjectLitePermission,
|
||||
]
|
||||
|
||||
def get_queryset(self):
|
||||
return (
|
||||
IssueComment.objects.filter(
|
||||
workspace__slug=self.kwargs.get("slug")
|
||||
)
|
||||
IssueComment.objects.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.filter(project_id=self.kwargs.get("project_id"))
|
||||
.filter(issue_id=self.kwargs.get("issue_id"))
|
||||
.filter(project__project_projectmember__member=self.request.user)
|
||||
@@ -605,9 +508,7 @@ class IssueCommentAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
)
|
||||
issue_activity.delay(
|
||||
type="comment.activity.created",
|
||||
requested_data=json.dumps(
|
||||
serializer.data, cls=DjangoJSONEncoder
|
||||
),
|
||||
requested_data=json.dumps(serializer.data, cls=DjangoJSONEncoder),
|
||||
actor_id=str(self.request.user.id),
|
||||
issue_id=str(self.kwargs.get("issue_id")),
|
||||
project_id=str(self.kwargs.get("project_id")),
|
||||
@@ -619,10 +520,7 @@ class IssueCommentAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
|
||||
def patch(self, request, slug, project_id, issue_id, pk):
|
||||
issue_comment = IssueComment.objects.get(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
issue_id=issue_id,
|
||||
pk=pk,
|
||||
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
|
||||
)
|
||||
requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder)
|
||||
current_instance = json.dumps(
|
||||
@@ -648,10 +546,7 @@ class IssueCommentAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
|
||||
def delete(self, request, slug, project_id, issue_id, pk):
|
||||
issue_comment = IssueComment.objects.get(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
issue_id=issue_id,
|
||||
pk=pk,
|
||||
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
|
||||
)
|
||||
current_instance = json.dumps(
|
||||
IssueCommentSerializer(issue_comment).data,
|
||||
@@ -686,13 +581,13 @@ class IssueActivityAPIEndpoint(BaseAPIView):
|
||||
)
|
||||
.select_related("actor", "workspace", "issue", "project")
|
||||
).order_by(request.GET.get("order_by", "created_at"))
|
||||
|
||||
|
||||
if pk:
|
||||
issue_activities = issue_activities.get(pk=pk)
|
||||
serializer = IssueActivitySerializer(issue_activities)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
return self.paginate(
|
||||
self.paginate(
|
||||
request=request,
|
||||
queryset=(issue_activities),
|
||||
on_results=lambda issue_activity: IssueActivitySerializer(
|
||||
|
||||
@@ -55,9 +55,7 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"link_module",
|
||||
queryset=ModuleLink.objects.select_related(
|
||||
"module", "created_by"
|
||||
),
|
||||
queryset=ModuleLink.objects.select_related("module", "created_by"),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
@@ -123,76 +121,15 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
)
|
||||
|
||||
def post(self, request, slug, project_id):
|
||||
project = Project.objects.get(pk=project_id, workspace__slug=slug)
|
||||
serializer = ModuleSerializer(
|
||||
data=request.data,
|
||||
context={
|
||||
"project_id": project_id,
|
||||
"workspace_id": project.workspace_id,
|
||||
},
|
||||
)
|
||||
project = Project.objects.get(workspace__slug=slug, pk=project_id)
|
||||
serializer = ModuleSerializer(data=request.data, context={"project": project})
|
||||
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",
|
||||
"module_id": str(module.id),
|
||||
},
|
||||
status=status.HTTP_409_CONFLICT,
|
||||
)
|
||||
serializer.save()
|
||||
module = Module.objects.get(pk=serializer.data["id"])
|
||||
serializer = ModuleSerializer(module)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.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",
|
||||
"module_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):
|
||||
if pk:
|
||||
queryset = self.get_queryset().get(pk=pk)
|
||||
@@ -217,13 +154,9 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
)
|
||||
|
||||
def delete(self, request, slug, project_id, pk):
|
||||
module = Module.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=pk
|
||||
)
|
||||
module = Module.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
|
||||
module_issues = list(
|
||||
ModuleIssue.objects.filter(module_id=pk).values_list(
|
||||
"issue", flat=True
|
||||
)
|
||||
ModuleIssue.objects.filter(module_id=pk).values_list("issue", flat=True)
|
||||
)
|
||||
issue_activity.delay(
|
||||
type="module.activity.deleted",
|
||||
@@ -235,7 +168,7 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
}
|
||||
),
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=None,
|
||||
issue_id=str(pk),
|
||||
project_id=str(project_id),
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
@@ -253,8 +186,7 @@ class ModuleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
|
||||
serializer_class = ModuleIssueSerializer
|
||||
model = ModuleIssue
|
||||
webhook_event = "module_issue"
|
||||
bulk = True
|
||||
webhook_event = "module"
|
||||
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
@@ -263,9 +195,7 @@ class ModuleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
def get_queryset(self):
|
||||
return (
|
||||
ModuleIssue.objects.annotate(
|
||||
sub_issues_count=Issue.issue_objects.filter(
|
||||
parent=OuterRef("issue")
|
||||
)
|
||||
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("issue"))
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
@@ -289,9 +219,7 @@ class ModuleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
issues = (
|
||||
Issue.issue_objects.filter(issue_module__module_id=module_id)
|
||||
.annotate(
|
||||
sub_issues_count=Issue.issue_objects.filter(
|
||||
parent=OuterRef("id")
|
||||
)
|
||||
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
@@ -313,9 +241,7 @@ class ModuleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
attachment_count=IssueAttachment.objects.filter(
|
||||
issue=OuterRef("id")
|
||||
)
|
||||
attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id"))
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
@@ -336,8 +262,7 @@ class ModuleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
issues = request.data.get("issues", [])
|
||||
if not len(issues):
|
||||
return Response(
|
||||
{"error": "Issues are required"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
{"error": "Issues are required"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
module = Module.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=module_id
|
||||
@@ -398,7 +323,7 @@ class ModuleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
# Capture Issue Activity
|
||||
issue_activity.delay(
|
||||
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),
|
||||
issue_id=None,
|
||||
project_id=str(self.kwargs.get("project_id", None)),
|
||||
@@ -418,12 +343,9 @@ class ModuleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
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(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
module_id=module_id,
|
||||
issue_id=issue_id,
|
||||
workspace__slug=slug, project_id=project_id, module_id=module_id, pk=pk
|
||||
)
|
||||
module_issue.delete()
|
||||
issue_activity.delay(
|
||||
@@ -435,9 +357,9 @@ class ModuleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
}
|
||||
),
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=str(issue_id),
|
||||
issue_id=str(pk),
|
||||
project_id=str(project_id),
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
@@ -39,15 +39,9 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
def get_queryset(self):
|
||||
return (
|
||||
Project.objects.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.filter(
|
||||
Q(project_projectmember__member=self.request.user)
|
||||
| Q(network=2)
|
||||
)
|
||||
.filter(Q(project_projectmember__member=self.request.user) | Q(network=2))
|
||||
.select_related(
|
||||
"workspace",
|
||||
"workspace__owner",
|
||||
"default_assignee",
|
||||
"project_lead",
|
||||
"workspace", "workspace__owner", "default_assignee", "project_lead"
|
||||
)
|
||||
.annotate(
|
||||
is_member=Exists(
|
||||
@@ -100,8 +94,8 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
.distinct()
|
||||
)
|
||||
|
||||
def get(self, request, slug, project_id=None):
|
||||
if project_id is None:
|
||||
def get(self, request, slug, pk=None):
|
||||
if pk is None:
|
||||
sort_order_query = ProjectMember.objects.filter(
|
||||
member=request.user,
|
||||
project_id=OuterRef("pk"),
|
||||
@@ -120,29 +114,24 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
).select_related("member"),
|
||||
)
|
||||
)
|
||||
.order_by(request.GET.get("order_by", "sort_order"))
|
||||
.order_by("sort_order", "name")
|
||||
)
|
||||
return self.paginate(
|
||||
request=request,
|
||||
queryset=(projects),
|
||||
on_results=lambda projects: ProjectSerializer(
|
||||
projects,
|
||||
many=True,
|
||||
fields=self.fields,
|
||||
expand=self.expand,
|
||||
projects, many=True, fields=self.fields, expand=self.expand,
|
||||
).data,
|
||||
)
|
||||
project = self.get_queryset().get(workspace__slug=slug, pk=project_id)
|
||||
serializer = ProjectSerializer(
|
||||
project,
|
||||
fields=self.fields,
|
||||
expand=self.expand,
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
else:
|
||||
project = self.get_queryset().get(workspace__slug=slug, pk=pk)
|
||||
serializer = ProjectSerializer(project, fields=self.fields, expand=self.expand,)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
def post(self, request, slug):
|
||||
try:
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
|
||||
serializer = ProjectSerializer(
|
||||
data={**request.data}, context={"workspace_id": workspace.id}
|
||||
)
|
||||
@@ -151,9 +140,7 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
|
||||
# Add the user as Administrator to the project
|
||||
project_member = ProjectMember.objects.create(
|
||||
project_id=serializer.data["id"],
|
||||
member=request.user,
|
||||
role=20,
|
||||
project_id=serializer.data["id"], member=request.user, role=20
|
||||
)
|
||||
# Also create the issue property for the user
|
||||
_ = IssueProperty.objects.create(
|
||||
@@ -226,15 +213,9 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
]
|
||||
)
|
||||
|
||||
project = (
|
||||
self.get_queryset()
|
||||
.filter(pk=serializer.data["id"])
|
||||
.first()
|
||||
)
|
||||
project = self.get_queryset().filter(pk=serializer.data["id"]).first()
|
||||
serializer = ProjectSerializer(project)
|
||||
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,
|
||||
@@ -247,8 +228,7 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
)
|
||||
except Workspace.DoesNotExist as e:
|
||||
return Response(
|
||||
{"error": "Workspace does not exist"},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
{"error": "Workspace does not exist"}, status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
except ValidationError as e:
|
||||
return Response(
|
||||
@@ -256,10 +236,10 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
status=status.HTTP_410_GONE,
|
||||
)
|
||||
|
||||
def patch(self, request, slug, project_id=None):
|
||||
def patch(self, request, slug, pk=None):
|
||||
try:
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
project = Project.objects.get(pk=project_id)
|
||||
project = Project.objects.get(pk=pk)
|
||||
|
||||
serializer = ProjectSerializer(
|
||||
project,
|
||||
@@ -272,9 +252,7 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
serializer.save()
|
||||
if serializer.data["inbox_view"]:
|
||||
Inbox.objects.get_or_create(
|
||||
name=f"{project.name} Inbox",
|
||||
project=project,
|
||||
is_default=True,
|
||||
name=f"{project.name} Inbox", project=project, is_default=True
|
||||
)
|
||||
|
||||
# Create the triage state in Backlog group
|
||||
@@ -282,20 +260,14 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
name="Triage",
|
||||
group="backlog",
|
||||
description="Default state for managing all Inbox Issues",
|
||||
project_id=project_id,
|
||||
project_id=pk,
|
||||
color="#ff7700",
|
||||
)
|
||||
|
||||
project = (
|
||||
self.get_queryset()
|
||||
.filter(pk=serializer.data["id"])
|
||||
.first()
|
||||
)
|
||||
project = self.get_queryset().filter(pk=serializer.data["id"]).first()
|
||||
serializer = ProjectSerializer(project)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(
|
||||
serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
except IntegrityError as e:
|
||||
if "already exists" in str(e):
|
||||
return Response(
|
||||
@@ -304,16 +276,10 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
)
|
||||
except (Project.DoesNotExist, Workspace.DoesNotExist):
|
||||
return Response(
|
||||
{"error": "Project does not exist"},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
{"error": "Project does not exist"}, status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
except ValidationError as e:
|
||||
return Response(
|
||||
{"identifier": "The project identifier is already taken"},
|
||||
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):
|
||||
return (
|
||||
State.objects.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
return self.filter_queryset(
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.filter(project_id=self.kwargs.get("project_id"))
|
||||
.filter(project__project_projectmember__member=self.request.user)
|
||||
.filter(~Q(name="Triage"))
|
||||
@@ -34,41 +36,15 @@ class StateAPIEndpoint(BaseAPIView):
|
||||
)
|
||||
|
||||
def post(self, request, slug, project_id):
|
||||
serializer = StateSerializer(
|
||||
data=request.data, context={"project_id": project_id}
|
||||
)
|
||||
serializer = StateSerializer(data=request.data, context={"project_id": project_id})
|
||||
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",
|
||||
"state_id": str(state.id),
|
||||
},
|
||||
status=status.HTTP_409_CONFLICT,
|
||||
)
|
||||
|
||||
serializer.save(project_id=project_id)
|
||||
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, state_id=None):
|
||||
if state_id:
|
||||
serializer = StateSerializer(self.get_queryset().get(pk=state_id))
|
||||
def get(self, request, slug, project_id, pk=None):
|
||||
if pk:
|
||||
serializer = StateSerializer(self.get_queryset().get(pk=pk))
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return self.paginate(
|
||||
request=request,
|
||||
@@ -81,57 +57,33 @@ class StateAPIEndpoint(BaseAPIView):
|
||||
).data,
|
||||
)
|
||||
|
||||
def delete(self, request, slug, project_id, state_id):
|
||||
def delete(self, request, slug, project_id, pk):
|
||||
state = State.objects.get(
|
||||
~Q(name="Triage"),
|
||||
pk=state_id,
|
||||
pk=pk,
|
||||
project_id=project_id,
|
||||
workspace__slug=slug,
|
||||
)
|
||||
|
||||
if state.default:
|
||||
return Response(
|
||||
{"error": "Default state cannot be deleted"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
return Response({"error": "Default state cannot be deleted"}, status=False)
|
||||
|
||||
# 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:
|
||||
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,
|
||||
)
|
||||
|
||||
state.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
def patch(self, request, slug, project_id, state_id=None):
|
||||
state = State.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=state_id
|
||||
)
|
||||
def patch(self, request, slug, project_id, pk=None):
|
||||
state = State.objects.filter(workspace__slug=slug, project_id=project_id, pk=pk)
|
||||
serializer = StateSerializer(state, data=request.data, partial=True)
|
||||
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",
|
||||
"state_id": str(state.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)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
@@ -25,10 +25,7 @@ class APIKeyAuthentication(authentication.BaseAuthentication):
|
||||
def validate_api_token(self, token):
|
||||
try:
|
||||
api_token = APIToken.objects.get(
|
||||
Q(
|
||||
Q(expired_at__gt=timezone.now())
|
||||
| Q(expired_at__isnull=True)
|
||||
),
|
||||
Q(Q(expired_at__gt=timezone.now()) | Q(expired_at__isnull=True)),
|
||||
token=token,
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
from .workspace import (
|
||||
WorkSpaceBasePermission,
|
||||
WorkspaceOwnerPermission,
|
||||
@@ -12,3 +13,5 @@ from .project import (
|
||||
ProjectMemberPermission,
|
||||
ProjectLitePermission,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -99,6 +99,7 @@ class WorkspaceViewerPermission(BasePermission):
|
||||
return WorkspaceMember.objects.filter(
|
||||
member=request.user,
|
||||
workspace__slug=view.workspace_slug,
|
||||
role__gte=10,
|
||||
is_active=True,
|
||||
).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,
|
||||
WorkspaceMemberAdminSerializer,
|
||||
WorkspaceMemberMeSerializer,
|
||||
WorkspaceUserPropertiesSerializer,
|
||||
)
|
||||
from .project import (
|
||||
ProjectSerializer,
|
||||
@@ -32,20 +31,14 @@ from .project import (
|
||||
ProjectDeployBoardSerializer,
|
||||
ProjectMemberAdminSerializer,
|
||||
ProjectPublicMemberSerializer,
|
||||
ProjectMemberRoleSerializer,
|
||||
)
|
||||
from .state import StateSerializer, StateLiteSerializer
|
||||
from .view import (
|
||||
GlobalViewSerializer,
|
||||
IssueViewSerializer,
|
||||
IssueViewFavoriteSerializer,
|
||||
)
|
||||
from .view import GlobalViewSerializer, IssueViewSerializer, IssueViewFavoriteSerializer
|
||||
from .cycle import (
|
||||
CycleSerializer,
|
||||
CycleIssueSerializer,
|
||||
CycleFavoriteSerializer,
|
||||
CycleWriteSerializer,
|
||||
CycleUserPropertiesSerializer,
|
||||
)
|
||||
from .asset import FileAssetSerializer
|
||||
from .issue import (
|
||||
@@ -76,7 +69,6 @@ from .module import (
|
||||
ModuleIssueSerializer,
|
||||
ModuleLinkSerializer,
|
||||
ModuleFavoriteSerializer,
|
||||
ModuleUserPropertiesSerializer,
|
||||
)
|
||||
|
||||
from .api import APITokenSerializer, APITokenReadSerializer
|
||||
@@ -93,33 +85,20 @@ from .integration import (
|
||||
|
||||
from .importer import ImporterSerializer
|
||||
|
||||
from .page import (
|
||||
PageSerializer,
|
||||
PageLogSerializer,
|
||||
SubPageSerializer,
|
||||
PageFavoriteSerializer,
|
||||
)
|
||||
from .page import PageSerializer, PageLogSerializer, SubPageSerializer, PageFavoriteSerializer
|
||||
|
||||
from .estimate import (
|
||||
EstimateSerializer,
|
||||
EstimatePointSerializer,
|
||||
EstimateReadSerializer,
|
||||
WorkspaceEstimateSerializer,
|
||||
)
|
||||
|
||||
from .inbox import (
|
||||
InboxSerializer,
|
||||
InboxIssueSerializer,
|
||||
IssueStateInboxSerializer,
|
||||
InboxIssueLiteSerializer,
|
||||
)
|
||||
from .inbox import InboxSerializer, InboxIssueSerializer, IssueStateInboxSerializer
|
||||
|
||||
from .analytic import AnalyticViewSerializer
|
||||
|
||||
from .notification import NotificationSerializer, UserNotificationPreferenceSerializer
|
||||
from .notification import NotificationSerializer
|
||||
|
||||
from .exporter import ExporterHistorySerializer
|
||||
|
||||
from .webhook import WebhookSerializer, WebhookLogSerializer
|
||||
|
||||
from .dashboard import DashboardSerializer, WidgetSerializer
|
||||
from .webhook import WebhookSerializer, WebhookLogSerializer
|
||||
@@ -3,6 +3,7 @@ from plane.db.models import APIToken, APIActivityLog
|
||||
|
||||
|
||||
class APITokenSerializer(BaseSerializer):
|
||||
|
||||
class Meta:
|
||||
model = APIToken
|
||||
fields = "__all__"
|
||||
@@ -17,12 +18,14 @@ class APITokenSerializer(BaseSerializer):
|
||||
|
||||
|
||||
class APITokenReadSerializer(BaseSerializer):
|
||||
|
||||
class Meta:
|
||||
model = APIToken
|
||||
exclude = ("token",)
|
||||
exclude = ('token',)
|
||||
|
||||
|
||||
class APIActivityLogSerializer(BaseSerializer):
|
||||
|
||||
class Meta:
|
||||
model = APIActivityLog
|
||||
fields = "__all__"
|
||||
|
||||
@@ -4,17 +4,16 @@ from rest_framework import serializers
|
||||
class BaseSerializer(serializers.ModelSerializer):
|
||||
id = serializers.PrimaryKeyRelatedField(read_only=True)
|
||||
|
||||
|
||||
class DynamicBaseSerializer(BaseSerializer):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# If 'fields' is provided in the arguments, remove it and store it separately.
|
||||
# This is done so as not to pass this custom argument up to the superclass.
|
||||
fields = kwargs.pop("fields", [])
|
||||
self.expand = kwargs.pop("expand", []) or []
|
||||
fields = self.expand
|
||||
fields = kwargs.pop("fields", None)
|
||||
|
||||
# Call the initialization of the superclass.
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# If 'fields' was provided, filter the fields of the serializer accordingly.
|
||||
if fields is not None:
|
||||
self.fields = self._filter_fields(fields)
|
||||
@@ -32,7 +31,7 @@ class DynamicBaseSerializer(BaseSerializer):
|
||||
# loop through its keys and values.
|
||||
if isinstance(field_name, dict):
|
||||
for key, value in field_name.items():
|
||||
# If the value of this nested field is a list,
|
||||
# If the value of this nested field is a list,
|
||||
# perform a recursive filter on it.
|
||||
if isinstance(value, list):
|
||||
self._filter_fields(self.fields[key], value)
|
||||
@@ -48,101 +47,12 @@ class DynamicBaseSerializer(BaseSerializer):
|
||||
elif isinstance(item, dict):
|
||||
allowed.append(list(item.keys())[0])
|
||||
|
||||
for field in allowed:
|
||||
if field not in self.fields:
|
||||
from . import (
|
||||
WorkspaceLiteSerializer,
|
||||
ProjectLiteSerializer,
|
||||
UserLiteSerializer,
|
||||
StateLiteSerializer,
|
||||
IssueSerializer,
|
||||
LabelSerializer,
|
||||
CycleIssueSerializer,
|
||||
IssueFlatSerializer,
|
||||
IssueRelationSerializer,
|
||||
InboxIssueLiteSerializer
|
||||
)
|
||||
# Convert the current serializer's fields and the allowed fields to sets.
|
||||
existing = set(self.fields)
|
||||
allowed = set(allowed)
|
||||
|
||||
# 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,
|
||||
}
|
||||
|
||||
self.fields[field] = expansion[field](many=True if field in ["members", "assignees", "labels", "issue_cycle", "issue_relation", "issue_inbox"] else False)
|
||||
# Remove fields from the serializer that aren't in the 'allowed' list.
|
||||
for field_name in (existing - allowed):
|
||||
self.fields.pop(field_name)
|
||||
|
||||
return self.fields
|
||||
|
||||
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 .workspace import WorkspaceLiteSerializer
|
||||
from .project import ProjectLiteSerializer
|
||||
from plane.db.models import (
|
||||
Cycle,
|
||||
CycleIssue,
|
||||
CycleFavorite,
|
||||
CycleUserProperties,
|
||||
)
|
||||
from plane.db.models import Cycle, CycleIssue, CycleFavorite
|
||||
|
||||
|
||||
class CycleWriteSerializer(BaseSerializer):
|
||||
@@ -22,9 +17,7 @@ class CycleWriteSerializer(BaseSerializer):
|
||||
and data.get("end_date", None) is not None
|
||||
and data.get("start_date", None) > data.get("end_date", None)
|
||||
):
|
||||
raise serializers.ValidationError(
|
||||
"Start date cannot exceed end date"
|
||||
)
|
||||
raise serializers.ValidationError("Start date cannot exceed end date")
|
||||
return data
|
||||
|
||||
class Meta:
|
||||
@@ -33,6 +26,7 @@ class CycleWriteSerializer(BaseSerializer):
|
||||
|
||||
|
||||
class CycleSerializer(BaseSerializer):
|
||||
owned_by = UserLiteSerializer(read_only=True)
|
||||
is_favorite = serializers.BooleanField(read_only=True)
|
||||
total_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)
|
||||
completed_estimates = serializers.IntegerField(read_only=True)
|
||||
started_estimates = serializers.IntegerField(read_only=True)
|
||||
workspace_detail = WorkspaceLiteSerializer(
|
||||
read_only=True, source="workspace"
|
||||
)
|
||||
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
|
||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||
status = serializers.CharField(read_only=True)
|
||||
|
||||
def validate(self, data):
|
||||
if (
|
||||
@@ -56,9 +47,7 @@ class CycleSerializer(BaseSerializer):
|
||||
and data.get("end_date", None) is not None
|
||||
and data.get("start_date", None) > data.get("end_date", None)
|
||||
):
|
||||
raise serializers.ValidationError(
|
||||
"Start date cannot exceed end date"
|
||||
)
|
||||
raise serializers.ValidationError("Start date cannot exceed end date")
|
||||
return data
|
||||
|
||||
def get_assignees(self, obj):
|
||||
@@ -116,14 +105,3 @@ class CycleFavoriteSerializer(BaseSerializer):
|
||||
"project",
|
||||
"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 plane.db.models import Estimate, EstimatePoint
|
||||
from plane.app.serializers import (
|
||||
WorkspaceLiteSerializer,
|
||||
ProjectLiteSerializer,
|
||||
)
|
||||
|
||||
from rest_framework import serializers
|
||||
from plane.app.serializers import WorkspaceLiteSerializer, ProjectLiteSerializer
|
||||
|
||||
|
||||
class EstimateSerializer(BaseSerializer):
|
||||
workspace_detail = WorkspaceLiteSerializer(
|
||||
read_only=True, source="workspace"
|
||||
)
|
||||
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
|
||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||
|
||||
class Meta:
|
||||
@@ -26,16 +19,6 @@ class EstimateSerializer(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:
|
||||
model = EstimatePoint
|
||||
fields = "__all__"
|
||||
@@ -48,9 +31,7 @@ class EstimatePointSerializer(BaseSerializer):
|
||||
|
||||
class EstimateReadSerializer(BaseSerializer):
|
||||
points = EstimatePointSerializer(read_only=True, many=True)
|
||||
workspace_detail = WorkspaceLiteSerializer(
|
||||
read_only=True, source="workspace"
|
||||
)
|
||||
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
|
||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||
|
||||
class Meta:
|
||||
@@ -61,18 +42,3 @@ class EstimateReadSerializer(BaseSerializer):
|
||||
"name",
|
||||
"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):
|
||||
initiated_by_detail = UserLiteSerializer(
|
||||
source="initiated_by", read_only=True
|
||||
)
|
||||
initiated_by_detail = UserLiteSerializer(source="initiated_by", read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = ExporterHistory
|
||||
|
||||
@@ -7,13 +7,9 @@ from plane.db.models import Importer
|
||||
|
||||
|
||||
class ImporterSerializer(BaseSerializer):
|
||||
initiated_by_detail = UserLiteSerializer(
|
||||
source="initiated_by", read_only=True
|
||||
)
|
||||
initiated_by_detail = UserLiteSerializer(source="initiated_by", read_only=True)
|
||||
project_detail = ProjectLiteSerializer(source="project", read_only=True)
|
||||
workspace_detail = WorkspaceLiteSerializer(
|
||||
source="workspace", read_only=True
|
||||
)
|
||||
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Importer
|
||||
|
||||
@@ -46,13 +46,10 @@ class InboxIssueLiteSerializer(BaseSerializer):
|
||||
class IssueStateInboxSerializer(BaseSerializer):
|
||||
state_detail = StateLiteSerializer(read_only=True, source="state")
|
||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||
label_details = LabelLiteSerializer(
|
||||
read_only=True, source="labels", many=True
|
||||
)
|
||||
assignee_details = UserLiteSerializer(
|
||||
read_only=True, source="assignees", many=True
|
||||
)
|
||||
label_details = LabelLiteSerializer(read_only=True, source="labels", many=True)
|
||||
assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
|
||||
sub_issues_count = serializers.IntegerField(read_only=True)
|
||||
bridge_id = serializers.UUIDField(read_only=True)
|
||||
issue_inbox = InboxIssueLiteSerializer(read_only=True, many=True)
|
||||
|
||||
class Meta:
|
||||
|
||||
@@ -13,9 +13,7 @@ class IntegrationSerializer(BaseSerializer):
|
||||
|
||||
|
||||
class WorkspaceIntegrationSerializer(BaseSerializer):
|
||||
integration_detail = IntegrationSerializer(
|
||||
read_only=True, source="integration"
|
||||
)
|
||||
integration_detail = IntegrationSerializer(read_only=True, source="integration")
|
||||
|
||||
class Meta:
|
||||
model = WorkspaceIntegration
|
||||
|
||||
@@ -30,8 +30,6 @@ from plane.db.models import (
|
||||
CommentReaction,
|
||||
IssueVote,
|
||||
IssueRelation,
|
||||
State,
|
||||
Project,
|
||||
)
|
||||
|
||||
|
||||
@@ -71,26 +69,19 @@ class IssueProjectLiteSerializer(BaseSerializer):
|
||||
##TODO: Find a better way to write this serializer
|
||||
## Find a better approach to save manytomany?
|
||||
class IssueCreateSerializer(BaseSerializer):
|
||||
# ids
|
||||
state_id = serializers.PrimaryKeyRelatedField(
|
||||
source="state",
|
||||
queryset=State.objects.all(),
|
||||
required=False,
|
||||
allow_null=True,
|
||||
)
|
||||
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()),
|
||||
state_detail = StateSerializer(read_only=True, source="state")
|
||||
created_by_detail = UserLiteSerializer(read_only=True, source="created_by")
|
||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
|
||||
|
||||
assignees = serializers.ListField(
|
||||
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
|
||||
write_only=True,
|
||||
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,
|
||||
required=False,
|
||||
)
|
||||
@@ -109,10 +100,8 @@ class IssueCreateSerializer(BaseSerializer):
|
||||
|
||||
def to_representation(self, instance):
|
||||
data = super().to_representation(instance)
|
||||
assignee_ids = self.initial_data.get("assignee_ids")
|
||||
data["assignee_ids"] = assignee_ids if assignee_ids else []
|
||||
label_ids = self.initial_data.get("label_ids")
|
||||
data["label_ids"] = label_ids if label_ids else []
|
||||
data['assignees'] = [str(assignee.id) for assignee in instance.assignees.all()]
|
||||
data['labels'] = [str(label.id) for label in instance.labels.all()]
|
||||
return data
|
||||
|
||||
def validate(self, data):
|
||||
@@ -121,14 +110,12 @@ class IssueCreateSerializer(BaseSerializer):
|
||||
and data.get("target_date", None) is not None
|
||||
and data.get("start_date", None) > data.get("target_date", None)
|
||||
):
|
||||
raise serializers.ValidationError(
|
||||
"Start date cannot exceed target date"
|
||||
)
|
||||
raise serializers.ValidationError("Start date cannot exceed target date")
|
||||
return data
|
||||
|
||||
def create(self, validated_data):
|
||||
assignees = validated_data.pop("assignee_ids", None)
|
||||
labels = validated_data.pop("label_ids", None)
|
||||
assignees = validated_data.pop("assignees", None)
|
||||
labels = validated_data.pop("labels", None)
|
||||
|
||||
project_id = self.context["project_id"]
|
||||
workspace_id = self.context["workspace_id"]
|
||||
@@ -186,8 +173,8 @@ class IssueCreateSerializer(BaseSerializer):
|
||||
return issue
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
assignees = validated_data.pop("assignee_ids", None)
|
||||
labels = validated_data.pop("label_ids", None)
|
||||
assignees = validated_data.pop("assignees", None)
|
||||
labels = validated_data.pop("labels", None)
|
||||
|
||||
# Related models
|
||||
project_id = instance.project_id
|
||||
@@ -238,15 +225,13 @@ class IssueActivitySerializer(BaseSerializer):
|
||||
actor_detail = UserLiteSerializer(read_only=True, source="actor")
|
||||
issue_detail = IssueFlatSerializer(read_only=True, source="issue")
|
||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||
workspace_detail = WorkspaceLiteSerializer(
|
||||
read_only=True, source="workspace"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = IssueActivity
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
|
||||
class IssuePropertySerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = IssueProperty
|
||||
@@ -259,17 +244,12 @@ class IssuePropertySerializer(BaseSerializer):
|
||||
|
||||
|
||||
class LabelSerializer(BaseSerializer):
|
||||
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
|
||||
project_detail = ProjectLiteSerializer(source="project", read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Label
|
||||
fields = [
|
||||
"parent",
|
||||
"name",
|
||||
"color",
|
||||
"id",
|
||||
"project_id",
|
||||
"workspace_id",
|
||||
"sort_order",
|
||||
]
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"project",
|
||||
@@ -287,6 +267,7 @@ class LabelLiteSerializer(BaseSerializer):
|
||||
|
||||
|
||||
class IssueLabelSerializer(BaseSerializer):
|
||||
|
||||
class Meta:
|
||||
model = IssueLabel
|
||||
fields = "__all__"
|
||||
@@ -297,50 +278,33 @@ class IssueLabelSerializer(BaseSerializer):
|
||||
|
||||
|
||||
class IssueRelationSerializer(BaseSerializer):
|
||||
id = serializers.UUIDField(source="related_issue.id", read_only=True)
|
||||
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)
|
||||
issue_detail = IssueProjectLiteSerializer(read_only=True, source="related_issue")
|
||||
|
||||
class Meta:
|
||||
model = IssueRelation
|
||||
fields = [
|
||||
"id",
|
||||
"project_id",
|
||||
"sequence_id",
|
||||
"issue_detail",
|
||||
"relation_type",
|
||||
"name",
|
||||
"related_issue",
|
||||
"issue",
|
||||
"id"
|
||||
]
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"project",
|
||||
]
|
||||
|
||||
|
||||
class RelatedIssueSerializer(BaseSerializer):
|
||||
id = serializers.UUIDField(source="issue.id", read_only=True)
|
||||
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)
|
||||
issue_detail = IssueProjectLiteSerializer(read_only=True, source="issue")
|
||||
|
||||
class Meta:
|
||||
model = IssueRelation
|
||||
fields = [
|
||||
"id",
|
||||
"project_id",
|
||||
"sequence_id",
|
||||
"issue_detail",
|
||||
"relation_type",
|
||||
"name",
|
||||
"related_issue",
|
||||
"issue",
|
||||
"id"
|
||||
]
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
@@ -435,8 +399,7 @@ class IssueLinkSerializer(BaseSerializer):
|
||||
# Validation if url already exists
|
||||
def create(self, validated_data):
|
||||
if IssueLink.objects.filter(
|
||||
url=validated_data.get("url"),
|
||||
issue_id=validated_data.get("issue_id"),
|
||||
url=validated_data.get("url"), issue_id=validated_data.get("issue_id")
|
||||
).exists():
|
||||
raise serializers.ValidationError(
|
||||
{"error": "URL already exists for this Issue"}
|
||||
@@ -460,8 +423,9 @@ class IssueAttachmentSerializer(BaseSerializer):
|
||||
|
||||
|
||||
class IssueReactionSerializer(BaseSerializer):
|
||||
|
||||
actor_detail = UserLiteSerializer(read_only=True, source="actor")
|
||||
|
||||
|
||||
class Meta:
|
||||
model = IssueReaction
|
||||
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 Meta:
|
||||
model = CommentReaction
|
||||
@@ -481,18 +458,12 @@ class CommentReactionSerializer(BaseSerializer):
|
||||
|
||||
|
||||
class IssueVoteSerializer(BaseSerializer):
|
||||
|
||||
actor_detail = UserLiteSerializer(read_only=True, source="actor")
|
||||
|
||||
class Meta:
|
||||
model = IssueVote
|
||||
fields = [
|
||||
"issue",
|
||||
"vote",
|
||||
"workspace",
|
||||
"project",
|
||||
"actor",
|
||||
"actor_detail",
|
||||
]
|
||||
fields = ["issue", "vote", "workspace", "project", "actor", "actor_detail"]
|
||||
read_only_fields = fields
|
||||
|
||||
|
||||
@@ -500,12 +471,8 @@ class IssueCommentSerializer(BaseSerializer):
|
||||
actor_detail = UserLiteSerializer(read_only=True, source="actor")
|
||||
issue_detail = IssueFlatSerializer(read_only=True, source="issue")
|
||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||
workspace_detail = WorkspaceLiteSerializer(
|
||||
read_only=True, source="workspace"
|
||||
)
|
||||
comment_reactions = CommentReactionSerializer(
|
||||
read_only=True, many=True
|
||||
)
|
||||
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
|
||||
comment_reactions = CommentReactionLiteSerializer(read_only=True, many=True)
|
||||
is_member = serializers.BooleanField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
@@ -539,15 +506,12 @@ class IssueStateFlatSerializer(BaseSerializer):
|
||||
|
||||
# Issue Serializer with state details
|
||||
class IssueStateSerializer(DynamicBaseSerializer):
|
||||
label_details = LabelLiteSerializer(
|
||||
read_only=True, source="labels", many=True
|
||||
)
|
||||
label_details = LabelLiteSerializer(read_only=True, source="labels", many=True)
|
||||
state_detail = StateLiteSerializer(read_only=True, source="state")
|
||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||
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)
|
||||
bridge_id = serializers.UUIDField(read_only=True)
|
||||
attachment_count = serializers.IntegerField(read_only=True)
|
||||
link_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
@@ -556,80 +520,40 @@ class IssueStateSerializer(DynamicBaseSerializer):
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class IssueSerializer(DynamicBaseSerializer):
|
||||
# ids
|
||||
project_id = serializers.PrimaryKeyRelatedField(read_only=True)
|
||||
state_id = serializers.PrimaryKeyRelatedField(read_only=True)
|
||||
parent_id = serializers.PrimaryKeyRelatedField(read_only=True)
|
||||
cycle_id = serializers.PrimaryKeyRelatedField(read_only=True)
|
||||
module_ids = serializers.SerializerMethodField()
|
||||
|
||||
# Many to many
|
||||
label_ids = serializers.PrimaryKeyRelatedField(
|
||||
read_only=True, many=True, source="labels"
|
||||
)
|
||||
assignee_ids = serializers.PrimaryKeyRelatedField(
|
||||
read_only=True, many=True, source="assignees"
|
||||
)
|
||||
|
||||
# Count items
|
||||
class IssueSerializer(BaseSerializer):
|
||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||
state_detail = StateSerializer(read_only=True, source="state")
|
||||
parent_detail = IssueStateFlatSerializer(read_only=True, source="parent")
|
||||
label_details = LabelSerializer(read_only=True, source="labels", many=True)
|
||||
assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
|
||||
related_issues = IssueRelationSerializer(read_only=True, source="issue_relation", many=True)
|
||||
issue_relations = RelatedIssueSerializer(read_only=True, source="issue_related", many=True)
|
||||
issue_cycle = IssueCycleDetailSerializer(read_only=True)
|
||||
issue_module = IssueModuleDetailSerializer(read_only=True)
|
||||
issue_link = IssueLinkSerializer(read_only=True, many=True)
|
||||
issue_attachment = IssueAttachmentSerializer(read_only=True, many=True)
|
||||
sub_issues_count = serializers.IntegerField(read_only=True)
|
||||
attachment_count = serializers.IntegerField(read_only=True)
|
||||
link_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
# is_subscribed
|
||||
is_subscribed = serializers.BooleanField(read_only=True)
|
||||
issue_reactions = IssueReactionSerializer(read_only=True, many=True)
|
||||
|
||||
class Meta:
|
||||
model = Issue
|
||||
fields = [
|
||||
"id",
|
||||
"name",
|
||||
"state_id",
|
||||
"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",
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"project",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"attachment_count",
|
||||
"link_count",
|
||||
"is_subscribed",
|
||||
"is_draft",
|
||||
"archived_at",
|
||||
"created_at",
|
||||
"updated_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):
|
||||
workspace_detail = WorkspaceLiteSerializer(
|
||||
read_only=True, source="workspace"
|
||||
)
|
||||
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
|
||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||
state_detail = StateLiteSerializer(read_only=True, source="state")
|
||||
label_details = LabelLiteSerializer(
|
||||
read_only=True, source="labels", many=True
|
||||
)
|
||||
assignee_details = UserLiteSerializer(
|
||||
read_only=True, source="assignees", many=True
|
||||
)
|
||||
label_details = LabelLiteSerializer(read_only=True, source="labels", many=True)
|
||||
assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
|
||||
sub_issues_count = serializers.IntegerField(read_only=True)
|
||||
cycle_id = serializers.UUIDField(read_only=True)
|
||||
module_id = serializers.UUIDField(read_only=True)
|
||||
@@ -656,9 +580,7 @@ class IssueLiteSerializer(DynamicBaseSerializer):
|
||||
class IssuePublicSerializer(BaseSerializer):
|
||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||
state_detail = StateLiteSerializer(read_only=True, source="state")
|
||||
reactions = IssueReactionSerializer(
|
||||
read_only=True, many=True, source="issue_reactions"
|
||||
)
|
||||
reactions = IssueReactionSerializer(read_only=True, many=True, source="issue_reactions")
|
||||
votes = IssueVoteSerializer(read_only=True, many=True)
|
||||
|
||||
class Meta:
|
||||
@@ -681,6 +603,7 @@ class IssuePublicSerializer(BaseSerializer):
|
||||
read_only_fields = fields
|
||||
|
||||
|
||||
|
||||
class IssueSubscriberSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = IssueSubscriber
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
# Module imports
|
||||
from .base import BaseSerializer, DynamicBaseSerializer
|
||||
from .base import BaseSerializer
|
||||
from .user import UserLiteSerializer
|
||||
from .project import ProjectLiteSerializer
|
||||
from .workspace import WorkspaceLiteSerializer
|
||||
@@ -14,7 +14,6 @@ from plane.db.models import (
|
||||
ModuleIssue,
|
||||
ModuleLink,
|
||||
ModuleFavorite,
|
||||
ModuleUserProperties,
|
||||
)
|
||||
|
||||
|
||||
@@ -26,9 +25,7 @@ class ModuleWriteSerializer(BaseSerializer):
|
||||
)
|
||||
|
||||
project_detail = ProjectLiteSerializer(source="project", read_only=True)
|
||||
workspace_detail = WorkspaceLiteSerializer(
|
||||
source="workspace", read_only=True
|
||||
)
|
||||
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Module
|
||||
@@ -41,22 +38,16 @@ class ModuleWriteSerializer(BaseSerializer):
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
|
||||
|
||||
def to_representation(self, 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
|
||||
|
||||
def validate(self, data):
|
||||
if (
|
||||
data.get("start_date", None) is not None
|
||||
and data.get("target_date", None) is not None
|
||||
and data.get("start_date", None) > data.get("target_date", None)
|
||||
):
|
||||
raise serializers.ValidationError(
|
||||
"Start date cannot exceed target date"
|
||||
)
|
||||
return data
|
||||
if data.get("start_date", None) is not None and data.get("target_date", None) is not None and data.get("start_date", None) > data.get("target_date", None):
|
||||
raise serializers.ValidationError("Start date cannot exceed target date")
|
||||
return data
|
||||
|
||||
def create(self, validated_data):
|
||||
members = validated_data.pop("members", None)
|
||||
@@ -160,8 +151,7 @@ class ModuleLinkSerializer(BaseSerializer):
|
||||
# Validation if url already exists
|
||||
def create(self, validated_data):
|
||||
if ModuleLink.objects.filter(
|
||||
url=validated_data.get("url"),
|
||||
module_id=validated_data.get("module_id"),
|
||||
url=validated_data.get("url"), module_id=validated_data.get("module_id")
|
||||
).exists():
|
||||
raise serializers.ValidationError(
|
||||
{"error": "URL already exists for this Issue"}
|
||||
@@ -169,12 +159,10 @@ class ModuleLinkSerializer(BaseSerializer):
|
||||
return ModuleLink.objects.create(**validated_data)
|
||||
|
||||
|
||||
class ModuleSerializer(DynamicBaseSerializer):
|
||||
class ModuleSerializer(BaseSerializer):
|
||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||
lead_detail = UserLiteSerializer(read_only=True, source="lead")
|
||||
members_detail = UserLiteSerializer(
|
||||
read_only=True, many=True, source="members"
|
||||
)
|
||||
members_detail = UserLiteSerializer(read_only=True, many=True, source="members")
|
||||
link_module = ModuleLinkSerializer(read_only=True, many=True)
|
||||
is_favorite = serializers.BooleanField(read_only=True)
|
||||
total_issues = serializers.IntegerField(read_only=True)
|
||||
@@ -208,10 +196,3 @@ class ModuleFavoriteSerializer(BaseSerializer):
|
||||
"project",
|
||||
"user",
|
||||
]
|
||||
|
||||
|
||||
class ModuleUserPropertiesSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = ModuleUserProperties
|
||||
fields = "__all__"
|
||||
read_only_fields = ["workspace", "project", "module", "user"]
|
||||
|
||||
@@ -1,21 +1,12 @@
|
||||
# Module imports
|
||||
from .base import BaseSerializer
|
||||
from .user import UserLiteSerializer
|
||||
from plane.db.models import Notification, UserNotificationPreference
|
||||
|
||||
from plane.db.models import Notification
|
||||
|
||||
class NotificationSerializer(BaseSerializer):
|
||||
triggered_by_details = UserLiteSerializer(
|
||||
read_only=True, source="triggered_by"
|
||||
)
|
||||
triggered_by_details = UserLiteSerializer(read_only=True, source="triggered_by")
|
||||
|
||||
class Meta:
|
||||
model = Notification
|
||||
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 .workspace import WorkspaceLiteSerializer
|
||||
from .project import ProjectLiteSerializer
|
||||
from plane.db.models import (
|
||||
Page,
|
||||
PageLog,
|
||||
PageFavorite,
|
||||
PageLabel,
|
||||
Label,
|
||||
Issue,
|
||||
Module,
|
||||
)
|
||||
from plane.db.models import Page, PageLog, PageFavorite, PageLabel, Label, Issue, Module
|
||||
|
||||
|
||||
class PageSerializer(BaseSerializer):
|
||||
is_favorite = serializers.BooleanField(read_only=True)
|
||||
label_details = LabelLiteSerializer(
|
||||
read_only=True, source="labels", many=True
|
||||
)
|
||||
label_details = LabelLiteSerializer(read_only=True, source="labels", many=True)
|
||||
labels = serializers.ListField(
|
||||
child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()),
|
||||
write_only=True,
|
||||
required=False,
|
||||
)
|
||||
project_detail = ProjectLiteSerializer(source="project", read_only=True)
|
||||
workspace_detail = WorkspaceLiteSerializer(
|
||||
source="workspace", read_only=True
|
||||
)
|
||||
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Page
|
||||
@@ -40,10 +28,9 @@ class PageSerializer(BaseSerializer):
|
||||
"project",
|
||||
"owned_by",
|
||||
]
|
||||
|
||||
def to_representation(self, 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
|
||||
|
||||
def create(self, validated_data):
|
||||
@@ -107,7 +94,7 @@ class SubPageSerializer(BaseSerializer):
|
||||
|
||||
def get_entity_details(self, obj):
|
||||
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:
|
||||
page = Page.objects.get(pk=obj.entity_identifier)
|
||||
return PageSerializer(page).data
|
||||
@@ -117,6 +104,7 @@ class SubPageSerializer(BaseSerializer):
|
||||
|
||||
|
||||
class PageLogSerializer(BaseSerializer):
|
||||
|
||||
class Meta:
|
||||
model = PageLog
|
||||
fields = "__all__"
|
||||
|
||||
@@ -4,10 +4,7 @@ from rest_framework import serializers
|
||||
# Module imports
|
||||
from .base import BaseSerializer, DynamicBaseSerializer
|
||||
from plane.app.serializers.workspace import WorkspaceLiteSerializer
|
||||
from plane.app.serializers.user import (
|
||||
UserLiteSerializer,
|
||||
UserAdminLiteSerializer,
|
||||
)
|
||||
from plane.app.serializers.user import UserLiteSerializer, UserAdminLiteSerializer
|
||||
from plane.db.models import (
|
||||
Project,
|
||||
ProjectMember,
|
||||
@@ -20,9 +17,7 @@ from plane.db.models import (
|
||||
|
||||
|
||||
class ProjectSerializer(BaseSerializer):
|
||||
workspace_detail = WorkspaceLiteSerializer(
|
||||
source="workspace", read_only=True
|
||||
)
|
||||
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Project
|
||||
@@ -34,16 +29,12 @@ class ProjectSerializer(BaseSerializer):
|
||||
def create(self, validated_data):
|
||||
identifier = validated_data.get("identifier", "").strip().upper()
|
||||
if identifier == "":
|
||||
raise serializers.ValidationError(
|
||||
detail="Project Identifier is required"
|
||||
)
|
||||
raise serializers.ValidationError(detail="Project Identifier is required")
|
||||
|
||||
if ProjectIdentifier.objects.filter(
|
||||
name=identifier, workspace_id=self.context["workspace_id"]
|
||||
).exists():
|
||||
raise serializers.ValidationError(
|
||||
detail="Project Identifier is taken"
|
||||
)
|
||||
raise serializers.ValidationError(detail="Project Identifier is taken")
|
||||
project = Project.objects.create(
|
||||
**validated_data, workspace_id=self.context["workspace_id"]
|
||||
)
|
||||
@@ -82,9 +73,7 @@ class ProjectSerializer(BaseSerializer):
|
||||
return project
|
||||
|
||||
# If not same fail update
|
||||
raise serializers.ValidationError(
|
||||
detail="Project Identifier is already taken"
|
||||
)
|
||||
raise serializers.ValidationError(detail="Project Identifier is already taken")
|
||||
|
||||
|
||||
class ProjectLiteSerializer(BaseSerializer):
|
||||
@@ -114,19 +103,16 @@ class ProjectListSerializer(DynamicBaseSerializer):
|
||||
members = serializers.SerializerMethodField()
|
||||
|
||||
def get_members(self, obj):
|
||||
project_members = getattr(obj, "members_list", None)
|
||||
if project_members is not None:
|
||||
# Filter members by the project ID
|
||||
return [
|
||||
{
|
||||
"id": member.id,
|
||||
"member_id": member.member_id,
|
||||
"member__display_name": member.member.display_name,
|
||||
"member__avatar": member.member.avatar,
|
||||
}
|
||||
for member in project_members
|
||||
]
|
||||
return []
|
||||
project_members = ProjectMember.objects.filter(
|
||||
project_id=obj.id,
|
||||
is_active=True,
|
||||
).values(
|
||||
"id",
|
||||
"member_id",
|
||||
"member__display_name",
|
||||
"member__avatar",
|
||||
)
|
||||
return list(project_members)
|
||||
|
||||
class Meta:
|
||||
model = Project
|
||||
@@ -171,12 +157,6 @@ class ProjectMemberAdminSerializer(BaseSerializer):
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class ProjectMemberRoleSerializer(DynamicBaseSerializer):
|
||||
class Meta:
|
||||
model = ProjectMember
|
||||
fields = ("id", "role", "member", "project")
|
||||
|
||||
|
||||
class ProjectMemberInviteSerializer(BaseSerializer):
|
||||
project = ProjectLiteSerializer(read_only=True)
|
||||
workspace = WorkspaceLiteSerializer(read_only=True)
|
||||
@@ -214,9 +194,7 @@ class ProjectMemberLiteSerializer(BaseSerializer):
|
||||
|
||||
class ProjectDeployBoardSerializer(BaseSerializer):
|
||||
project_details = ProjectLiteSerializer(read_only=True, source="project")
|
||||
workspace_detail = WorkspaceLiteSerializer(
|
||||
read_only=True, source="workspace"
|
||||
)
|
||||
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
|
||||
|
||||
class Meta:
|
||||
model = ProjectDeployBoard
|
||||
@@ -236,4 +214,4 @@ class ProjectPublicMemberSerializer(BaseSerializer):
|
||||
"workspace",
|
||||
"project",
|
||||
"member",
|
||||
]
|
||||
]
|
||||
@@ -6,19 +6,10 @@ from plane.db.models import State
|
||||
|
||||
|
||||
class StateSerializer(BaseSerializer):
|
||||
|
||||
class Meta:
|
||||
model = State
|
||||
fields = [
|
||||
"id",
|
||||
"project_id",
|
||||
"workspace_id",
|
||||
"name",
|
||||
"color",
|
||||
"group",
|
||||
"default",
|
||||
"description",
|
||||
"sequence",
|
||||
]
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"project",
|
||||
@@ -34,4 +25,4 @@ class StateLiteSerializer(BaseSerializer):
|
||||
"color",
|
||||
"group",
|
||||
]
|
||||
read_only_fields = fields
|
||||
read_only_fields = fields
|
||||
@@ -26,8 +26,6 @@ class UserSerializer(BaseSerializer):
|
||||
"token_updated_at",
|
||||
"is_onboarded",
|
||||
"is_bot",
|
||||
"is_password_autoset",
|
||||
"is_email_verified",
|
||||
]
|
||||
extra_kwargs = {"password": {"write_only": True}}
|
||||
|
||||
@@ -62,8 +60,6 @@ class UserMeSerializer(BaseSerializer):
|
||||
"theme",
|
||||
"last_workspace_id",
|
||||
"use_case",
|
||||
"is_password_autoset",
|
||||
"is_email_verified",
|
||||
]
|
||||
read_only_fields = fields
|
||||
|
||||
@@ -84,24 +80,13 @@ class UserMeSettingsSerializer(BaseSerializer):
|
||||
workspace_invites = WorkspaceMemberInvite.objects.filter(
|
||||
email=obj.email
|
||||
).count()
|
||||
if (
|
||||
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()
|
||||
):
|
||||
if obj.last_workspace_id is not None:
|
||||
workspace = Workspace.objects.filter(
|
||||
pk=obj.last_workspace_id,
|
||||
workspace_member__member=obj.id,
|
||||
workspace_member__is_active=True,
|
||||
pk=obj.last_workspace_id, workspace_member__member=obj.id
|
||||
).first()
|
||||
return {
|
||||
"last_workspace_id": obj.last_workspace_id,
|
||||
"last_workspace_slug": workspace.slug
|
||||
if workspace is not None
|
||||
else "",
|
||||
"last_workspace_slug": workspace.slug if workspace is not None else "",
|
||||
"fallback_workspace_id": obj.last_workspace_id,
|
||||
"fallback_workspace_slug": workspace.slug
|
||||
if workspace is not None
|
||||
@@ -110,10 +95,7 @@ class UserMeSettingsSerializer(BaseSerializer):
|
||||
}
|
||||
else:
|
||||
fallback_workspace = (
|
||||
Workspace.objects.filter(
|
||||
workspace_member__member_id=obj.id,
|
||||
workspace_member__is_active=True,
|
||||
)
|
||||
Workspace.objects.filter(workspace_member__member_id=obj.id)
|
||||
.order_by("created_at")
|
||||
.first()
|
||||
)
|
||||
@@ -172,28 +154,14 @@ class ChangePasswordSerializer(serializers.Serializer):
|
||||
Serializer for password change endpoint.
|
||||
"""
|
||||
old_password = serializers.CharField(required=True)
|
||||
new_password = serializers.CharField(required=True, min_length=8)
|
||||
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
|
||||
new_password = serializers.CharField(required=True)
|
||||
|
||||
|
||||
class ResetPasswordSerializer(serializers.Serializer):
|
||||
model = User
|
||||
|
||||
"""
|
||||
Serializer for password change endpoint.
|
||||
"""
|
||||
|
||||
new_password = serializers.CharField(required=True, min_length=8)
|
||||
new_password = serializers.CharField(required=True)
|
||||
confirm_password = serializers.CharField(required=True)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
# Module imports
|
||||
from .base import BaseSerializer, DynamicBaseSerializer
|
||||
from .base import BaseSerializer
|
||||
from .workspace import WorkspaceLiteSerializer
|
||||
from .project import ProjectLiteSerializer
|
||||
from plane.db.models import GlobalView, IssueView, IssueViewFavorite
|
||||
@@ -10,9 +10,7 @@ from plane.utils.issue_filters import issue_filters
|
||||
|
||||
|
||||
class GlobalViewSerializer(BaseSerializer):
|
||||
workspace_detail = WorkspaceLiteSerializer(
|
||||
source="workspace", read_only=True
|
||||
)
|
||||
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = GlobalView
|
||||
@@ -40,12 +38,10 @@ class GlobalViewSerializer(BaseSerializer):
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
|
||||
class IssueViewSerializer(DynamicBaseSerializer):
|
||||
class IssueViewSerializer(BaseSerializer):
|
||||
is_favorite = serializers.BooleanField(read_only=True)
|
||||
project_detail = ProjectLiteSerializer(source="project", read_only=True)
|
||||
workspace_detail = WorkspaceLiteSerializer(
|
||||
source="workspace", read_only=True
|
||||
)
|
||||
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = IssueView
|
||||
|
||||
@@ -1,125 +1,14 @@
|
||||
# Python imports
|
||||
import urllib
|
||||
import socket
|
||||
import ipaddress
|
||||
from urllib.parse import urlparse
|
||||
|
||||
# Third party imports
|
||||
from rest_framework import serializers
|
||||
|
||||
# Module imports
|
||||
from .base import DynamicBaseSerializer
|
||||
from plane.db.models import Webhook, WebhookLog
|
||||
from plane.db.models.webhook import validate_domain, validate_schema
|
||||
|
||||
from plane.db.models.webhook import validate_domain, validate_schema
|
||||
|
||||
class WebhookSerializer(DynamicBaseSerializer):
|
||||
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:
|
||||
model = Webhook
|
||||
fields = "__all__"
|
||||
@@ -130,7 +19,12 @@ class WebhookSerializer(DynamicBaseSerializer):
|
||||
|
||||
|
||||
class WebhookLogSerializer(DynamicBaseSerializer):
|
||||
|
||||
class Meta:
|
||||
model = WebhookLog
|
||||
fields = "__all__"
|
||||
read_only_fields = ["workspace", "webhook"]
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"webhook"
|
||||
]
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
# Module imports
|
||||
from .base import BaseSerializer, DynamicBaseSerializer
|
||||
from .base import BaseSerializer
|
||||
from .user import UserLiteSerializer, UserAdminLiteSerializer
|
||||
|
||||
from plane.db.models import (
|
||||
@@ -13,32 +13,14 @@ from plane.db.models import (
|
||||
TeamMember,
|
||||
WorkspaceMemberInvite,
|
||||
WorkspaceTheme,
|
||||
WorkspaceUserProperties,
|
||||
)
|
||||
|
||||
|
||||
class WorkSpaceSerializer(DynamicBaseSerializer):
|
||||
class WorkSpaceSerializer(BaseSerializer):
|
||||
owner = UserLiteSerializer(read_only=True)
|
||||
total_members = 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:
|
||||
model = Workspace
|
||||
fields = "__all__"
|
||||
@@ -51,7 +33,6 @@ class WorkSpaceSerializer(DynamicBaseSerializer):
|
||||
"owner",
|
||||
]
|
||||
|
||||
|
||||
class WorkspaceLiteSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = Workspace
|
||||
@@ -63,7 +44,8 @@ class WorkspaceLiteSerializer(BaseSerializer):
|
||||
read_only_fields = fields
|
||||
|
||||
|
||||
class WorkSpaceMemberSerializer(DynamicBaseSerializer):
|
||||
|
||||
class WorkSpaceMemberSerializer(BaseSerializer):
|
||||
member = UserLiteSerializer(read_only=True)
|
||||
workspace = WorkspaceLiteSerializer(read_only=True)
|
||||
|
||||
@@ -73,12 +55,13 @@ class WorkSpaceMemberSerializer(DynamicBaseSerializer):
|
||||
|
||||
|
||||
class WorkspaceMemberMeSerializer(BaseSerializer):
|
||||
|
||||
class Meta:
|
||||
model = WorkspaceMember
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class WorkspaceMemberAdminSerializer(DynamicBaseSerializer):
|
||||
class WorkspaceMemberAdminSerializer(BaseSerializer):
|
||||
member = UserAdminLiteSerializer(read_only=True)
|
||||
workspace = WorkspaceLiteSerializer(read_only=True)
|
||||
|
||||
@@ -95,22 +78,10 @@ class WorkSpaceMemberInviteSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = WorkspaceMemberInvite
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"id",
|
||||
"email",
|
||||
"token",
|
||||
"workspace",
|
||||
"message",
|
||||
"responded_at",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
|
||||
|
||||
class TeamSerializer(BaseSerializer):
|
||||
members_detail = UserLiteSerializer(
|
||||
read_only=True, source="members", many=True
|
||||
)
|
||||
members_detail = UserLiteSerializer(read_only=True, source="members", many=True)
|
||||
members = serializers.ListField(
|
||||
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
|
||||
write_only=True,
|
||||
@@ -147,9 +118,7 @@ class TeamSerializer(BaseSerializer):
|
||||
members = validated_data.pop("members")
|
||||
TeamMember.objects.filter(team=instance).delete()
|
||||
team_members = [
|
||||
TeamMember(
|
||||
member=member, team=instance, workspace=instance.workspace
|
||||
)
|
||||
TeamMember(member=member, team=instance, workspace=instance.workspace)
|
||||
for member in members
|
||||
]
|
||||
TeamMember.objects.bulk_create(team_members, batch_size=10)
|
||||
@@ -165,13 +134,3 @@ class WorkspaceThemeSerializer(BaseSerializer):
|
||||
"workspace",
|
||||
"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 .config import urlpatterns as configuration_urls
|
||||
from .cycle import urlpatterns as cycle_urls
|
||||
from .dashboard import urlpatterns as dashboard_urls
|
||||
from .estimate import urlpatterns as estimate_urls
|
||||
from .external import urlpatterns as external_urls
|
||||
from .importer import urlpatterns as importer_urls
|
||||
@@ -29,7 +28,6 @@ urlpatterns = [
|
||||
*authentication_urls,
|
||||
*configuration_urls,
|
||||
*cycle_urls,
|
||||
*dashboard_urls,
|
||||
*estimate_urls,
|
||||
*external_urls,
|
||||
*importer_urls,
|
||||
@@ -47,4 +45,4 @@ urlpatterns = [
|
||||
*workspace_urls,
|
||||
*api_urls,
|
||||
*webhook_urls,
|
||||
]
|
||||
]
|
||||
@@ -4,7 +4,6 @@ from django.urls import path
|
||||
from plane.app.views import (
|
||||
FileAssetEndpoint,
|
||||
UserAssetsEndpoint,
|
||||
FileAssetViewSet,
|
||||
)
|
||||
|
||||
|
||||
@@ -29,13 +28,4 @@ urlpatterns = [
|
||||
UserAssetsEndpoint.as_view(),
|
||||
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 (
|
||||
# Authentication
|
||||
SignUpEndpoint,
|
||||
SignInEndpoint,
|
||||
SignOutEndpoint,
|
||||
MagicGenerateEndpoint,
|
||||
MagicSignInEndpoint,
|
||||
MagicSignInGenerateEndpoint,
|
||||
OauthEndpoint,
|
||||
EmailCheckEndpoint,
|
||||
## End Authentication
|
||||
# Auth Extended
|
||||
ForgotPasswordEndpoint,
|
||||
VerifyEmailEndpoint,
|
||||
ResetPasswordEndpoint,
|
||||
RequestEmailVerificationEndpoint,
|
||||
ChangePasswordEndpoint,
|
||||
## End Auth Extender
|
||||
# API Tokens
|
||||
@@ -25,21 +27,24 @@ from plane.app.views import (
|
||||
|
||||
urlpatterns = [
|
||||
# Social Auth
|
||||
path("email-check/", EmailCheckEndpoint.as_view(), name="email"),
|
||||
path("social-auth/", OauthEndpoint.as_view(), name="oauth"),
|
||||
# Auth
|
||||
path("sign-up/", SignUpEndpoint.as_view(), name="sign-up"),
|
||||
path("sign-in/", SignInEndpoint.as_view(), name="sign-in"),
|
||||
path("sign-out/", SignOutEndpoint.as_view(), name="sign-out"),
|
||||
# magic sign in
|
||||
# Magic Sign In/Up
|
||||
path(
|
||||
"magic-generate/",
|
||||
MagicGenerateEndpoint.as_view(),
|
||||
name="magic-generate",
|
||||
),
|
||||
path(
|
||||
"magic-sign-in/", MagicSignInEndpoint.as_view(), name="magic-sign-in"
|
||||
"magic-generate/", MagicSignInGenerateEndpoint.as_view(), name="magic-generate"
|
||||
),
|
||||
path("magic-sign-in/", MagicSignInEndpoint.as_view(), name="magic-sign-in"),
|
||||
path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
|
||||
# Email verification
|
||||
path("email-verify/", VerifyEmailEndpoint.as_view(), name="email-verify"),
|
||||
path(
|
||||
"request-email-verify/",
|
||||
RequestEmailVerificationEndpoint.as_view(),
|
||||
name="request-reset-email",
|
||||
),
|
||||
# Password Manipulation
|
||||
path(
|
||||
"users/me/change-password/",
|
||||
@@ -58,8 +63,6 @@ urlpatterns = [
|
||||
),
|
||||
# API Tokens
|
||||
path("api-tokens/", ApiTokenEndpoint.as_view(), name="api-tokens"),
|
||||
path(
|
||||
"api-tokens/<uuid:pk>/", ApiTokenEndpoint.as_view(), name="api-tokens"
|
||||
),
|
||||
path("api-tokens/<uuid:pk>/", ApiTokenEndpoint.as_view(), name="api-tokens"),
|
||||
## End API Tokens
|
||||
]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from django.urls import path
|
||||
|
||||
|
||||
from plane.app.views import ConfigurationEndpoint, MobileConfigurationEndpoint
|
||||
from plane.app.views import ConfigurationEndpoint
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
@@ -9,9 +9,4 @@ urlpatterns = [
|
||||
ConfigurationEndpoint.as_view(),
|
||||
name="configuration",
|
||||
),
|
||||
path(
|
||||
"mobile-configs/",
|
||||
MobileConfigurationEndpoint.as_view(),
|
||||
name="configuration",
|
||||
),
|
||||
]
|
||||
]
|
||||
@@ -7,7 +7,7 @@ from plane.app.views import (
|
||||
CycleDateCheckEndpoint,
|
||||
CycleFavoriteViewSet,
|
||||
TransferCycleIssueEndpoint,
|
||||
CycleUserPropertiesEndpoint,
|
||||
CycleIssueGroupedEndpoint,
|
||||
)
|
||||
|
||||
|
||||
@@ -45,7 +45,12 @@ urlpatterns = [
|
||||
name="project-issue-cycle",
|
||||
),
|
||||
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(
|
||||
{
|
||||
"get": "retrieve",
|
||||
@@ -85,9 +90,4 @@ urlpatterns = [
|
||||
TransferCycleIssueEndpoint.as_view(),
|
||||
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",
|
||||
),
|
||||
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(
|
||||
{
|
||||
"get": "retrieve",
|
||||
|
||||
@@ -3,6 +3,8 @@ from django.urls import path
|
||||
|
||||
from plane.app.views import (
|
||||
IssueViewSet,
|
||||
IssueListEndpoint,
|
||||
IssueListGroupedEndpoint,
|
||||
LabelViewSet,
|
||||
BulkCreateIssueLabelsEndpoint,
|
||||
BulkDeleteIssuesEndpoint,
|
||||
@@ -35,6 +37,16 @@ urlpatterns = [
|
||||
),
|
||||
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(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:pk>/",
|
||||
IssueViewSet.as_view(
|
||||
@@ -235,7 +247,7 @@ urlpatterns = [
|
||||
## End Comment Reactions
|
||||
## IssueProperty
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/user-properties/",
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-display-properties/",
|
||||
IssueUserDisplayPropertyEndpoint.as_view(),
|
||||
name="project-issue-display-properties",
|
||||
),
|
||||
@@ -275,17 +287,16 @@ urlpatterns = [
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-relation/",
|
||||
IssueRelationViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="issue-relation",
|
||||
),
|
||||
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(
|
||||
{
|
||||
"post": "remove_relation",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="issue-relation",
|
||||
|
||||
@@ -7,7 +7,7 @@ from plane.app.views import (
|
||||
ModuleLinkViewSet,
|
||||
ModuleFavoriteViewSet,
|
||||
BulkImportModulesEndpoint,
|
||||
ModuleUserPropertiesEndpoint,
|
||||
ModuleIssueGroupedEndpoint,
|
||||
)
|
||||
|
||||
|
||||
@@ -35,26 +35,22 @@ urlpatterns = [
|
||||
name="project-modules",
|
||||
),
|
||||
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(
|
||||
{
|
||||
"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",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="project-module-issues",
|
||||
),
|
||||
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(
|
||||
{
|
||||
"get": "retrieve",
|
||||
@@ -111,9 +107,4 @@ urlpatterns = [
|
||||
BulkImportModulesEndpoint.as_view(),
|
||||
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,
|
||||
UnreadNotificationEndpoint,
|
||||
MarkAllReadNotificationViewSet,
|
||||
UserNotificationPreferenceEndpoint,
|
||||
)
|
||||
|
||||
|
||||
@@ -64,9 +63,4 @@ urlpatterns = [
|
||||
),
|
||||
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,
|
||||
ProjectPublicCoverImagesEndpoint,
|
||||
ProjectDeployBoardViewSet,
|
||||
UserProjectRolesEndpoint,
|
||||
)
|
||||
|
||||
|
||||
@@ -66,7 +65,7 @@ urlpatterns = [
|
||||
name="project-member-invite",
|
||||
),
|
||||
path(
|
||||
"users/me/workspaces/<str:slug>/projects/invitations/",
|
||||
"users/me/invitations/projects/",
|
||||
UserProjectInvitationsViewset.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
@@ -76,12 +75,7 @@ urlpatterns = [
|
||||
name="user-project-invitations",
|
||||
),
|
||||
path(
|
||||
"users/me/workspaces/<str:slug>/project-roles/",
|
||||
UserProjectRolesEndpoint.as_view(),
|
||||
name="user-project-roles",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/join/<uuid:pk>/",
|
||||
"workspaces/<str:slug>/projects/join/",
|
||||
ProjectJoinEndpoint.as_view(),
|
||||
name="project-join",
|
||||
),
|
||||
@@ -175,4 +169,4 @@ urlpatterns = [
|
||||
),
|
||||
name="project-deploy-board",
|
||||
),
|
||||
]
|
||||
]
|
||||
@@ -7,7 +7,6 @@ from plane.app.views import (
|
||||
UpdateUserTourCompletedEndpoint,
|
||||
UserActivityEndpoint,
|
||||
ChangePasswordEndpoint,
|
||||
SetUserPasswordEndpoint,
|
||||
## End User
|
||||
## Workspaces
|
||||
UserWorkSpacesEndpoint,
|
||||
@@ -64,7 +63,7 @@ urlpatterns = [
|
||||
name="user-tour",
|
||||
),
|
||||
path(
|
||||
"users/me/activities/",
|
||||
"users/workspaces/<str:slug>/activities/",
|
||||
UserActivityEndpoint.as_view(),
|
||||
name="user-activities",
|
||||
),
|
||||
@@ -90,10 +89,5 @@ urlpatterns = [
|
||||
UserWorkspaceDashboardEndpoint.as_view(),
|
||||
name="user-workspace-dashboard",
|
||||
),
|
||||
path(
|
||||
"users/me/set-password/",
|
||||
SetUserPasswordEndpoint.as_view(),
|
||||
name="set-password",
|
||||
),
|
||||
## End User Graph
|
||||
]
|
||||
|
||||
@@ -18,10 +18,6 @@ from plane.app.views import (
|
||||
WorkspaceUserProfileEndpoint,
|
||||
WorkspaceUserProfileIssuesEndpoint,
|
||||
WorkspaceLabelsEndpoint,
|
||||
WorkspaceProjectMemberEndpoint,
|
||||
WorkspaceUserPropertiesEndpoint,
|
||||
WorkspaceStatesEndpoint,
|
||||
WorkspaceEstimatesEndpoint,
|
||||
)
|
||||
|
||||
|
||||
@@ -69,7 +65,6 @@ urlpatterns = [
|
||||
{
|
||||
"delete": "destroy",
|
||||
"get": "retrieve",
|
||||
"patch": "partial_update",
|
||||
}
|
||||
),
|
||||
name="workspace-invitations",
|
||||
@@ -96,11 +91,6 @@ urlpatterns = [
|
||||
WorkSpaceMemberViewSet.as_view({"get": "list"}),
|
||||
name="workspace-member",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/project-members/",
|
||||
WorkspaceProjectMemberEndpoint.as_view(),
|
||||
name="workspace-member-roles",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/members/<uuid:pk>/",
|
||||
WorkSpaceMemberViewSet.as_view(
|
||||
@@ -204,19 +194,4 @@ urlpatterns = [
|
||||
WorkspaceLabelsEndpoint.as_view(),
|
||||
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
|
||||
|
||||
urlpatterns = [
|
||||
@@ -204,14 +204,10 @@ urlpatterns = [
|
||||
path("sign-out/", SignOutEndpoint.as_view(), name="sign-out"),
|
||||
# Magic Sign In/Up
|
||||
path(
|
||||
"magic-generate/",
|
||||
MagicSignInGenerateEndpoint.as_view(),
|
||||
name="magic-generate",
|
||||
"magic-generate/", MagicSignInGenerateEndpoint.as_view(), name="magic-generate"
|
||||
),
|
||||
path(
|
||||
"magic-sign-in/", MagicSignInEndpoint.as_view(), name="magic-sign-in"
|
||||
),
|
||||
path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
|
||||
path("magic-sign-in/", MagicSignInEndpoint.as_view(), name="magic-sign-in"),
|
||||
path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
|
||||
# Email verification
|
||||
path("email-verify/", VerifyEmailEndpoint.as_view(), name="email-verify"),
|
||||
path(
|
||||
@@ -276,9 +272,7 @@ urlpatterns = [
|
||||
# user workspace invitations
|
||||
path(
|
||||
"users/me/invitations/workspaces/",
|
||||
UserWorkspaceInvitationsEndpoint.as_view(
|
||||
{"get": "list", "post": "create"}
|
||||
),
|
||||
UserWorkspaceInvitationsEndpoint.as_view({"get": "list", "post": "create"}),
|
||||
name="user-workspace-invitations",
|
||||
),
|
||||
# user workspace invitation
|
||||
@@ -317,9 +311,7 @@ urlpatterns = [
|
||||
# user project invitations
|
||||
path(
|
||||
"users/me/invitations/projects/",
|
||||
UserProjectInvitationsViewset.as_view(
|
||||
{"get": "list", "post": "create"}
|
||||
),
|
||||
UserProjectInvitationsViewset.as_view({"get": "list", "post": "create"}),
|
||||
name="user-project-invitaions",
|
||||
),
|
||||
## Workspaces ##
|
||||
@@ -1246,7 +1238,7 @@ urlpatterns = [
|
||||
"post": "unarchive",
|
||||
}
|
||||
),
|
||||
name="project-page-unarchive",
|
||||
name="project-page-unarchive"
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-pages/",
|
||||
@@ -1272,22 +1264,19 @@ urlpatterns = [
|
||||
{
|
||||
"post": "unlock",
|
||||
}
|
||||
),
|
||||
)
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/transactions/",
|
||||
PageLogEndpoint.as_view(),
|
||||
name="page-transactions",
|
||||
PageLogEndpoint.as_view(), name="page-transactions"
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/transactions/<uuid:transaction>/",
|
||||
PageLogEndpoint.as_view(),
|
||||
name="page-transactions",
|
||||
PageLogEndpoint.as_view(), name="page-transactions"
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/sub-pages/",
|
||||
SubPagesEndpoint.as_view(),
|
||||
name="sub-page",
|
||||
SubPagesEndpoint.as_view(), name="sub-page"
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/",
|
||||
@@ -1337,9 +1326,7 @@ urlpatterns = [
|
||||
## End Pages
|
||||
# API Tokens
|
||||
path("api-tokens/", ApiTokenEndpoint.as_view(), name="api-tokens"),
|
||||
path(
|
||||
"api-tokens/<uuid:pk>/", ApiTokenEndpoint.as_view(), name="api-tokens"
|
||||
),
|
||||
path("api-tokens/<uuid:pk>/", ApiTokenEndpoint.as_view(), name="api-tokens"),
|
||||
## End API Tokens
|
||||
# Integrations
|
||||
path(
|
||||
|
||||
@@ -11,7 +11,6 @@ from .project import (
|
||||
ProjectFavoritesViewSet,
|
||||
ProjectPublicCoverImagesEndpoint,
|
||||
ProjectDeployBoardViewSet,
|
||||
UserProjectRolesEndpoint,
|
||||
)
|
||||
from .user import (
|
||||
UserEndpoint,
|
||||
@@ -45,10 +44,6 @@ from .workspace import (
|
||||
WorkspaceUserProfileEndpoint,
|
||||
WorkspaceUserProfileIssuesEndpoint,
|
||||
WorkspaceLabelsEndpoint,
|
||||
WorkspaceProjectMemberEndpoint,
|
||||
WorkspaceUserPropertiesEndpoint,
|
||||
WorkspaceStatesEndpoint,
|
||||
WorkspaceEstimatesEndpoint,
|
||||
)
|
||||
from .state import StateViewSet
|
||||
from .view import (
|
||||
@@ -63,11 +58,13 @@ from .cycle import (
|
||||
CycleDateCheckEndpoint,
|
||||
CycleFavoriteViewSet,
|
||||
TransferCycleIssueEndpoint,
|
||||
CycleUserPropertiesEndpoint,
|
||||
CycleIssueGroupedEndpoint,
|
||||
)
|
||||
from .asset import FileAssetEndpoint, UserAssetsEndpoint, FileAssetViewSet
|
||||
from .asset import FileAssetEndpoint, UserAssetsEndpoint
|
||||
from .issue import (
|
||||
IssueViewSet,
|
||||
IssueListEndpoint,
|
||||
IssueListGroupedEndpoint,
|
||||
WorkSpaceIssuesEndpoint,
|
||||
IssueActivityEndpoint,
|
||||
IssueCommentViewSet,
|
||||
@@ -88,19 +85,20 @@ from .issue import (
|
||||
)
|
||||
|
||||
from .auth_extended import (
|
||||
VerifyEmailEndpoint,
|
||||
RequestEmailVerificationEndpoint,
|
||||
ForgotPasswordEndpoint,
|
||||
ResetPasswordEndpoint,
|
||||
ChangePasswordEndpoint,
|
||||
SetUserPasswordEndpoint,
|
||||
EmailCheckEndpoint,
|
||||
MagicGenerateEndpoint,
|
||||
)
|
||||
|
||||
|
||||
from .authentication import (
|
||||
SignUpEndpoint,
|
||||
SignInEndpoint,
|
||||
SignOutEndpoint,
|
||||
MagicSignInEndpoint,
|
||||
MagicSignInGenerateEndpoint,
|
||||
)
|
||||
|
||||
from .module import (
|
||||
@@ -108,7 +106,7 @@ from .module import (
|
||||
ModuleIssueViewSet,
|
||||
ModuleLinkViewSet,
|
||||
ModuleFavoriteViewSet,
|
||||
ModuleUserPropertiesEndpoint,
|
||||
ModuleIssueGroupedEndpoint,
|
||||
)
|
||||
|
||||
from .api import ApiTokenEndpoint
|
||||
@@ -137,16 +135,13 @@ from .page import (
|
||||
PageFavoriteViewSet,
|
||||
PageLogEndpoint,
|
||||
SubPagesEndpoint,
|
||||
CreateIssueFromBlockEndpoint,
|
||||
)
|
||||
|
||||
from .search import GlobalSearchEndpoint, IssueSearchEndpoint
|
||||
|
||||
|
||||
from .external import (
|
||||
GPTIntegrationEndpoint,
|
||||
ReleaseNotesEndpoint,
|
||||
UnsplashEndpoint,
|
||||
)
|
||||
from .external import GPTIntegrationEndpoint, ReleaseNotesEndpoint, UnsplashEndpoint
|
||||
|
||||
from .estimate import (
|
||||
ProjectEstimatePointEndpoint,
|
||||
@@ -167,20 +162,10 @@ from .notification import (
|
||||
NotificationViewSet,
|
||||
UnreadNotificationEndpoint,
|
||||
MarkAllReadNotificationViewSet,
|
||||
UserNotificationPreferenceEndpoint,
|
||||
)
|
||||
|
||||
from .exporter import ExportIssuesEndpoint
|
||||
|
||||
from .config import ConfigurationEndpoint, MobileConfigurationEndpoint
|
||||
from .config import ConfigurationEndpoint
|
||||
|
||||
from .webhook import (
|
||||
WebhookEndpoint,
|
||||
WebhookLogsEndpoint,
|
||||
WebhookSecretRegenerateEndpoint,
|
||||
)
|
||||
|
||||
from .dashboard import (
|
||||
DashboardEndpoint,
|
||||
WidgetsEndpoint
|
||||
)
|
||||
from .webhook import WebhookEndpoint, WebhookLogsEndpoint, WebhookSecretRegenerateEndpoint
|
||||
@@ -61,9 +61,7 @@ class AnalyticsEndpoint(BaseAPIView):
|
||||
)
|
||||
|
||||
# If segment is present it cannot be same as x-axis
|
||||
if segment and (
|
||||
segment not in valid_xaxis_segment or x_axis == segment
|
||||
):
|
||||
if segment and (segment not in valid_xaxis_segment or x_axis == segment):
|
||||
return Response(
|
||||
{
|
||||
"error": "Both segment and x axis cannot be same and segment should be valid"
|
||||
@@ -112,9 +110,7 @@ class AnalyticsEndpoint(BaseAPIView):
|
||||
if x_axis in ["assignees__id"] or segment in ["assignees__id"]:
|
||||
assignee_details = (
|
||||
Issue.issue_objects.filter(
|
||||
workspace__slug=slug,
|
||||
**filters,
|
||||
assignees__avatar__isnull=False,
|
||||
workspace__slug=slug, **filters, assignees__avatar__isnull=False
|
||||
)
|
||||
.order_by("assignees__id")
|
||||
.distinct("assignees__id")
|
||||
@@ -128,9 +124,7 @@ class AnalyticsEndpoint(BaseAPIView):
|
||||
)
|
||||
|
||||
cycle_details = {}
|
||||
if x_axis in ["issue_cycle__cycle_id"] or segment in [
|
||||
"issue_cycle__cycle_id"
|
||||
]:
|
||||
if x_axis in ["issue_cycle__cycle_id"] or segment in ["issue_cycle__cycle_id"]:
|
||||
cycle_details = (
|
||||
Issue.issue_objects.filter(
|
||||
workspace__slug=slug,
|
||||
@@ -192,9 +186,7 @@ class AnalyticViewViewset(BaseViewSet):
|
||||
|
||||
def get_queryset(self):
|
||||
return self.filter_queryset(
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
super().get_queryset().filter(workspace__slug=self.kwargs.get("slug"))
|
||||
)
|
||||
|
||||
|
||||
@@ -204,9 +196,7 @@ class SavedAnalyticEndpoint(BaseAPIView):
|
||||
]
|
||||
|
||||
def get(self, request, slug, analytic_id):
|
||||
analytic_view = AnalyticView.objects.get(
|
||||
pk=analytic_id, workspace__slug=slug
|
||||
)
|
||||
analytic_view = AnalyticView.objects.get(pk=analytic_id, workspace__slug=slug)
|
||||
|
||||
filter = analytic_view.query
|
||||
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 and (
|
||||
segment not in valid_xaxis_segment or x_axis == segment
|
||||
):
|
||||
if segment and (segment not in valid_xaxis_segment or x_axis == segment):
|
||||
return Response(
|
||||
{
|
||||
"error": "Both segment and x axis cannot be same and segment should be valid"
|
||||
@@ -305,9 +293,7 @@ class DefaultAnalyticsEndpoint(BaseAPIView):
|
||||
|
||||
def get(self, request, slug):
|
||||
filters = issue_filters(request.GET, "GET")
|
||||
base_issues = Issue.issue_objects.filter(
|
||||
workspace__slug=slug, **filters
|
||||
)
|
||||
base_issues = Issue.issue_objects.filter(workspace__slug=slug, **filters)
|
||||
|
||||
total_issues = base_issues.count()
|
||||
|
||||
@@ -320,9 +306,7 @@ class DefaultAnalyticsEndpoint(BaseAPIView):
|
||||
)
|
||||
|
||||
open_issues_groups = ["backlog", "unstarted", "started"]
|
||||
open_issues_queryset = state_groups.filter(
|
||||
state__group__in=open_issues_groups
|
||||
)
|
||||
open_issues_queryset = state_groups.filter(state__group__in=open_issues_groups)
|
||||
|
||||
open_issues = open_issues_queryset.count()
|
||||
open_issues_classified = (
|
||||
@@ -377,12 +361,10 @@ class DefaultAnalyticsEndpoint(BaseAPIView):
|
||||
.order_by("-count")
|
||||
)
|
||||
|
||||
open_estimate_sum = open_issues_queryset.aggregate(
|
||||
sum=Sum("estimate_point")
|
||||
)["sum"]
|
||||
total_estimate_sum = base_issues.aggregate(sum=Sum("estimate_point"))[
|
||||
open_estimate_sum = open_issues_queryset.aggregate(sum=Sum("estimate_point"))[
|
||||
"sum"
|
||||
]
|
||||
total_estimate_sum = base_issues.aggregate(sum=Sum("estimate_point"))["sum"]
|
||||
|
||||
return Response(
|
||||
{
|
||||
|
||||
@@ -71,9 +71,7 @@ class ApiTokenEndpoint(BaseAPIView):
|
||||
user=request.user,
|
||||
pk=pk,
|
||||
)
|
||||
serializer = APITokenSerializer(
|
||||
api_token, data=request.data, partial=True
|
||||
)
|
||||
serializer = APITokenSerializer(api_token, data=request.data, partial=True)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
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
|
||||
|
||||
# Module imports
|
||||
from .base import BaseAPIView, BaseViewSet
|
||||
from .base import BaseAPIView
|
||||
from plane.db.models import FileAsset, Workspace
|
||||
from plane.app.serializers import FileAssetSerializer
|
||||
|
||||
|
||||
class FileAssetEndpoint(BaseAPIView):
|
||||
parser_classes = (
|
||||
MultiPartParser,
|
||||
FormParser,
|
||||
JSONParser,
|
||||
)
|
||||
parser_classes = (MultiPartParser, FormParser, JSONParser,)
|
||||
|
||||
"""
|
||||
A viewset for viewing and editing task instances.
|
||||
@@ -24,18 +20,10 @@ class FileAssetEndpoint(BaseAPIView):
|
||||
asset_key = str(workspace_id) + "/" + asset_key
|
||||
files = FileAsset.objects.filter(asset=asset_key)
|
||||
if files.exists():
|
||||
serializer = FileAssetSerializer(
|
||||
files, context={"request": request}, many=True
|
||||
)
|
||||
return Response(
|
||||
{"data": serializer.data, "status": True},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
serializer = FileAssetSerializer(files, context={"request": request}, many=True)
|
||||
return Response({"data": serializer.data, "status": True}, status=status.HTTP_200_OK)
|
||||
else:
|
||||
return Response(
|
||||
{"error": "Asset key does not exist", "status": False},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
return Response({"error": "Asset key does not exist", "status": False}, status=status.HTTP_200_OK)
|
||||
|
||||
def post(self, request, slug):
|
||||
serializer = FileAssetSerializer(data=request.data)
|
||||
@@ -45,20 +33,11 @@ class FileAssetEndpoint(BaseAPIView):
|
||||
serializer.save(workspace_id=workspace.id)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def delete(self, request, workspace_id, asset_key):
|
||||
|
||||
def patch(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 = True
|
||||
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.is_deleted = request.data.get("is_deleted", file_asset.is_deleted)
|
||||
file_asset.save()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@@ -67,22 +46,12 @@ class UserAssetsEndpoint(BaseAPIView):
|
||||
parser_classes = (MultiPartParser, FormParser)
|
||||
|
||||
def get(self, request, asset_key):
|
||||
files = FileAsset.objects.filter(
|
||||
asset=asset_key, created_by=request.user
|
||||
)
|
||||
files = FileAsset.objects.filter(asset=asset_key, created_by=request.user)
|
||||
if files.exists():
|
||||
serializer = FileAssetSerializer(
|
||||
files, context={"request": request}
|
||||
)
|
||||
return Response(
|
||||
{"data": serializer.data, "status": True},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
serializer = FileAssetSerializer(files, context={"request": request})
|
||||
return Response({"data": serializer.data, "status": True}, status=status.HTTP_200_OK)
|
||||
else:
|
||||
return Response(
|
||||
{"error": "Asset key does not exist", "status": False},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
return Response({"error": "Asset key does not exist", "status": False}, status=status.HTTP_200_OK)
|
||||
|
||||
def post(self, request):
|
||||
serializer = FileAssetSerializer(data=request.data)
|
||||
@@ -91,10 +60,11 @@ class UserAssetsEndpoint(BaseAPIView):
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
def delete(self, request, asset_key):
|
||||
file_asset = FileAsset.objects.get(
|
||||
asset=asset_key, created_by=request.user
|
||||
)
|
||||
file_asset.is_deleted = True
|
||||
file_asset.save()
|
||||
file_asset = FileAsset.objects.get(asset=asset_key, created_by=request.user)
|
||||
# Delete the file from storage
|
||||
file_asset.asset.delete(save=False)
|
||||
# Delete the file object
|
||||
file_asset.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
## Python imports
|
||||
import uuid
|
||||
import os
|
||||
import json
|
||||
import random
|
||||
import string
|
||||
import jwt
|
||||
|
||||
## Django imports
|
||||
from django.contrib.auth.tokens import PasswordResetTokenGenerator
|
||||
@@ -12,165 +8,115 @@ from django.utils.encoding import (
|
||||
smart_bytes,
|
||||
DjangoUnicodeDecodeError,
|
||||
)
|
||||
from django.contrib.auth.hashers import make_password
|
||||
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
|
||||
|
||||
## Third Party Imports
|
||||
from rest_framework import status
|
||||
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 sentry_sdk import capture_exception
|
||||
|
||||
## Module imports
|
||||
from . import BaseAPIView
|
||||
from plane.app.serializers import (
|
||||
ChangePasswordSerializer,
|
||||
ResetPasswordSerializer,
|
||||
UserSerializer,
|
||||
)
|
||||
from plane.db.models import User, WorkspaceMemberInvite
|
||||
from plane.license.utils.instance_value import get_configuration_value
|
||||
from plane.db.models import User
|
||||
from plane.bgtasks.email_verification_task import email_verification
|
||||
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):
|
||||
refresh = RefreshToken.for_user(user)
|
||||
return (
|
||||
str(refresh.access_token),
|
||||
str(refresh),
|
||||
)
|
||||
class RequestEmailVerificationEndpoint(BaseAPIView):
|
||||
def get(self, request):
|
||||
token = RefreshToken.for_user(request.user).access_token
|
||||
current_site = request.META.get('HTTP_ORIGIN')
|
||||
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):
|
||||
key = "magic_" + str(email)
|
||||
class VerifyEmailEndpoint(BaseAPIView):
|
||||
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
|
||||
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))
|
||||
)
|
||||
|
||||
# Initialize the redis instance
|
||||
ri = redis_instance()
|
||||
|
||||
# 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 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
|
||||
if not user.is_email_verified:
|
||||
user.is_email_verified = True
|
||||
user.save()
|
||||
return Response(
|
||||
{"email": "Successfully activated"}, status=status.HTTP_200_OK
|
||||
)
|
||||
except jwt.ExpiredSignatureError as _indentifier:
|
||||
return Response(
|
||||
{"email": "Activation expired"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
except jwt.exceptions.DecodeError as _indentifier:
|
||||
return Response(
|
||||
{"email": "Invalid token"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
|
||||
class ForgotPasswordEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
AllowAny,
|
||||
]
|
||||
permission_classes = [permissions.AllowAny]
|
||||
|
||||
def post(self, request):
|
||||
email = request.data.get("email")
|
||||
|
||||
try:
|
||||
validate_email(email)
|
||||
except ValidationError:
|
||||
return Response(
|
||||
{"error": "Please enter a valid email"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
if User.objects.filter(email=email).exists():
|
||||
user = User.objects.get(email=email)
|
||||
uidb64 = urlsafe_base64_encode(smart_bytes(user.id))
|
||||
token = PasswordResetTokenGenerator().make_token(user)
|
||||
|
||||
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(
|
||||
user.first_name, user.email, uidb64, token, current_site
|
||||
)
|
||||
|
||||
return Response(
|
||||
{"message": "Check your email to reset your password"},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
return Response(
|
||||
{"error": "Please check the email"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
{"error": "Please check the email"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
|
||||
class ResetPasswordEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
AllowAny,
|
||||
]
|
||||
permission_classes = [permissions.AllowAny]
|
||||
|
||||
def post(self, request, uidb64, token):
|
||||
try:
|
||||
# Decode the id from the uidb64
|
||||
id = smart_str(urlsafe_base64_decode(uidb64))
|
||||
user = User.objects.get(id=id)
|
||||
|
||||
# check if the token is valid for the user
|
||||
if not PasswordResetTokenGenerator().check_token(user, token):
|
||||
return Response(
|
||||
{"error": "Token is invalid"},
|
||||
{"error": "token is not valid, please check the new one"},
|
||||
status=status.HTTP_401_UNAUTHORIZED,
|
||||
)
|
||||
|
||||
# Reset the password
|
||||
serializer = ResetPasswordSerializer(data=request.data)
|
||||
|
||||
if serializer.is_valid():
|
||||
# set_password also hashes the password that the user will get
|
||||
user.set_password(serializer.data.get("new_password"))
|
||||
user.is_password_autoset = False
|
||||
user.save()
|
||||
|
||||
# Log the user in
|
||||
# Generate access token for the user
|
||||
access_token, refresh_token = get_tokens_for_user(user)
|
||||
data = {
|
||||
"access_token": access_token,
|
||||
"refresh_token": refresh_token,
|
||||
response = {
|
||||
"status": "success",
|
||||
"code": status.HTTP_200_OK,
|
||||
"message": "Password updated successfully",
|
||||
}
|
||||
|
||||
return Response(data, status=status.HTTP_200_OK)
|
||||
return Response(
|
||||
serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
return Response(response)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
except DjangoUnicodeDecodeError as indentifier:
|
||||
return Response(
|
||||
@@ -182,302 +128,24 @@ class ResetPasswordEndpoint(BaseAPIView):
|
||||
class ChangePasswordEndpoint(BaseAPIView):
|
||||
def post(self, request):
|
||||
serializer = ChangePasswordSerializer(data=request.data)
|
||||
|
||||
user = User.objects.get(pk=request.user.id)
|
||||
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(
|
||||
{"error": "Old password is not correct"},
|
||||
{"old_password": ["Wrong password."]},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
# set_password also hashes the password that the user will get
|
||||
user.set_password(serializer.data.get("new_password"))
|
||||
user.is_password_autoset = False
|
||||
user.save()
|
||||
return Response(
|
||||
{"message": "Password updated successfully"},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
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,
|
||||
self.object.set_password(serializer.data.get("new_password"))
|
||||
self.object.save()
|
||||
response = {
|
||||
"status": "success",
|
||||
"code": status.HTTP_200_OK,
|
||||
"message": "Password updated successfully",
|
||||
}
|
||||
expiry = 600
|
||||
|
||||
ri.set(key, json.dumps(value), ex=expiry)
|
||||
return Response(response)
|
||||
|
||||
else:
|
||||
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,
|
||||
)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
# Python imports
|
||||
import os
|
||||
import uuid
|
||||
import random
|
||||
import string
|
||||
import json
|
||||
import requests
|
||||
from requests.exceptions import RequestException
|
||||
|
||||
# Django imports
|
||||
from django.utils import timezone
|
||||
@@ -15,7 +18,8 @@ from rest_framework.response import Response
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework import status
|
||||
from rest_framework_simplejwt.tokens import RefreshToken
|
||||
from sentry_sdk import capture_message
|
||||
|
||||
from sentry_sdk import capture_exception, capture_message
|
||||
|
||||
# Module imports
|
||||
from . import BaseAPIView
|
||||
@@ -27,9 +31,7 @@ from plane.db.models import (
|
||||
ProjectMember,
|
||||
)
|
||||
from plane.settings.redis import redis_instance
|
||||
from plane.license.models import Instance
|
||||
from plane.license.utils.instance_value import get_configuration_value
|
||||
from plane.bgtasks.event_tracking_task import auth_events
|
||||
from plane.bgtasks.magic_link_code_task import magic_link
|
||||
|
||||
|
||||
def get_tokens_for_user(user):
|
||||
@@ -44,16 +46,17 @@ class SignUpEndpoint(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:
|
||||
if not settings.ENABLE_SIGNUP:
|
||||
return Response(
|
||||
{"error": "Instance is not configured"},
|
||||
{
|
||||
"error": "New account creation is disabled. Please contact your site administrator"
|
||||
},
|
||||
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(
|
||||
@@ -61,8 +64,8 @@ class SignUpEndpoint(BaseAPIView):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Validate the email
|
||||
email = email.strip().lower()
|
||||
|
||||
try:
|
||||
validate_email(email)
|
||||
except ValidationError as e:
|
||||
@@ -71,31 +74,6 @@ class SignUpEndpoint(BaseAPIView):
|
||||
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
|
||||
if User.objects.filter(email=email).exists():
|
||||
return Response(
|
||||
@@ -107,103 +85,6 @@ class SignUpEndpoint(BaseAPIView):
|
||||
user.set_password(password)
|
||||
|
||||
# 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_login_time = timezone.now()
|
||||
user.last_login_ip = request.META.get("REMOTE_ADDR")
|
||||
@@ -259,25 +140,186 @@ class SignInEndpoint(BaseAPIView):
|
||||
else 15,
|
||||
member=user,
|
||||
created_by_id=project_member_invite.created_by_id,
|
||||
) for project_member_invite in project_member_invites
|
||||
],
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
# Delete all the invites
|
||||
workspace_member_invites.delete()
|
||||
project_member_invites.delete()
|
||||
|
||||
try:
|
||||
# Send Analytics
|
||||
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
|
||||
],
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
|
||||
# Now add the users to project
|
||||
ProjectMember.objects.bulk_create(
|
||||
[
|
||||
ProjectMember(
|
||||
workspace_id=project_member_invite.workspace_id,
|
||||
role=project_member_invite.role
|
||||
if project_member_invite.role in [5, 10, 15]
|
||||
else 15,
|
||||
member=user,
|
||||
created_by_id=project_member_invite.created_by_id,
|
||||
) for project_member_invite in project_member_invites
|
||||
],
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
|
||||
# Delete all the invites
|
||||
workspace_member_invites.delete()
|
||||
project_member_invites.delete()
|
||||
# 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="EMAIL",
|
||||
first_time=False,
|
||||
)
|
||||
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_IN",
|
||||
},
|
||||
)
|
||||
except RequestException as e:
|
||||
capture_exception(e)
|
||||
|
||||
access_token, refresh_token = get_tokens_for_user(user)
|
||||
data = {
|
||||
@@ -310,22 +352,79 @@ class SignOutEndpoint(BaseAPIView):
|
||||
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):
|
||||
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,
|
||||
)
|
||||
|
||||
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 == "":
|
||||
return Response(
|
||||
@@ -342,20 +441,71 @@ class MagicSignInEndpoint(BaseAPIView):
|
||||
email = data["email"]
|
||||
|
||||
if str(token) == str(user_token):
|
||||
user = User.objects.get(email=email)
|
||||
# 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=False,
|
||||
)
|
||||
if User.objects.filter(email=email).exists():
|
||||
user = User.objects.get(email=email)
|
||||
if not user.is_active:
|
||||
return Response(
|
||||
{
|
||||
"error": "Your account has been deactivated. Please contact your site administrator."
|
||||
},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
try:
|
||||
# Send event to Jitsu for tracking
|
||||
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_login_time = timezone.now()
|
||||
user.last_login_ip = request.META.get("REMOTE_ADDR")
|
||||
@@ -364,10 +514,8 @@ class MagicSignInEndpoint(BaseAPIView):
|
||||
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
|
||||
)
|
||||
workspace_member_invites = WorkspaceMemberInvite.objects.filter(
|
||||
email=user.email, accepted=True
|
||||
)
|
||||
|
||||
WorkspaceMember.objects.bulk_create(
|
||||
@@ -413,8 +561,7 @@ class MagicSignInEndpoint(BaseAPIView):
|
||||
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,
|
||||
)
|
||||
@@ -433,9 +580,7 @@ class MagicSignInEndpoint(BaseAPIView):
|
||||
|
||||
else:
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user