mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
commit
c6e3f1b932
10
.env.example
10
.env.example
@ -1,14 +1,12 @@
|
|||||||
# Database Settings
|
# Database Settings
|
||||||
PGUSER="plane"
|
POSTGRES_USER="plane"
|
||||||
PGPASSWORD="plane"
|
POSTGRES_PASSWORD="plane"
|
||||||
PGHOST="plane-db"
|
POSTGRES_DB="plane"
|
||||||
PGDATABASE="plane"
|
PGDATA="/var/lib/postgresql/data"
|
||||||
DATABASE_URL=postgresql://${PGUSER}:${PGPASSWORD}@${PGHOST}/${PGDATABASE}
|
|
||||||
|
|
||||||
# Redis Settings
|
# Redis Settings
|
||||||
REDIS_HOST="plane-redis"
|
REDIS_HOST="plane-redis"
|
||||||
REDIS_PORT="6379"
|
REDIS_PORT="6379"
|
||||||
REDIS_URL="redis://${REDIS_HOST}:6379/"
|
|
||||||
|
|
||||||
# AWS Settings
|
# AWS Settings
|
||||||
AWS_REGION=""
|
AWS_REGION=""
|
||||||
|
3
.github/ISSUE_TEMPLATE/--bug-report.yaml
vendored
3
.github/ISSUE_TEMPLATE/--bug-report.yaml
vendored
@ -1,7 +1,8 @@
|
|||||||
name: Bug report
|
name: Bug report
|
||||||
description: Create a bug report to help us improve Plane
|
description: Create a bug report to help us improve Plane
|
||||||
title: "[bug]: "
|
title: "[bug]: "
|
||||||
labels: [bug, need testing]
|
labels: [🐛bug]
|
||||||
|
assignees: [srinivaspendem, pushya-plane]
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
name: Feature request
|
name: Feature request
|
||||||
description: Suggest a feature to improve Plane
|
description: Suggest a feature to improve Plane
|
||||||
title: "[feature]: "
|
title: "[feature]: "
|
||||||
labels: [feature]
|
labels: [✨feature]
|
||||||
|
assignees: [srinivaspendem, pushya-plane]
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
|
157
.github/workflows/build-branch.yml
vendored
157
.github/workflows/build-branch.yml
vendored
@ -1,61 +1,30 @@
|
|||||||
name: Branch Build
|
name: Branch Build
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
workflow_dispatch:
|
||||||
types:
|
inputs:
|
||||||
- closed
|
branch_name:
|
||||||
|
description: "Branch Name"
|
||||||
|
required: true
|
||||||
|
default: "preview"
|
||||||
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
- preview
|
- preview
|
||||||
- qa
|
|
||||||
- develop
|
- develop
|
||||||
- release-*
|
|
||||||
release:
|
release:
|
||||||
types: [released, prereleased]
|
types: [released, prereleased]
|
||||||
|
|
||||||
env:
|
env:
|
||||||
TARGET_BRANCH: ${{ github.event.pull_request.base.ref || github.event.release.target_commitish }}
|
TARGET_BRANCH: ${{ inputs.branch_name || github.ref_name || github.event.release.target_commitish }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
branch_build_setup:
|
branch_build_setup:
|
||||||
if: ${{ (github.event_name == 'pull_request' && github.event.action =='closed' && github.event.pull_request.merged == true) || github.event_name == 'release' }}
|
|
||||||
name: Build-Push Web/Space/API/Proxy Docker Image
|
name: Build-Push Web/Space/API/Proxy Docker Image
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out the repo
|
- name: Check out the repo
|
||||||
uses: actions/checkout@v3.3.0
|
uses: actions/checkout@v3.3.0
|
||||||
|
|
||||||
- name: Uploading Proxy Source
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: proxy-src-code
|
|
||||||
path: ./nginx
|
|
||||||
- name: Uploading Backend Source
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: backend-src-code
|
|
||||||
path: ./apiserver
|
|
||||||
- name: Uploading Web Source
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: web-src-code
|
|
||||||
path: |
|
|
||||||
./
|
|
||||||
!./apiserver
|
|
||||||
!./nginx
|
|
||||||
!./deploy
|
|
||||||
!./space
|
|
||||||
- name: Uploading Space Source
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: space-src-code
|
|
||||||
path: |
|
|
||||||
./
|
|
||||||
!./apiserver
|
|
||||||
!./nginx
|
|
||||||
!./deploy
|
|
||||||
!./web
|
|
||||||
outputs:
|
outputs:
|
||||||
gh_branch_name: ${{ env.TARGET_BRANCH }}
|
gh_branch_name: ${{ env.TARGET_BRANCH }}
|
||||||
|
|
||||||
@ -63,33 +32,38 @@ jobs:
|
|||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
needs: [branch_build_setup]
|
needs: [branch_build_setup]
|
||||||
env:
|
env:
|
||||||
FRONTEND_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend${{ secrets.DOCKER_REPO_SUFFIX || '' }}:${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
FRONTEND_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend:${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
||||||
steps:
|
steps:
|
||||||
- name: Set Frontend Docker Tag
|
- name: Set Frontend Docker Tag
|
||||||
run: |
|
run: |
|
||||||
if [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ] && [ "${{ github.event_name }}" == "release" ]; then
|
if [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ] && [ "${{ github.event_name }}" == "release" ]; then
|
||||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend${{ secrets.DOCKER_REPO_SUFFIX || '' }}:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend${{ secrets.DOCKER_REPO_SUFFIX || '' }}:${{ github.event.release.tag_name }}
|
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
|
elif [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ]; then
|
||||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend${{ secrets.DOCKER_REPO_SUFFIX || '' }}:stable
|
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend:stable
|
||||||
else
|
else
|
||||||
TAG=${{ env.FRONTEND_TAG }}
|
TAG=${{ env.FRONTEND_TAG }}
|
||||||
fi
|
fi
|
||||||
echo "FRONTEND_TAG=${TAG}" >> $GITHUB_ENV
|
echo "FRONTEND_TAG=${TAG}" >> $GITHUB_ENV
|
||||||
|
- name: Docker Setup QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3.0.0
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2.5.0
|
uses: docker/setup-buildx-action@v3.0.0
|
||||||
|
with:
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
buildkitd-flags: "--allow-insecure-entitlement security.insecure"
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v2.1.0
|
uses: docker/login-action@v3.0.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
- name: Downloading Web Source Code
|
|
||||||
uses: actions/download-artifact@v3
|
- name: Check out the repo
|
||||||
with:
|
uses: actions/checkout@v4.1.1
|
||||||
name: web-src-code
|
|
||||||
|
|
||||||
- name: Build and Push Frontend to Docker Container Registry
|
- name: Build and Push Frontend to Docker Container Registry
|
||||||
uses: docker/build-push-action@v4.0.0
|
uses: docker/build-push-action@v5.1.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./web/Dockerfile.web
|
file: ./web/Dockerfile.web
|
||||||
@ -105,33 +79,39 @@ jobs:
|
|||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
needs: [branch_build_setup]
|
needs: [branch_build_setup]
|
||||||
env:
|
env:
|
||||||
SPACE_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-space${{ secrets.DOCKER_REPO_SUFFIX || '' }}:${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
SPACE_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-space:${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
||||||
steps:
|
steps:
|
||||||
- name: Set Space Docker Tag
|
- name: Set Space Docker Tag
|
||||||
run: |
|
run: |
|
||||||
if [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ] && [ "${{ github.event_name }}" == "release" ]; then
|
if [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ] && [ "${{ github.event_name }}" == "release" ]; then
|
||||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-space${{ secrets.DOCKER_REPO_SUFFIX || '' }}:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-space${{ secrets.DOCKER_REPO_SUFFIX || '' }}:${{ github.event.release.tag_name }}
|
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
|
elif [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ]; then
|
||||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-space${{ secrets.DOCKER_REPO_SUFFIX || '' }}:stable
|
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-space:stable
|
||||||
else
|
else
|
||||||
TAG=${{ env.SPACE_TAG }}
|
TAG=${{ env.SPACE_TAG }}
|
||||||
fi
|
fi
|
||||||
echo "SPACE_TAG=${TAG}" >> $GITHUB_ENV
|
echo "SPACE_TAG=${TAG}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Docker Setup QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3.0.0
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2.5.0
|
uses: docker/setup-buildx-action@v3.0.0
|
||||||
|
with:
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
buildkitd-flags: "--allow-insecure-entitlement security.insecure"
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v2.1.0
|
uses: docker/login-action@v3.0.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
- name: Downloading Space Source Code
|
|
||||||
uses: actions/download-artifact@v3
|
- name: Check out the repo
|
||||||
with:
|
uses: actions/checkout@v4.1.1
|
||||||
name: space-src-code
|
|
||||||
|
|
||||||
- name: Build and Push Space to Docker Hub
|
- name: Build and Push Space to Docker Hub
|
||||||
uses: docker/build-push-action@v4.0.0
|
uses: docker/build-push-action@v5.1.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./space/Dockerfile.space
|
file: ./space/Dockerfile.space
|
||||||
@ -147,36 +127,42 @@ jobs:
|
|||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
needs: [branch_build_setup]
|
needs: [branch_build_setup]
|
||||||
env:
|
env:
|
||||||
BACKEND_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-backend${{ secrets.DOCKER_REPO_SUFFIX || '' }}:${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
BACKEND_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-backend:${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
||||||
steps:
|
steps:
|
||||||
- name: Set Backend Docker Tag
|
- name: Set Backend Docker Tag
|
||||||
run: |
|
run: |
|
||||||
if [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ] && [ "${{ github.event_name }}" == "release" ]; then
|
if [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ] && [ "${{ github.event_name }}" == "release" ]; then
|
||||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-backend${{ secrets.DOCKER_REPO_SUFFIX || '' }}:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-backend${{ secrets.DOCKER_REPO_SUFFIX || '' }}:${{ github.event.release.tag_name }}
|
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
|
elif [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ]; then
|
||||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-backend${{ secrets.DOCKER_REPO_SUFFIX || '' }}:stable
|
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-backend:stable
|
||||||
else
|
else
|
||||||
TAG=${{ env.BACKEND_TAG }}
|
TAG=${{ env.BACKEND_TAG }}
|
||||||
fi
|
fi
|
||||||
echo "BACKEND_TAG=${TAG}" >> $GITHUB_ENV
|
echo "BACKEND_TAG=${TAG}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Docker Setup QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3.0.0
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2.5.0
|
uses: docker/setup-buildx-action@v3.0.0
|
||||||
|
with:
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
buildkitd-flags: "--allow-insecure-entitlement security.insecure"
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v2.1.0
|
uses: docker/login-action@v3.0.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
- name: Downloading Backend Source Code
|
|
||||||
uses: actions/download-artifact@v3
|
- name: Check out the repo
|
||||||
with:
|
uses: actions/checkout@v4.1.1
|
||||||
name: backend-src-code
|
|
||||||
|
|
||||||
- name: Build and Push Backend to Docker Hub
|
- name: Build and Push Backend to Docker Hub
|
||||||
uses: docker/build-push-action@v4.0.0
|
uses: docker/build-push-action@v5.1.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: ./apiserver
|
||||||
file: ./Dockerfile.api
|
file: ./apiserver/Dockerfile.api
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ env.BACKEND_TAG }}
|
tags: ${{ env.BACKEND_TAG }}
|
||||||
@ -189,37 +175,42 @@ jobs:
|
|||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
needs: [branch_build_setup]
|
needs: [branch_build_setup]
|
||||||
env:
|
env:
|
||||||
PROXY_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy${{ secrets.DOCKER_REPO_SUFFIX || '' }}:${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
PROXY_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy:${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
||||||
steps:
|
steps:
|
||||||
- name: Set Proxy Docker Tag
|
- name: Set Proxy Docker Tag
|
||||||
run: |
|
run: |
|
||||||
if [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ] && [ "${{ github.event_name }}" == "release" ]; then
|
if [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ] && [ "${{ github.event_name }}" == "release" ]; then
|
||||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy${{ secrets.DOCKER_REPO_SUFFIX || '' }}:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy${{ secrets.DOCKER_REPO_SUFFIX || '' }}:${{ github.event.release.tag_name }}
|
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
|
elif [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ]; then
|
||||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy${{ secrets.DOCKER_REPO_SUFFIX || '' }}:stable
|
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy:stable
|
||||||
else
|
else
|
||||||
TAG=${{ env.PROXY_TAG }}
|
TAG=${{ env.PROXY_TAG }}
|
||||||
fi
|
fi
|
||||||
echo "PROXY_TAG=${TAG}" >> $GITHUB_ENV
|
echo "PROXY_TAG=${TAG}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Docker Setup QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3.0.0
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2.5.0
|
uses: docker/setup-buildx-action@v3.0.0
|
||||||
|
with:
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
buildkitd-flags: "--allow-insecure-entitlement security.insecure"
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v2.1.0
|
uses: docker/login-action@v3.0.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Downloading Proxy Source Code
|
- name: Check out the repo
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/checkout@v4.1.1
|
||||||
with:
|
|
||||||
name: proxy-src-code
|
|
||||||
|
|
||||||
- name: Build and Push Plane-Proxy to Docker Hub
|
- name: Build and Push Plane-Proxy to Docker Hub
|
||||||
uses: docker/build-push-action@v4.0.0
|
uses: docker/build-push-action@v5.1.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: ./nginx
|
||||||
file: ./Dockerfile
|
file: ./nginx/Dockerfile
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
tags: ${{ env.PROXY_TAG }}
|
tags: ${{ env.PROXY_TAG }}
|
||||||
push: true
|
push: true
|
||||||
|
@ -25,7 +25,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Get changed files
|
- name: Get changed files
|
||||||
id: changed-files
|
id: changed-files
|
||||||
uses: tj-actions/changed-files@v38
|
uses: tj-actions/changed-files@v41
|
||||||
with:
|
with:
|
||||||
files_yaml: |
|
files_yaml: |
|
||||||
apiserver:
|
apiserver:
|
||||||
|
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@ -2,10 +2,10 @@ name: "CodeQL"
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ 'develop', 'hot-fix', 'stage-release' ]
|
branches: [ 'develop', 'preview', 'master' ]
|
||||||
pull_request:
|
pull_request:
|
||||||
# The branches below must be a subset of the branches above
|
# The branches below must be a subset of the branches above
|
||||||
branches: [ 'develop' ]
|
branches: [ 'develop', 'preview', 'master' ]
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '53 19 * * 5'
|
- cron: '53 19 * * 5'
|
||||||
|
|
||||||
|
14
.github/workflows/create-sync-pr.yml
vendored
14
.github/workflows/create-sync-pr.yml
vendored
@ -1,25 +1,23 @@
|
|||||||
name: Create Sync Action
|
name: Create Sync Action
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
branches:
|
branches:
|
||||||
- preview
|
- preview
|
||||||
types:
|
|
||||||
- closed
|
|
||||||
env:
|
env:
|
||||||
SOURCE_BRANCH_NAME: ${{github.event.pull_request.base.ref}}
|
SOURCE_BRANCH_NAME: ${{ github.ref_name }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
create_pr:
|
sync_changes:
|
||||||
# Only run the job when a PR is merged
|
|
||||||
if: github.event.pull_request.merged == true
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4.1.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
@ -37,7 +37,7 @@ Meet [Plane](https://plane.so). An open-source software development tool to mana
|
|||||||
|
|
||||||
> Plane is still in its early days, not everything will be perfect yet, and hiccups may happen. Please let us know of any suggestions, ideas, or bugs that you encounter on our [Discord](https://discord.com/invite/A92xrEGCge) or GitHub issues, and we will use your feedback to improve on our upcoming releases.
|
> Plane is still in its early days, not everything will be perfect yet, and hiccups may happen. Please let us know of any suggestions, ideas, or bugs that you encounter on our [Discord](https://discord.com/invite/A92xrEGCge) or GitHub issues, and we will use your feedback to improve on our upcoming releases.
|
||||||
|
|
||||||
The easiest way to get started with Plane is by creating a [Plane Cloud](https://app.plane.so) account. Plane Cloud offers a hosted solution for Plane. If you prefer to self-host Plane, please refer to our [deployment documentation](https://docs.plane.so/self-hosting/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/docker-compose).
|
||||||
|
|
||||||
## ⚡️ Contributors Quick Start
|
## ⚡️ Contributors Quick Start
|
||||||
|
|
||||||
@ -63,7 +63,7 @@ Thats it!
|
|||||||
|
|
||||||
## 🍙 Self Hosting
|
## 🍙 Self Hosting
|
||||||
|
|
||||||
For self hosting environment setup, visit the [Self Hosting](https://docs.plane.so/self-hosting/docker-compose) documentation page
|
For self hosting environment setup, visit the [Self Hosting](https://docs.plane.so/docker-compose) documentation page
|
||||||
|
|
||||||
## 🚀 Features
|
## 🚀 Features
|
||||||
|
|
||||||
|
@ -8,11 +8,11 @@ SENTRY_DSN=""
|
|||||||
SENTRY_ENVIRONMENT="development"
|
SENTRY_ENVIRONMENT="development"
|
||||||
|
|
||||||
# Database Settings
|
# Database Settings
|
||||||
PGUSER="plane"
|
POSTGRES_USER="plane"
|
||||||
PGPASSWORD="plane"
|
POSTGRES_PASSWORD="plane"
|
||||||
PGHOST="plane-db"
|
POSTGRES_HOST="plane-db"
|
||||||
PGDATABASE="plane"
|
POSTGRES_DB="plane"
|
||||||
DATABASE_URL=postgresql://${PGUSER}:${PGPASSWORD}@${PGHOST}/${PGDATABASE}
|
DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}/${POSTGRES_DB}
|
||||||
|
|
||||||
# Oauth variables
|
# Oauth variables
|
||||||
GOOGLE_CLIENT_ID=""
|
GOOGLE_CLIENT_ID=""
|
||||||
@ -39,9 +39,6 @@ OPENAI_API_BASE="https://api.openai.com/v1" # deprecated
|
|||||||
OPENAI_API_KEY="sk-" # deprecated
|
OPENAI_API_KEY="sk-" # deprecated
|
||||||
GPT_ENGINE="gpt-3.5-turbo" # deprecated
|
GPT_ENGINE="gpt-3.5-turbo" # deprecated
|
||||||
|
|
||||||
# Github
|
|
||||||
GITHUB_CLIENT_SECRET="" # For fetching release notes
|
|
||||||
|
|
||||||
# Settings related to Docker
|
# Settings related to Docker
|
||||||
DOCKERIZED=1 # deprecated
|
DOCKERIZED=1 # deprecated
|
||||||
|
|
||||||
|
@ -33,15 +33,10 @@ RUN pip install -r requirements/local.txt --compile --no-cache-dir
|
|||||||
RUN addgroup -S plane && \
|
RUN addgroup -S plane && \
|
||||||
adduser -S captain -G plane
|
adduser -S captain -G plane
|
||||||
|
|
||||||
RUN chown captain.plane /code
|
COPY . .
|
||||||
|
|
||||||
USER captain
|
RUN chown -R captain.plane /code
|
||||||
|
RUN chmod -R +x /code/bin
|
||||||
# Add in Django deps and generate Django's static files
|
|
||||||
|
|
||||||
USER root
|
|
||||||
|
|
||||||
# RUN chmod +x ./bin/takeoff ./bin/worker ./bin/beat
|
|
||||||
RUN chmod -R 777 /code
|
RUN chmod -R 777 /code
|
||||||
|
|
||||||
USER captain
|
USER captain
|
||||||
|
@ -26,7 +26,9 @@ def update_description():
|
|||||||
updated_issues.append(issue)
|
updated_issues.append(issue)
|
||||||
|
|
||||||
Issue.objects.bulk_update(
|
Issue.objects.bulk_update(
|
||||||
updated_issues, ["description_html", "description_stripped"], batch_size=100
|
updated_issues,
|
||||||
|
["description_html", "description_stripped"],
|
||||||
|
batch_size=100,
|
||||||
)
|
)
|
||||||
print("Success")
|
print("Success")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -40,7 +42,9 @@ def update_comments():
|
|||||||
updated_issue_comments = []
|
updated_issue_comments = []
|
||||||
|
|
||||||
for issue_comment in issue_comments:
|
for issue_comment in issue_comments:
|
||||||
issue_comment.comment_html = f"<p>{issue_comment.comment_stripped}</p>"
|
issue_comment.comment_html = (
|
||||||
|
f"<p>{issue_comment.comment_stripped}</p>"
|
||||||
|
)
|
||||||
updated_issue_comments.append(issue_comment)
|
updated_issue_comments.append(issue_comment)
|
||||||
|
|
||||||
IssueComment.objects.bulk_update(
|
IssueComment.objects.bulk_update(
|
||||||
@ -99,7 +103,9 @@ def updated_issue_sort_order():
|
|||||||
issue.sort_order = issue.sequence_id * random.randint(100, 500)
|
issue.sort_order = issue.sequence_id * random.randint(100, 500)
|
||||||
updated_issues.append(issue)
|
updated_issues.append(issue)
|
||||||
|
|
||||||
Issue.objects.bulk_update(updated_issues, ["sort_order"], batch_size=100)
|
Issue.objects.bulk_update(
|
||||||
|
updated_issues, ["sort_order"], batch_size=100
|
||||||
|
)
|
||||||
print("Success")
|
print("Success")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
print(e)
|
||||||
@ -137,7 +143,9 @@ def update_project_cover_images():
|
|||||||
project.cover_image = project_cover_images[random.randint(0, 19)]
|
project.cover_image = project_cover_images[random.randint(0, 19)]
|
||||||
updated_projects.append(project)
|
updated_projects.append(project)
|
||||||
|
|
||||||
Project.objects.bulk_update(updated_projects, ["cover_image"], batch_size=100)
|
Project.objects.bulk_update(
|
||||||
|
updated_projects, ["cover_image"], batch_size=100
|
||||||
|
)
|
||||||
print("Success")
|
print("Success")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
print(e)
|
||||||
@ -186,7 +194,9 @@ def update_label_color():
|
|||||||
|
|
||||||
def create_slack_integration():
|
def create_slack_integration():
|
||||||
try:
|
try:
|
||||||
_ = Integration.objects.create(provider="slack", network=2, title="Slack")
|
_ = Integration.objects.create(
|
||||||
|
provider="slack", network=2, title="Slack"
|
||||||
|
)
|
||||||
print("Success")
|
print("Success")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
print(e)
|
||||||
@ -212,12 +222,16 @@ def update_integration_verified():
|
|||||||
|
|
||||||
def update_start_date():
|
def update_start_date():
|
||||||
try:
|
try:
|
||||||
issues = Issue.objects.filter(state__group__in=["started", "completed"])
|
issues = Issue.objects.filter(
|
||||||
|
state__group__in=["started", "completed"]
|
||||||
|
)
|
||||||
updated_issues = []
|
updated_issues = []
|
||||||
for issue in issues:
|
for issue in issues:
|
||||||
issue.start_date = issue.created_at.date()
|
issue.start_date = issue.created_at.date()
|
||||||
updated_issues.append(issue)
|
updated_issues.append(issue)
|
||||||
Issue.objects.bulk_update(updated_issues, ["start_date"], batch_size=500)
|
Issue.objects.bulk_update(
|
||||||
|
updated_issues, ["start_date"], batch_size=500
|
||||||
|
)
|
||||||
print("Success")
|
print("Success")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
print(e)
|
||||||
|
3
apiserver/bin/beat
Normal file → Executable file
3
apiserver/bin/beat
Normal file → Executable file
@ -2,4 +2,7 @@
|
|||||||
set -e
|
set -e
|
||||||
|
|
||||||
python manage.py wait_for_db
|
python manage.py wait_for_db
|
||||||
|
# Wait for migrations
|
||||||
|
python manage.py wait_for_migrations
|
||||||
|
# Run the processes
|
||||||
celery -A plane beat -l info
|
celery -A plane beat -l info
|
@ -1,7 +1,8 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
python manage.py wait_for_db
|
python manage.py wait_for_db
|
||||||
python manage.py migrate
|
# Wait for migrations
|
||||||
|
python manage.py wait_for_migrations
|
||||||
|
|
||||||
# Create the default bucket
|
# Create the default bucket
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
python manage.py wait_for_db
|
python manage.py wait_for_db
|
||||||
python manage.py migrate
|
# Wait for migrations
|
||||||
|
python manage.py wait_for_migrations
|
||||||
|
|
||||||
# Create the default bucket
|
# Create the default bucket
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
@ -2,4 +2,7 @@
|
|||||||
set -e
|
set -e
|
||||||
|
|
||||||
python manage.py wait_for_db
|
python manage.py wait_for_db
|
||||||
|
# Wait for migrations
|
||||||
|
python manage.py wait_for_migrations
|
||||||
|
# Run the processes
|
||||||
celery -A plane worker -l info
|
celery -A plane worker -l info
|
@ -2,10 +2,10 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
os.environ.setdefault(
|
os.environ.setdefault(
|
||||||
'DJANGO_SETTINGS_MODULE',
|
"DJANGO_SETTINGS_MODULE", "plane.settings.production"
|
||||||
'plane.settings.production')
|
)
|
||||||
try:
|
try:
|
||||||
from django.core.management import execute_from_command_line
|
from django.core.management import execute_from_command_line
|
||||||
except ImportError as exc:
|
except ImportError as exc:
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"name": "plane-api",
|
"name": "plane-api",
|
||||||
"version": "0.14.0"
|
"version": "0.15.0"
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
from .celery import app as celery_app
|
from .celery import app as celery_app
|
||||||
|
|
||||||
__all__ = ('celery_app',)
|
__all__ = ("celery_app",)
|
||||||
|
@ -2,4 +2,4 @@ from django.apps import AppConfig
|
|||||||
|
|
||||||
|
|
||||||
class AnalyticsConfig(AppConfig):
|
class AnalyticsConfig(AppConfig):
|
||||||
name = 'plane.analytics'
|
name = "plane.analytics"
|
||||||
|
@ -25,7 +25,10 @@ class APIKeyAuthentication(authentication.BaseAuthentication):
|
|||||||
def validate_api_token(self, token):
|
def validate_api_token(self, token):
|
||||||
try:
|
try:
|
||||||
api_token = APIToken.objects.get(
|
api_token = APIToken.objects.get(
|
||||||
Q(Q(expired_at__gt=timezone.now()) | Q(expired_at__isnull=True)),
|
Q(
|
||||||
|
Q(expired_at__gt=timezone.now())
|
||||||
|
| Q(expired_at__isnull=True)
|
||||||
|
),
|
||||||
token=token,
|
token=token,
|
||||||
is_active=True,
|
is_active=True,
|
||||||
)
|
)
|
||||||
|
@ -1,17 +1,18 @@
|
|||||||
from rest_framework.throttling import SimpleRateThrottle
|
from rest_framework.throttling import SimpleRateThrottle
|
||||||
|
|
||||||
|
|
||||||
class ApiKeyRateThrottle(SimpleRateThrottle):
|
class ApiKeyRateThrottle(SimpleRateThrottle):
|
||||||
scope = 'api_key'
|
scope = "api_key"
|
||||||
rate = '60/minute'
|
rate = "60/minute"
|
||||||
|
|
||||||
def get_cache_key(self, request, view):
|
def get_cache_key(self, request, view):
|
||||||
# Retrieve the API key from the request header
|
# Retrieve the API key from the request header
|
||||||
api_key = request.headers.get('X-Api-Key')
|
api_key = request.headers.get("X-Api-Key")
|
||||||
if not api_key:
|
if not api_key:
|
||||||
return None # Allow the request if there's no API key
|
return None # Allow the request if there's no API key
|
||||||
|
|
||||||
# Use the API key as part of the cache key
|
# Use the API key as part of the cache key
|
||||||
return f'{self.scope}:{api_key}'
|
return f"{self.scope}:{api_key}"
|
||||||
|
|
||||||
def allow_request(self, request, view):
|
def allow_request(self, request, view):
|
||||||
allowed = super().allow_request(request, view)
|
allowed = super().allow_request(request, view)
|
||||||
@ -35,7 +36,7 @@ class ApiKeyRateThrottle(SimpleRateThrottle):
|
|||||||
reset_time = int(now + self.duration)
|
reset_time = int(now + self.duration)
|
||||||
|
|
||||||
# Add headers
|
# Add headers
|
||||||
request.META['X-RateLimit-Remaining'] = max(0, available)
|
request.META["X-RateLimit-Remaining"] = max(0, available)
|
||||||
request.META['X-RateLimit-Reset'] = reset_time
|
request.META["X-RateLimit-Reset"] = reset_time
|
||||||
|
|
||||||
return allowed
|
return allowed
|
@ -13,5 +13,9 @@ from .issue import (
|
|||||||
)
|
)
|
||||||
from .state import StateLiteSerializer, StateSerializer
|
from .state import StateLiteSerializer, StateSerializer
|
||||||
from .cycle import CycleSerializer, CycleIssueSerializer, CycleLiteSerializer
|
from .cycle import CycleSerializer, CycleIssueSerializer, CycleLiteSerializer
|
||||||
from .module import ModuleSerializer, ModuleIssueSerializer, ModuleLiteSerializer
|
from .module import (
|
||||||
|
ModuleSerializer,
|
||||||
|
ModuleIssueSerializer,
|
||||||
|
ModuleLiteSerializer,
|
||||||
|
)
|
||||||
from .inbox import InboxIssueSerializer
|
from .inbox import InboxIssueSerializer
|
@ -97,9 +97,11 @@ class BaseSerializer(serializers.ModelSerializer):
|
|||||||
exp_serializer = expansion[expand](
|
exp_serializer = expansion[expand](
|
||||||
getattr(instance, expand)
|
getattr(instance, expand)
|
||||||
)
|
)
|
||||||
response[expand] = exp_serializer.data
|
response[expand] = exp_serializer.data
|
||||||
else:
|
else:
|
||||||
# You might need to handle this case differently
|
# You might need to handle this case differently
|
||||||
response[expand] = getattr(instance, f"{expand}_id", None)
|
response[expand] = getattr(
|
||||||
|
instance, f"{expand}_id", None
|
||||||
|
)
|
||||||
|
|
||||||
return response
|
return response
|
@ -23,7 +23,9 @@ class CycleSerializer(BaseSerializer):
|
|||||||
and data.get("end_date", None) is not None
|
and data.get("end_date", None) is not None
|
||||||
and data.get("start_date", None) > data.get("end_date", None)
|
and data.get("start_date", None) > data.get("end_date", None)
|
||||||
):
|
):
|
||||||
raise serializers.ValidationError("Start date cannot exceed end date")
|
raise serializers.ValidationError(
|
||||||
|
"Start date cannot exceed end date"
|
||||||
|
)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -55,7 +57,6 @@ class CycleIssueSerializer(BaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class CycleLiteSerializer(BaseSerializer):
|
class CycleLiteSerializer(BaseSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Cycle
|
model = Cycle
|
||||||
fields = "__all__"
|
fields = "__all__"
|
@ -2,8 +2,8 @@
|
|||||||
from .base import BaseSerializer
|
from .base import BaseSerializer
|
||||||
from plane.db.models import InboxIssue
|
from plane.db.models import InboxIssue
|
||||||
|
|
||||||
class InboxIssueSerializer(BaseSerializer):
|
|
||||||
|
|
||||||
|
class InboxIssueSerializer(BaseSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = InboxIssue
|
model = InboxIssue
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
@ -27,6 +27,7 @@ from .module import ModuleSerializer, ModuleLiteSerializer
|
|||||||
from .user import UserLiteSerializer
|
from .user import UserLiteSerializer
|
||||||
from .state import StateLiteSerializer
|
from .state import StateLiteSerializer
|
||||||
|
|
||||||
|
|
||||||
class IssueSerializer(BaseSerializer):
|
class IssueSerializer(BaseSerializer):
|
||||||
assignees = serializers.ListField(
|
assignees = serializers.ListField(
|
||||||
child=serializers.PrimaryKeyRelatedField(
|
child=serializers.PrimaryKeyRelatedField(
|
||||||
@ -66,12 +67,14 @@ class IssueSerializer(BaseSerializer):
|
|||||||
and data.get("target_date", None) is not None
|
and data.get("target_date", None) is not None
|
||||||
and data.get("start_date", None) > data.get("target_date", None)
|
and data.get("start_date", None) > data.get("target_date", None)
|
||||||
):
|
):
|
||||||
raise serializers.ValidationError("Start date cannot exceed target date")
|
raise serializers.ValidationError(
|
||||||
|
"Start date cannot exceed target date"
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if(data.get("description_html", None) is not None):
|
if data.get("description_html", None) is not None:
|
||||||
parsed = html.fromstring(data["description_html"])
|
parsed = html.fromstring(data["description_html"])
|
||||||
parsed_str = html.tostring(parsed, encoding='unicode')
|
parsed_str = html.tostring(parsed, encoding="unicode")
|
||||||
data["description_html"] = parsed_str
|
data["description_html"] = parsed_str
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -96,7 +99,8 @@ class IssueSerializer(BaseSerializer):
|
|||||||
if (
|
if (
|
||||||
data.get("state")
|
data.get("state")
|
||||||
and not State.objects.filter(
|
and not State.objects.filter(
|
||||||
project_id=self.context.get("project_id"), pk=data.get("state").id
|
project_id=self.context.get("project_id"),
|
||||||
|
pk=data.get("state").id,
|
||||||
).exists()
|
).exists()
|
||||||
):
|
):
|
||||||
raise serializers.ValidationError(
|
raise serializers.ValidationError(
|
||||||
@ -107,7 +111,8 @@ class IssueSerializer(BaseSerializer):
|
|||||||
if (
|
if (
|
||||||
data.get("parent")
|
data.get("parent")
|
||||||
and not Issue.objects.filter(
|
and not Issue.objects.filter(
|
||||||
workspace_id=self.context.get("workspace_id"), pk=data.get("parent").id
|
workspace_id=self.context.get("workspace_id"),
|
||||||
|
pk=data.get("parent").id,
|
||||||
).exists()
|
).exists()
|
||||||
):
|
):
|
||||||
raise serializers.ValidationError(
|
raise serializers.ValidationError(
|
||||||
@ -238,9 +243,13 @@ class IssueSerializer(BaseSerializer):
|
|||||||
]
|
]
|
||||||
if "labels" in self.fields:
|
if "labels" in self.fields:
|
||||||
if "labels" in self.expand:
|
if "labels" in self.expand:
|
||||||
data["labels"] = LabelSerializer(instance.labels.all(), many=True).data
|
data["labels"] = LabelSerializer(
|
||||||
|
instance.labels.all(), many=True
|
||||||
|
).data
|
||||||
else:
|
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
|
return data
|
||||||
|
|
||||||
@ -278,7 +287,8 @@ class IssueLinkSerializer(BaseSerializer):
|
|||||||
# Validation if url already exists
|
# Validation if url already exists
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
if IssueLink.objects.filter(
|
if IssueLink.objects.filter(
|
||||||
url=validated_data.get("url"), issue_id=validated_data.get("issue_id")
|
url=validated_data.get("url"),
|
||||||
|
issue_id=validated_data.get("issue_id"),
|
||||||
).exists():
|
).exists():
|
||||||
raise serializers.ValidationError(
|
raise serializers.ValidationError(
|
||||||
{"error": "URL already exists for this Issue"}
|
{"error": "URL already exists for this Issue"}
|
||||||
@ -324,9 +334,9 @@ class IssueCommentSerializer(BaseSerializer):
|
|||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
try:
|
try:
|
||||||
if(data.get("comment_html", None) is not None):
|
if data.get("comment_html", None) is not None:
|
||||||
parsed = html.fromstring(data["comment_html"])
|
parsed = html.fromstring(data["comment_html"])
|
||||||
parsed_str = html.tostring(parsed, encoding='unicode')
|
parsed_str = html.tostring(parsed, encoding="unicode")
|
||||||
data["comment_html"] = parsed_str
|
data["comment_html"] = parsed_str
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -362,7 +372,6 @@ class ModuleIssueSerializer(BaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class LabelLiteSerializer(BaseSerializer):
|
class LabelLiteSerializer(BaseSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Label
|
model = Label
|
||||||
fields = [
|
fields = [
|
||||||
|
@ -52,7 +52,9 @@ class ModuleSerializer(BaseSerializer):
|
|||||||
and data.get("target_date", None) is not None
|
and data.get("target_date", None) is not None
|
||||||
and data.get("start_date", None) > data.get("target_date", None)
|
and data.get("start_date", None) > data.get("target_date", None)
|
||||||
):
|
):
|
||||||
raise serializers.ValidationError("Start date cannot exceed target date")
|
raise serializers.ValidationError(
|
||||||
|
"Start date cannot exceed target date"
|
||||||
|
)
|
||||||
|
|
||||||
if data.get("members", []):
|
if data.get("members", []):
|
||||||
data["members"] = ProjectMember.objects.filter(
|
data["members"] = ProjectMember.objects.filter(
|
||||||
@ -146,7 +148,8 @@ class ModuleLinkSerializer(BaseSerializer):
|
|||||||
# Validation if url already exists
|
# Validation if url already exists
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
if ModuleLink.objects.filter(
|
if ModuleLink.objects.filter(
|
||||||
url=validated_data.get("url"), module_id=validated_data.get("module_id")
|
url=validated_data.get("url"),
|
||||||
|
module_id=validated_data.get("module_id"),
|
||||||
).exists():
|
).exists():
|
||||||
raise serializers.ValidationError(
|
raise serializers.ValidationError(
|
||||||
{"error": "URL already exists for this Issue"}
|
{"error": "URL already exists for this Issue"}
|
||||||
@ -155,7 +158,6 @@ class ModuleLinkSerializer(BaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class ModuleLiteSerializer(BaseSerializer):
|
class ModuleLiteSerializer(BaseSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Module
|
model = Module
|
||||||
fields = "__all__"
|
fields = "__all__"
|
@ -2,12 +2,17 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
# Module imports
|
# 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
|
from .base import BaseSerializer
|
||||||
|
|
||||||
|
|
||||||
class ProjectSerializer(BaseSerializer):
|
class ProjectSerializer(BaseSerializer):
|
||||||
|
|
||||||
total_members = serializers.IntegerField(read_only=True)
|
total_members = serializers.IntegerField(read_only=True)
|
||||||
total_cycles = serializers.IntegerField(read_only=True)
|
total_cycles = serializers.IntegerField(read_only=True)
|
||||||
total_modules = serializers.IntegerField(read_only=True)
|
total_modules = serializers.IntegerField(read_only=True)
|
||||||
@ -21,7 +26,7 @@ class ProjectSerializer(BaseSerializer):
|
|||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
read_only_fields = [
|
read_only_fields = [
|
||||||
"id",
|
"id",
|
||||||
'emoji',
|
"emoji",
|
||||||
"workspace",
|
"workspace",
|
||||||
"created_at",
|
"created_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
@ -59,12 +64,16 @@ class ProjectSerializer(BaseSerializer):
|
|||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
identifier = validated_data.get("identifier", "").strip().upper()
|
identifier = validated_data.get("identifier", "").strip().upper()
|
||||||
if identifier == "":
|
if identifier == "":
|
||||||
raise serializers.ValidationError(detail="Project Identifier is required")
|
raise serializers.ValidationError(
|
||||||
|
detail="Project Identifier is required"
|
||||||
|
)
|
||||||
|
|
||||||
if ProjectIdentifier.objects.filter(
|
if ProjectIdentifier.objects.filter(
|
||||||
name=identifier, workspace_id=self.context["workspace_id"]
|
name=identifier, workspace_id=self.context["workspace_id"]
|
||||||
).exists():
|
).exists():
|
||||||
raise serializers.ValidationError(detail="Project Identifier is taken")
|
raise serializers.ValidationError(
|
||||||
|
detail="Project Identifier is taken"
|
||||||
|
)
|
||||||
|
|
||||||
project = Project.objects.create(
|
project = Project.objects.create(
|
||||||
**validated_data, workspace_id=self.context["workspace_id"]
|
**validated_data, workspace_id=self.context["workspace_id"]
|
||||||
|
@ -7,9 +7,9 @@ class StateSerializer(BaseSerializer):
|
|||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
# If the default is being provided then make all other states default False
|
# If the default is being provided then make all other states default False
|
||||||
if data.get("default", False):
|
if data.get("default", False):
|
||||||
State.objects.filter(project_id=self.context.get("project_id")).update(
|
State.objects.filter(
|
||||||
default=False
|
project_id=self.context.get("project_id")
|
||||||
)
|
).update(default=False)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -5,6 +5,7 @@ from .base import BaseSerializer
|
|||||||
|
|
||||||
class WorkspaceLiteSerializer(BaseSerializer):
|
class WorkspaceLiteSerializer(BaseSerializer):
|
||||||
"""Lite serializer with only required fields"""
|
"""Lite serializer with only required fields"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Workspace
|
model = Workspace
|
||||||
fields = [
|
fields = [
|
||||||
|
@ -3,7 +3,7 @@ from django.urls import path
|
|||||||
from plane.api.views import ProjectAPIEndpoint
|
from plane.api.views import ProjectAPIEndpoint
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/",
|
"workspaces/<str:slug>/projects/",
|
||||||
ProjectAPIEndpoint.as_view(),
|
ProjectAPIEndpoint.as_view(),
|
||||||
name="project",
|
name="project",
|
||||||
|
@ -41,7 +41,9 @@ class WebhookMixin:
|
|||||||
bulk = False
|
bulk = False
|
||||||
|
|
||||||
def finalize_response(self, request, response, *args, **kwargs):
|
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
|
# Check for the case should webhook be sent
|
||||||
if (
|
if (
|
||||||
@ -104,15 +106,14 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if isinstance(e, ObjectDoesNotExist):
|
if isinstance(e, ObjectDoesNotExist):
|
||||||
model_name = str(exc).split(" matching query does not exist.")[0]
|
|
||||||
return Response(
|
return Response(
|
||||||
{"error": f"{model_name} does not exist."},
|
{"error": f"The required object does not exist."},
|
||||||
status=status.HTTP_404_NOT_FOUND,
|
status=status.HTTP_404_NOT_FOUND,
|
||||||
)
|
)
|
||||||
|
|
||||||
if isinstance(e, KeyError):
|
if isinstance(e, KeyError):
|
||||||
return Response(
|
return Response(
|
||||||
{"error": f"key {e} does not exist"},
|
{"error": f" The required key does not exist."},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -140,7 +141,9 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
|
|||||||
|
|
||||||
def finalize_response(self, request, response, *args, **kwargs):
|
def finalize_response(self, request, response, *args, **kwargs):
|
||||||
# Call super to get the default response
|
# Call super to get the default response
|
||||||
response = super().finalize_response(request, response, *args, **kwargs)
|
response = super().finalize_response(
|
||||||
|
request, response, *args, **kwargs
|
||||||
|
)
|
||||||
|
|
||||||
# Add custom headers if they exist in the request META
|
# Add custom headers if they exist in the request META
|
||||||
ratelimit_remaining = request.META.get("X-RateLimit-Remaining")
|
ratelimit_remaining = request.META.get("X-RateLimit-Remaining")
|
||||||
@ -164,13 +167,17 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
|
|||||||
@property
|
@property
|
||||||
def fields(self):
|
def fields(self):
|
||||||
fields = [
|
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
|
return fields if fields else None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def expand(self):
|
def expand(self):
|
||||||
expand = [
|
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,7 +12,13 @@ from rest_framework import status
|
|||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from .base import BaseAPIView, WebhookMixin
|
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.app.permissions import ProjectEntityPermission
|
||||||
from plane.api.serializers import (
|
from plane.api.serializers import (
|
||||||
CycleSerializer,
|
CycleSerializer,
|
||||||
@ -102,7 +108,9 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.annotate(total_estimates=Sum("issue_cycle__issue__estimate_point"))
|
.annotate(
|
||||||
|
total_estimates=Sum("issue_cycle__issue__estimate_point")
|
||||||
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
completed_estimates=Sum(
|
completed_estimates=Sum(
|
||||||
"issue_cycle__issue__estimate_point",
|
"issue_cycle__issue__estimate_point",
|
||||||
@ -201,7 +209,8 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
# Incomplete Cycles
|
# Incomplete Cycles
|
||||||
if cycle_view == "incomplete":
|
if cycle_view == "incomplete":
|
||||||
queryset = queryset.filter(
|
queryset = queryset.filter(
|
||||||
Q(end_date__gte=timezone.now().date()) | Q(end_date__isnull=True),
|
Q(end_date__gte=timezone.now().date())
|
||||||
|
| Q(end_date__isnull=True),
|
||||||
)
|
)
|
||||||
return self.paginate(
|
return self.paginate(
|
||||||
request=request,
|
request=request,
|
||||||
@ -238,8 +247,12 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
owned_by=request.user,
|
owned_by=request.user,
|
||||||
)
|
)
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
serializer.data, status=status.HTTP_201_CREATED
|
||||||
|
)
|
||||||
|
return Response(
|
||||||
|
serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
@ -249,15 +262,22 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def patch(self, request, slug, project_id, pk):
|
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
|
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:
|
if "sort_order" in request_data:
|
||||||
# Can only change sort order
|
# Can only change sort order
|
||||||
request_data = {
|
request_data = {
|
||||||
"sort_order": request_data.get("sort_order", cycle.sort_order)
|
"sort_order": request_data.get(
|
||||||
|
"sort_order", cycle.sort_order
|
||||||
|
)
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
return Response(
|
return Response(
|
||||||
@ -275,11 +295,13 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
|
|
||||||
def delete(self, request, slug, project_id, pk):
|
def delete(self, request, slug, project_id, pk):
|
||||||
cycle_issues = list(
|
cycle_issues = list(
|
||||||
CycleIssue.objects.filter(cycle_id=self.kwargs.get("pk")).values_list(
|
CycleIssue.objects.filter(
|
||||||
"issue", flat=True
|
cycle_id=self.kwargs.get("pk")
|
||||||
)
|
).values_list("issue", flat=True)
|
||||||
|
)
|
||||||
|
cycle = Cycle.objects.get(
|
||||||
|
workspace__slug=slug, project_id=project_id, pk=pk
|
||||||
)
|
)
|
||||||
cycle = Cycle.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
|
|
||||||
|
|
||||||
issue_activity.delay(
|
issue_activity.delay(
|
||||||
type="cycle.activity.deleted",
|
type="cycle.activity.deleted",
|
||||||
@ -319,7 +341,9 @@ class CycleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return (
|
return (
|
||||||
CycleIssue.objects.annotate(
|
CycleIssue.objects.annotate(
|
||||||
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("issue_id"))
|
sub_issues_count=Issue.issue_objects.filter(
|
||||||
|
parent=OuterRef("issue_id")
|
||||||
|
)
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
@ -342,7 +366,9 @@ class CycleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
issues = (
|
issues = (
|
||||||
Issue.issue_objects.filter(issue_cycle__cycle_id=cycle_id)
|
Issue.issue_objects.filter(issue_cycle__cycle_id=cycle_id)
|
||||||
.annotate(
|
.annotate(
|
||||||
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
|
sub_issues_count=Issue.issue_objects.filter(
|
||||||
|
parent=OuterRef("id")
|
||||||
|
)
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
@ -364,7 +390,9 @@ class CycleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
.values("count")
|
.values("count")
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id"))
|
attachment_count=IssueAttachment.objects.filter(
|
||||||
|
issue=OuterRef("id")
|
||||||
|
)
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
@ -387,14 +415,18 @@ class CycleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
|
|
||||||
if not issues:
|
if not issues:
|
||||||
return Response(
|
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(
|
cycle = Cycle.objects.get(
|
||||||
workspace__slug=slug, project_id=project_id, pk=cycle_id
|
workspace__slug=slug, project_id=project_id, pk=cycle_id
|
||||||
)
|
)
|
||||||
|
|
||||||
if 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(
|
return Response(
|
||||||
{
|
{
|
||||||
"error": "The Cycle has already been completed so no new issues can be added"
|
"error": "The Cycle has already been completed so no new issues can be added"
|
||||||
@ -479,7 +511,10 @@ class CycleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
|
|
||||||
def delete(self, request, slug, project_id, cycle_id, issue_id):
|
def delete(self, request, slug, project_id, cycle_id, issue_id):
|
||||||
cycle_issue = CycleIssue.objects.get(
|
cycle_issue = CycleIssue.objects.get(
|
||||||
issue_id=issue_id, workspace__slug=slug, project_id=project_id, cycle_id=cycle_id
|
issue_id=issue_id,
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
cycle_id=cycle_id,
|
||||||
)
|
)
|
||||||
issue_id = cycle_issue.issue_id
|
issue_id = cycle_issue.issue_id
|
||||||
cycle_issue.delete()
|
cycle_issue.delete()
|
||||||
|
@ -14,7 +14,14 @@ from rest_framework.response import Response
|
|||||||
from .base import BaseAPIView
|
from .base import BaseAPIView
|
||||||
from plane.app.permissions import ProjectLitePermission
|
from plane.app.permissions import ProjectLitePermission
|
||||||
from plane.api.serializers import InboxIssueSerializer, IssueSerializer
|
from plane.api.serializers import InboxIssueSerializer, IssueSerializer
|
||||||
from plane.db.models import InboxIssue, Issue, State, ProjectMember, Project, Inbox
|
from plane.db.models import (
|
||||||
|
InboxIssue,
|
||||||
|
Issue,
|
||||||
|
State,
|
||||||
|
ProjectMember,
|
||||||
|
Project,
|
||||||
|
Inbox,
|
||||||
|
)
|
||||||
from plane.bgtasks.issue_activites_task import issue_activity
|
from plane.bgtasks.issue_activites_task import issue_activity
|
||||||
|
|
||||||
|
|
||||||
@ -43,7 +50,8 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
|||||||
).first()
|
).first()
|
||||||
|
|
||||||
project = Project.objects.get(
|
project = Project.objects.get(
|
||||||
workspace__slug=self.kwargs.get("slug"), pk=self.kwargs.get("project_id")
|
workspace__slug=self.kwargs.get("slug"),
|
||||||
|
pk=self.kwargs.get("project_id"),
|
||||||
)
|
)
|
||||||
|
|
||||||
if inbox is None and not project.inbox_view:
|
if inbox is None and not project.inbox_view:
|
||||||
@ -51,7 +59,8 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
InboxIssue.objects.filter(
|
InboxIssue.objects.filter(
|
||||||
Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True),
|
Q(snoozed_till__gte=timezone.now())
|
||||||
|
| Q(snoozed_till__isnull=True),
|
||||||
workspace__slug=self.kwargs.get("slug"),
|
workspace__slug=self.kwargs.get("slug"),
|
||||||
project_id=self.kwargs.get("project_id"),
|
project_id=self.kwargs.get("project_id"),
|
||||||
inbox_id=inbox.id,
|
inbox_id=inbox.id,
|
||||||
@ -87,7 +96,8 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
|||||||
def post(self, request, slug, project_id):
|
def post(self, request, slug, project_id):
|
||||||
if not request.data.get("issue", {}).get("name", False):
|
if not request.data.get("issue", {}).get("name", False):
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST
|
{"error": "Name is required"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
inbox = Inbox.objects.filter(
|
inbox = Inbox.objects.filter(
|
||||||
@ -117,7 +127,8 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
|||||||
"none",
|
"none",
|
||||||
]:
|
]:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST
|
{"error": "Invalid priority"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create or get state
|
# Create or get state
|
||||||
@ -222,10 +233,14 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
|||||||
"description_html": issue_data.get(
|
"description_html": issue_data.get(
|
||||||
"description_html", issue.description_html
|
"description_html", issue.description_html
|
||||||
),
|
),
|
||||||
"description": issue_data.get("description", issue.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():
|
if issue_serializer.is_valid():
|
||||||
current_instance = issue
|
current_instance = issue
|
||||||
@ -266,7 +281,9 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
|||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
)
|
)
|
||||||
state = State.objects.filter(
|
state = State.objects.filter(
|
||||||
group="cancelled", workspace__slug=slug, project_id=project_id
|
group="cancelled",
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
).first()
|
).first()
|
||||||
if state is not None:
|
if state is not None:
|
||||||
issue.state = state
|
issue.state = state
|
||||||
@ -284,17 +301,22 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
|||||||
if issue.state.name == "Triage":
|
if issue.state.name == "Triage":
|
||||||
# Move to default state
|
# Move to default state
|
||||||
state = State.objects.filter(
|
state = State.objects.filter(
|
||||||
workspace__slug=slug, project_id=project_id, default=True
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
default=True,
|
||||||
).first()
|
).first()
|
||||||
if state is not None:
|
if state is not None:
|
||||||
issue.state = state
|
issue.state = state
|
||||||
issue.save()
|
issue.save()
|
||||||
|
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(
|
||||||
|
serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
return Response(
|
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):
|
def delete(self, request, slug, project_id, issue_id):
|
||||||
|
@ -67,7 +67,9 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return (
|
return (
|
||||||
Issue.issue_objects.annotate(
|
Issue.issue_objects.annotate(
|
||||||
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
|
sub_issues_count=Issue.issue_objects.filter(
|
||||||
|
parent=OuterRef("id")
|
||||||
|
)
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
@ -86,7 +88,9 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
def get(self, request, slug, project_id, pk=None):
|
def get(self, request, slug, project_id, pk=None):
|
||||||
if pk:
|
if pk:
|
||||||
issue = Issue.issue_objects.annotate(
|
issue = Issue.issue_objects.annotate(
|
||||||
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
|
sub_issues_count=Issue.issue_objects.filter(
|
||||||
|
parent=OuterRef("id")
|
||||||
|
)
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
@ -102,7 +106,13 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
|
|
||||||
# Custom ordering for priority and state
|
# Custom ordering for priority and state
|
||||||
priority_order = ["urgent", "high", "medium", "low", "none"]
|
priority_order = ["urgent", "high", "medium", "low", "none"]
|
||||||
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
|
state_order = [
|
||||||
|
"backlog",
|
||||||
|
"unstarted",
|
||||||
|
"started",
|
||||||
|
"completed",
|
||||||
|
"cancelled",
|
||||||
|
]
|
||||||
|
|
||||||
order_by_param = request.GET.get("order_by", "-created_at")
|
order_by_param = request.GET.get("order_by", "-created_at")
|
||||||
|
|
||||||
@ -117,7 +127,9 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
.values("count")
|
.values("count")
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id"))
|
attachment_count=IssueAttachment.objects.filter(
|
||||||
|
issue=OuterRef("id")
|
||||||
|
)
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
@ -127,7 +139,9 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
# Priority Ordering
|
# Priority Ordering
|
||||||
if order_by_param == "priority" or order_by_param == "-priority":
|
if order_by_param == "priority" or order_by_param == "-priority":
|
||||||
priority_order = (
|
priority_order = (
|
||||||
priority_order 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(
|
issue_queryset = issue_queryset.annotate(
|
||||||
priority_order=Case(
|
priority_order=Case(
|
||||||
@ -175,7 +189,9 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
else order_by_param
|
else order_by_param
|
||||||
)
|
)
|
||||||
).order_by(
|
).order_by(
|
||||||
"-max_values" if order_by_param.startswith("-") else "max_values"
|
"-max_values"
|
||||||
|
if order_by_param.startswith("-")
|
||||||
|
else "max_values"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
issue_queryset = issue_queryset.order_by(order_by_param)
|
issue_queryset = issue_queryset.order_by(order_by_param)
|
||||||
@ -209,7 +225,9 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
# Track the issue
|
# Track the issue
|
||||||
issue_activity.delay(
|
issue_activity.delay(
|
||||||
type="issue.activity.created",
|
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),
|
actor_id=str(request.user.id),
|
||||||
issue_id=str(serializer.data.get("id", None)),
|
issue_id=str(serializer.data.get("id", None)),
|
||||||
project_id=str(project_id),
|
project_id=str(project_id),
|
||||||
@ -220,7 +238,9 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
def patch(self, request, slug, project_id, pk=None):
|
def patch(self, request, slug, project_id, pk=None):
|
||||||
issue = Issue.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
|
issue = Issue.objects.get(
|
||||||
|
workspace__slug=slug, project_id=project_id, pk=pk
|
||||||
|
)
|
||||||
project = Project.objects.get(pk=project_id)
|
project = Project.objects.get(pk=project_id)
|
||||||
current_instance = json.dumps(
|
current_instance = json.dumps(
|
||||||
IssueSerializer(issue).data, cls=DjangoJSONEncoder
|
IssueSerializer(issue).data, cls=DjangoJSONEncoder
|
||||||
@ -250,7 +270,9 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
def delete(self, request, slug, project_id, pk=None):
|
def delete(self, request, slug, project_id, pk=None):
|
||||||
issue = Issue.objects.get(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(
|
current_instance = json.dumps(
|
||||||
IssueSerializer(issue).data, cls=DjangoJSONEncoder
|
IssueSerializer(issue).data, cls=DjangoJSONEncoder
|
||||||
)
|
)
|
||||||
@ -297,11 +319,17 @@ class LabelAPIEndpoint(BaseAPIView):
|
|||||||
serializer = LabelSerializer(data=request.data)
|
serializer = LabelSerializer(data=request.data)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
serializer.save(project_id=project_id)
|
serializer.save(project_id=project_id)
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
serializer.data, status=status.HTTP_201_CREATED
|
||||||
|
)
|
||||||
|
return Response(
|
||||||
|
serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Label with the same name already exists in the project"},
|
{
|
||||||
|
"error": "Label with the same name already exists in the project"
|
||||||
|
},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -318,7 +346,11 @@ class LabelAPIEndpoint(BaseAPIView):
|
|||||||
).data,
|
).data,
|
||||||
)
|
)
|
||||||
label = self.get_queryset().get(pk=pk)
|
label = self.get_queryset().get(pk=pk)
|
||||||
serializer = LabelSerializer(label, fields=self.fields, expand=self.expand,)
|
serializer = LabelSerializer(
|
||||||
|
label,
|
||||||
|
fields=self.fields,
|
||||||
|
expand=self.expand,
|
||||||
|
)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def patch(self, request, slug, project_id, pk=None):
|
def patch(self, request, slug, project_id, pk=None):
|
||||||
@ -329,7 +361,6 @@ class LabelAPIEndpoint(BaseAPIView):
|
|||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
def delete(self, request, slug, project_id, pk=None):
|
def delete(self, request, slug, project_id, pk=None):
|
||||||
label = self.get_queryset().get(pk=pk)
|
label = self.get_queryset().get(pk=pk)
|
||||||
label.delete()
|
label.delete()
|
||||||
@ -395,7 +426,9 @@ class IssueLinkAPIEndpoint(BaseAPIView):
|
|||||||
)
|
)
|
||||||
issue_activity.delay(
|
issue_activity.delay(
|
||||||
type="link.activity.created",
|
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),
|
actor_id=str(self.request.user.id),
|
||||||
issue_id=str(self.kwargs.get("issue_id")),
|
issue_id=str(self.kwargs.get("issue_id")),
|
||||||
project_id=str(self.kwargs.get("project_id")),
|
project_id=str(self.kwargs.get("project_id")),
|
||||||
@ -407,14 +440,19 @@ class IssueLinkAPIEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
def patch(self, request, slug, project_id, issue_id, pk):
|
def patch(self, request, slug, project_id, issue_id, pk):
|
||||||
issue_link = IssueLink.objects.get(
|
issue_link = IssueLink.objects.get(
|
||||||
workspace__slug=slug, 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)
|
requested_data = json.dumps(request.data, cls=DjangoJSONEncoder)
|
||||||
current_instance = json.dumps(
|
current_instance = json.dumps(
|
||||||
IssueLinkSerializer(issue_link).data,
|
IssueLinkSerializer(issue_link).data,
|
||||||
cls=DjangoJSONEncoder,
|
cls=DjangoJSONEncoder,
|
||||||
)
|
)
|
||||||
serializer = IssueLinkSerializer(issue_link, data=request.data, partial=True)
|
serializer = IssueLinkSerializer(
|
||||||
|
issue_link, data=request.data, partial=True
|
||||||
|
)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
serializer.save()
|
serializer.save()
|
||||||
issue_activity.delay(
|
issue_activity.delay(
|
||||||
@ -431,7 +469,10 @@ class IssueLinkAPIEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
def delete(self, request, slug, project_id, issue_id, pk):
|
def delete(self, request, slug, project_id, issue_id, pk):
|
||||||
issue_link = IssueLink.objects.get(
|
issue_link = IssueLink.objects.get(
|
||||||
workspace__slug=slug, 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(
|
current_instance = json.dumps(
|
||||||
IssueLinkSerializer(issue_link).data,
|
IssueLinkSerializer(issue_link).data,
|
||||||
@ -466,7 +507,9 @@ class IssueCommentAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return (
|
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(project_id=self.kwargs.get("project_id"))
|
||||||
.filter(issue_id=self.kwargs.get("issue_id"))
|
.filter(issue_id=self.kwargs.get("issue_id"))
|
||||||
.filter(project__project_projectmember__member=self.request.user)
|
.filter(project__project_projectmember__member=self.request.user)
|
||||||
@ -518,7 +561,9 @@ class IssueCommentAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
)
|
)
|
||||||
issue_activity.delay(
|
issue_activity.delay(
|
||||||
type="comment.activity.created",
|
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),
|
actor_id=str(self.request.user.id),
|
||||||
issue_id=str(self.kwargs.get("issue_id")),
|
issue_id=str(self.kwargs.get("issue_id")),
|
||||||
project_id=str(self.kwargs.get("project_id")),
|
project_id=str(self.kwargs.get("project_id")),
|
||||||
@ -530,7 +575,10 @@ class IssueCommentAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
|
|
||||||
def patch(self, request, slug, project_id, issue_id, pk):
|
def patch(self, request, slug, project_id, issue_id, pk):
|
||||||
issue_comment = IssueComment.objects.get(
|
issue_comment = IssueComment.objects.get(
|
||||||
workspace__slug=slug, 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)
|
requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder)
|
||||||
current_instance = json.dumps(
|
current_instance = json.dumps(
|
||||||
@ -556,7 +604,10 @@ class IssueCommentAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
|
|
||||||
def delete(self, request, slug, project_id, issue_id, pk):
|
def delete(self, request, slug, project_id, issue_id, pk):
|
||||||
issue_comment = IssueComment.objects.get(
|
issue_comment = IssueComment.objects.get(
|
||||||
workspace__slug=slug, 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(
|
current_instance = json.dumps(
|
||||||
IssueCommentSerializer(issue_comment).data,
|
IssueCommentSerializer(issue_comment).data,
|
||||||
|
@ -55,7 +55,9 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
.prefetch_related(
|
.prefetch_related(
|
||||||
Prefetch(
|
Prefetch(
|
||||||
"link_module",
|
"link_module",
|
||||||
queryset=ModuleLink.objects.select_related("module", "created_by"),
|
queryset=ModuleLink.objects.select_related(
|
||||||
|
"module", "created_by"
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
@ -122,7 +124,13 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
|
|
||||||
def post(self, request, slug, project_id):
|
def post(self, request, slug, project_id):
|
||||||
project = Project.objects.get(pk=project_id, workspace__slug=slug)
|
project = Project.objects.get(pk=project_id, workspace__slug=slug)
|
||||||
serializer = ModuleSerializer(data=request.data, context={"project_id": project_id, "workspace_id": project.workspace_id})
|
serializer = ModuleSerializer(
|
||||||
|
data=request.data,
|
||||||
|
context={
|
||||||
|
"project_id": project_id,
|
||||||
|
"workspace_id": project.workspace_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
serializer.save()
|
serializer.save()
|
||||||
module = Module.objects.get(pk=serializer.data["id"])
|
module = Module.objects.get(pk=serializer.data["id"])
|
||||||
@ -131,8 +139,15 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
def patch(self, request, slug, project_id, pk):
|
def patch(self, request, slug, project_id, pk):
|
||||||
module = Module.objects.get(pk=pk, project_id=project_id, workspace__slug=slug)
|
module = Module.objects.get(
|
||||||
serializer = ModuleSerializer(module, data=request.data, context={"project_id": project_id}, partial=True)
|
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 serializer.is_valid():
|
||||||
serializer.save()
|
serializer.save()
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
@ -162,9 +177,13 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def delete(self, request, slug, project_id, pk):
|
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(
|
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(
|
issue_activity.delay(
|
||||||
type="module.activity.deleted",
|
type="module.activity.deleted",
|
||||||
@ -204,7 +223,9 @@ class ModuleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return (
|
return (
|
||||||
ModuleIssue.objects.annotate(
|
ModuleIssue.objects.annotate(
|
||||||
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("issue"))
|
sub_issues_count=Issue.issue_objects.filter(
|
||||||
|
parent=OuterRef("issue")
|
||||||
|
)
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
@ -228,7 +249,9 @@ class ModuleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
issues = (
|
issues = (
|
||||||
Issue.issue_objects.filter(issue_module__module_id=module_id)
|
Issue.issue_objects.filter(issue_module__module_id=module_id)
|
||||||
.annotate(
|
.annotate(
|
||||||
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
|
sub_issues_count=Issue.issue_objects.filter(
|
||||||
|
parent=OuterRef("id")
|
||||||
|
)
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
@ -250,7 +273,9 @@ class ModuleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
.values("count")
|
.values("count")
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id"))
|
attachment_count=IssueAttachment.objects.filter(
|
||||||
|
issue=OuterRef("id")
|
||||||
|
)
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
@ -271,7 +296,8 @@ class ModuleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
issues = request.data.get("issues", [])
|
issues = request.data.get("issues", [])
|
||||||
if not len(issues):
|
if not len(issues):
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Issues are required"}, status=status.HTTP_400_BAD_REQUEST
|
{"error": "Issues are required"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
module = Module.objects.get(
|
module = Module.objects.get(
|
||||||
workspace__slug=slug, project_id=project_id, pk=module_id
|
workspace__slug=slug, project_id=project_id, pk=module_id
|
||||||
@ -354,7 +380,10 @@ class ModuleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
|
|
||||||
def delete(self, request, slug, project_id, module_id, issue_id):
|
def delete(self, request, slug, project_id, module_id, issue_id):
|
||||||
module_issue = ModuleIssue.objects.get(
|
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,
|
||||||
|
issue_id=issue_id,
|
||||||
)
|
)
|
||||||
module_issue.delete()
|
module_issue.delete()
|
||||||
issue_activity.delay(
|
issue_activity.delay(
|
||||||
|
@ -39,9 +39,15 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return (
|
return (
|
||||||
Project.objects.filter(workspace__slug=self.kwargs.get("slug"))
|
Project.objects.filter(workspace__slug=self.kwargs.get("slug"))
|
||||||
.filter(Q(project_projectmember__member=self.request.user) | Q(network=2))
|
.filter(
|
||||||
|
Q(project_projectmember__member=self.request.user)
|
||||||
|
| Q(network=2)
|
||||||
|
)
|
||||||
.select_related(
|
.select_related(
|
||||||
"workspace", "workspace__owner", "default_assignee", "project_lead"
|
"workspace",
|
||||||
|
"workspace__owner",
|
||||||
|
"default_assignee",
|
||||||
|
"project_lead",
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
is_member=Exists(
|
is_member=Exists(
|
||||||
@ -120,11 +126,18 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
request=request,
|
request=request,
|
||||||
queryset=(projects),
|
queryset=(projects),
|
||||||
on_results=lambda projects: ProjectSerializer(
|
on_results=lambda projects: ProjectSerializer(
|
||||||
projects, many=True, fields=self.fields, expand=self.expand,
|
projects,
|
||||||
|
many=True,
|
||||||
|
fields=self.fields,
|
||||||
|
expand=self.expand,
|
||||||
).data,
|
).data,
|
||||||
)
|
)
|
||||||
project = self.get_queryset().get(workspace__slug=slug, pk=project_id)
|
project = self.get_queryset().get(workspace__slug=slug, pk=project_id)
|
||||||
serializer = ProjectSerializer(project, fields=self.fields, expand=self.expand,)
|
serializer = ProjectSerializer(
|
||||||
|
project,
|
||||||
|
fields=self.fields,
|
||||||
|
expand=self.expand,
|
||||||
|
)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def post(self, request, slug):
|
def post(self, request, slug):
|
||||||
@ -138,7 +151,9 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
|
|
||||||
# Add the user as Administrator to the project
|
# Add the user as Administrator to the project
|
||||||
project_member = ProjectMember.objects.create(
|
project_member = ProjectMember.objects.create(
|
||||||
project_id=serializer.data["id"], member=request.user, role=20
|
project_id=serializer.data["id"],
|
||||||
|
member=request.user,
|
||||||
|
role=20,
|
||||||
)
|
)
|
||||||
# Also create the issue property for the user
|
# Also create the issue property for the user
|
||||||
_ = IssueProperty.objects.create(
|
_ = IssueProperty.objects.create(
|
||||||
@ -211,9 +226,15 @@ 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)
|
serializer = ProjectSerializer(project)
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(
|
||||||
|
serializer.data, status=status.HTTP_201_CREATED
|
||||||
|
)
|
||||||
return Response(
|
return Response(
|
||||||
serializer.errors,
|
serializer.errors,
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
@ -226,7 +247,8 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
)
|
)
|
||||||
except Workspace.DoesNotExist as e:
|
except Workspace.DoesNotExist as e:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Workspace does not exist"}, status=status.HTTP_404_NOT_FOUND
|
{"error": "Workspace does not exist"},
|
||||||
|
status=status.HTTP_404_NOT_FOUND,
|
||||||
)
|
)
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
return Response(
|
return Response(
|
||||||
@ -250,7 +272,9 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
serializer.save()
|
serializer.save()
|
||||||
if serializer.data["inbox_view"]:
|
if serializer.data["inbox_view"]:
|
||||||
Inbox.objects.get_or_create(
|
Inbox.objects.get_or_create(
|
||||||
name=f"{project.name} Inbox", project=project, is_default=True
|
name=f"{project.name} Inbox",
|
||||||
|
project=project,
|
||||||
|
is_default=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create the triage state in Backlog group
|
# Create the triage state in Backlog group
|
||||||
@ -262,10 +286,16 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
color="#ff7700",
|
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)
|
serializer = ProjectSerializer(project)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(
|
||||||
|
serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
except IntegrityError as e:
|
except IntegrityError as e:
|
||||||
if "already exists" in str(e):
|
if "already exists" in str(e):
|
||||||
return Response(
|
return Response(
|
||||||
@ -274,7 +304,8 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
)
|
)
|
||||||
except (Project.DoesNotExist, Workspace.DoesNotExist):
|
except (Project.DoesNotExist, Workspace.DoesNotExist):
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Project does not exist"}, status=status.HTTP_404_NOT_FOUND
|
{"error": "Project does not exist"},
|
||||||
|
status=status.HTTP_404_NOT_FOUND,
|
||||||
)
|
)
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
return Response(
|
return Response(
|
||||||
|
@ -34,7 +34,9 @@ class StateAPIEndpoint(BaseAPIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def post(self, request, slug, project_id):
|
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 serializer.is_valid():
|
||||||
serializer.save(project_id=project_id)
|
serializer.save(project_id=project_id)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
@ -64,14 +66,19 @@ class StateAPIEndpoint(BaseAPIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if state.default:
|
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=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
# Check for any issues in the state
|
# Check for any issues in the state
|
||||||
issue_exist = Issue.issue_objects.filter(state=state_id).exists()
|
issue_exist = Issue.issue_objects.filter(state=state_id).exists()
|
||||||
|
|
||||||
if issue_exist:
|
if issue_exist:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "The state is not empty, only empty states can be deleted"},
|
{
|
||||||
|
"error": "The state is not empty, only empty states can be deleted"
|
||||||
|
},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -79,7 +86,9 @@ class StateAPIEndpoint(BaseAPIView):
|
|||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
def patch(self, request, slug, project_id, state_id=None):
|
def patch(self, request, slug, project_id, state_id=None):
|
||||||
state = State.objects.get(workspace__slug=slug, project_id=project_id, pk=state_id)
|
state = State.objects.get(
|
||||||
|
workspace__slug=slug, project_id=project_id, pk=state_id
|
||||||
|
)
|
||||||
serializer = StateSerializer(state, data=request.data, partial=True)
|
serializer = StateSerializer(state, data=request.data, partial=True)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
serializer.save()
|
serializer.save()
|
||||||
|
@ -25,7 +25,10 @@ class APIKeyAuthentication(authentication.BaseAuthentication):
|
|||||||
def validate_api_token(self, token):
|
def validate_api_token(self, token):
|
||||||
try:
|
try:
|
||||||
api_token = APIToken.objects.get(
|
api_token = APIToken.objects.get(
|
||||||
Q(Q(expired_at__gt=timezone.now()) | Q(expired_at__isnull=True)),
|
Q(
|
||||||
|
Q(expired_at__gt=timezone.now())
|
||||||
|
| Q(expired_at__isnull=True)
|
||||||
|
),
|
||||||
token=token,
|
token=token,
|
||||||
is_active=True,
|
is_active=True,
|
||||||
)
|
)
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
from .workspace import (
|
from .workspace import (
|
||||||
WorkSpaceBasePermission,
|
WorkSpaceBasePermission,
|
||||||
WorkspaceOwnerPermission,
|
WorkspaceOwnerPermission,
|
||||||
@ -13,5 +12,3 @@ from .project import (
|
|||||||
ProjectMemberPermission,
|
ProjectMemberPermission,
|
||||||
ProjectLitePermission,
|
ProjectLitePermission,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@ from .workspace import (
|
|||||||
WorkspaceThemeSerializer,
|
WorkspaceThemeSerializer,
|
||||||
WorkspaceMemberAdminSerializer,
|
WorkspaceMemberAdminSerializer,
|
||||||
WorkspaceMemberMeSerializer,
|
WorkspaceMemberMeSerializer,
|
||||||
|
WorkspaceUserPropertiesSerializer,
|
||||||
)
|
)
|
||||||
from .project import (
|
from .project import (
|
||||||
ProjectSerializer,
|
ProjectSerializer,
|
||||||
@ -31,14 +32,20 @@ from .project import (
|
|||||||
ProjectDeployBoardSerializer,
|
ProjectDeployBoardSerializer,
|
||||||
ProjectMemberAdminSerializer,
|
ProjectMemberAdminSerializer,
|
||||||
ProjectPublicMemberSerializer,
|
ProjectPublicMemberSerializer,
|
||||||
|
ProjectMemberRoleSerializer,
|
||||||
)
|
)
|
||||||
from .state import StateSerializer, StateLiteSerializer
|
from .state import StateSerializer, StateLiteSerializer
|
||||||
from .view import GlobalViewSerializer, IssueViewSerializer, IssueViewFavoriteSerializer
|
from .view import (
|
||||||
|
GlobalViewSerializer,
|
||||||
|
IssueViewSerializer,
|
||||||
|
IssueViewFavoriteSerializer,
|
||||||
|
)
|
||||||
from .cycle import (
|
from .cycle import (
|
||||||
CycleSerializer,
|
CycleSerializer,
|
||||||
CycleIssueSerializer,
|
CycleIssueSerializer,
|
||||||
CycleFavoriteSerializer,
|
CycleFavoriteSerializer,
|
||||||
CycleWriteSerializer,
|
CycleWriteSerializer,
|
||||||
|
CycleUserPropertiesSerializer,
|
||||||
)
|
)
|
||||||
from .asset import FileAssetSerializer
|
from .asset import FileAssetSerializer
|
||||||
from .issue import (
|
from .issue import (
|
||||||
@ -69,6 +76,7 @@ from .module import (
|
|||||||
ModuleIssueSerializer,
|
ModuleIssueSerializer,
|
||||||
ModuleLinkSerializer,
|
ModuleLinkSerializer,
|
||||||
ModuleFavoriteSerializer,
|
ModuleFavoriteSerializer,
|
||||||
|
ModuleUserPropertiesSerializer,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .api import APITokenSerializer, APITokenReadSerializer
|
from .api import APITokenSerializer, APITokenReadSerializer
|
||||||
@ -85,20 +93,33 @@ from .integration import (
|
|||||||
|
|
||||||
from .importer import ImporterSerializer
|
from .importer import ImporterSerializer
|
||||||
|
|
||||||
from .page import PageSerializer, PageLogSerializer, SubPageSerializer, PageFavoriteSerializer
|
from .page import (
|
||||||
|
PageSerializer,
|
||||||
|
PageLogSerializer,
|
||||||
|
SubPageSerializer,
|
||||||
|
PageFavoriteSerializer,
|
||||||
|
)
|
||||||
|
|
||||||
from .estimate import (
|
from .estimate import (
|
||||||
EstimateSerializer,
|
EstimateSerializer,
|
||||||
EstimatePointSerializer,
|
EstimatePointSerializer,
|
||||||
EstimateReadSerializer,
|
EstimateReadSerializer,
|
||||||
|
WorkspaceEstimateSerializer,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .inbox import InboxSerializer, InboxIssueSerializer, IssueStateInboxSerializer
|
from .inbox import (
|
||||||
|
InboxSerializer,
|
||||||
|
InboxIssueSerializer,
|
||||||
|
IssueStateInboxSerializer,
|
||||||
|
InboxIssueLiteSerializer,
|
||||||
|
)
|
||||||
|
|
||||||
from .analytic import AnalyticViewSerializer
|
from .analytic import AnalyticViewSerializer
|
||||||
|
|
||||||
from .notification import NotificationSerializer
|
from .notification import NotificationSerializer, UserNotificationPreferenceSerializer
|
||||||
|
|
||||||
from .exporter import ExporterHistorySerializer
|
from .exporter import ExporterHistorySerializer
|
||||||
|
|
||||||
from .webhook import WebhookSerializer, WebhookLogSerializer
|
from .webhook import WebhookSerializer, WebhookLogSerializer
|
||||||
|
|
||||||
|
from .dashboard import DashboardSerializer, WidgetSerializer
|
||||||
|
@ -3,7 +3,6 @@ from plane.db.models import APIToken, APIActivityLog
|
|||||||
|
|
||||||
|
|
||||||
class APITokenSerializer(BaseSerializer):
|
class APITokenSerializer(BaseSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = APIToken
|
model = APIToken
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
@ -18,14 +17,12 @@ class APITokenSerializer(BaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class APITokenReadSerializer(BaseSerializer):
|
class APITokenReadSerializer(BaseSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = APIToken
|
model = APIToken
|
||||||
exclude = ('token',)
|
exclude = ("token",)
|
||||||
|
|
||||||
|
|
||||||
class APIActivityLogSerializer(BaseSerializer):
|
class APIActivityLogSerializer(BaseSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = APIActivityLog
|
model = APIActivityLog
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
@ -4,16 +4,17 @@ from rest_framework import serializers
|
|||||||
class BaseSerializer(serializers.ModelSerializer):
|
class BaseSerializer(serializers.ModelSerializer):
|
||||||
id = serializers.PrimaryKeyRelatedField(read_only=True)
|
id = serializers.PrimaryKeyRelatedField(read_only=True)
|
||||||
|
|
||||||
class DynamicBaseSerializer(BaseSerializer):
|
|
||||||
|
|
||||||
|
class DynamicBaseSerializer(BaseSerializer):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
# If 'fields' is provided in the arguments, remove it and store it separately.
|
# If 'fields' is provided in the arguments, remove it and store it separately.
|
||||||
# This is done so as not to pass this custom argument up to the superclass.
|
# This is done so as not to pass this custom argument up to the superclass.
|
||||||
fields = kwargs.pop("fields", None)
|
fields = kwargs.pop("fields", [])
|
||||||
|
self.expand = kwargs.pop("expand", []) or []
|
||||||
|
fields = self.expand
|
||||||
|
|
||||||
# Call the initialization of the superclass.
|
# Call the initialization of the superclass.
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
# If 'fields' was provided, filter the fields of the serializer accordingly.
|
# If 'fields' was provided, filter the fields of the serializer accordingly.
|
||||||
if fields is not None:
|
if fields is not None:
|
||||||
self.fields = self._filter_fields(fields)
|
self.fields = self._filter_fields(fields)
|
||||||
@ -47,12 +48,101 @@ class DynamicBaseSerializer(BaseSerializer):
|
|||||||
elif isinstance(item, dict):
|
elif isinstance(item, dict):
|
||||||
allowed.append(list(item.keys())[0])
|
allowed.append(list(item.keys())[0])
|
||||||
|
|
||||||
# Convert the current serializer's fields and the allowed fields to sets.
|
for field in allowed:
|
||||||
existing = set(self.fields)
|
if field not in self.fields:
|
||||||
allowed = set(allowed)
|
from . import (
|
||||||
|
WorkspaceLiteSerializer,
|
||||||
|
ProjectLiteSerializer,
|
||||||
|
UserLiteSerializer,
|
||||||
|
StateLiteSerializer,
|
||||||
|
IssueSerializer,
|
||||||
|
LabelSerializer,
|
||||||
|
CycleIssueSerializer,
|
||||||
|
IssueFlatSerializer,
|
||||||
|
IssueRelationSerializer,
|
||||||
|
InboxIssueLiteSerializer
|
||||||
|
)
|
||||||
|
|
||||||
# Remove fields from the serializer that aren't in the 'allowed' list.
|
# Expansion mapper
|
||||||
for field_name in (existing - allowed):
|
expansion = {
|
||||||
self.fields.pop(field_name)
|
"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)
|
||||||
|
|
||||||
return self.fields
|
return self.fields
|
||||||
|
|
||||||
|
def to_representation(self, instance):
|
||||||
|
response = super().to_representation(instance)
|
||||||
|
|
||||||
|
# Ensure 'expand' is iterable before processing
|
||||||
|
if self.expand:
|
||||||
|
for expand in self.expand:
|
||||||
|
if expand in self.fields:
|
||||||
|
# Import all the expandable serializers
|
||||||
|
from . import (
|
||||||
|
WorkspaceLiteSerializer,
|
||||||
|
ProjectLiteSerializer,
|
||||||
|
UserLiteSerializer,
|
||||||
|
StateLiteSerializer,
|
||||||
|
IssueSerializer,
|
||||||
|
LabelSerializer,
|
||||||
|
CycleIssueSerializer,
|
||||||
|
IssueRelationSerializer,
|
||||||
|
InboxIssueLiteSerializer
|
||||||
|
)
|
||||||
|
|
||||||
|
# Expansion mapper
|
||||||
|
expansion = {
|
||||||
|
"user": UserLiteSerializer,
|
||||||
|
"workspace": WorkspaceLiteSerializer,
|
||||||
|
"project": ProjectLiteSerializer,
|
||||||
|
"default_assignee": UserLiteSerializer,
|
||||||
|
"project_lead": UserLiteSerializer,
|
||||||
|
"state": StateLiteSerializer,
|
||||||
|
"created_by": UserLiteSerializer,
|
||||||
|
"issue": IssueSerializer,
|
||||||
|
"actor": UserLiteSerializer,
|
||||||
|
"owned_by": UserLiteSerializer,
|
||||||
|
"members": UserLiteSerializer,
|
||||||
|
"assignees": UserLiteSerializer,
|
||||||
|
"labels": LabelSerializer,
|
||||||
|
"issue_cycle": CycleIssueSerializer,
|
||||||
|
"parent": IssueSerializer,
|
||||||
|
"issue_relation": IssueRelationSerializer,
|
||||||
|
"issue_inbox" : InboxIssueLiteSerializer,
|
||||||
|
}
|
||||||
|
# Check if field in expansion then expand the field
|
||||||
|
if expand in expansion:
|
||||||
|
if isinstance(response.get(expand), list):
|
||||||
|
exp_serializer = expansion[expand](
|
||||||
|
getattr(instance, expand), many=True
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
exp_serializer = expansion[expand](
|
||||||
|
getattr(instance, expand)
|
||||||
|
)
|
||||||
|
response[expand] = exp_serializer.data
|
||||||
|
else:
|
||||||
|
# You might need to handle this case differently
|
||||||
|
response[expand] = getattr(
|
||||||
|
instance, f"{expand}_id", None
|
||||||
|
)
|
||||||
|
|
||||||
|
return response
|
||||||
|
@ -7,7 +7,12 @@ from .user import UserLiteSerializer
|
|||||||
from .issue import IssueStateSerializer
|
from .issue import IssueStateSerializer
|
||||||
from .workspace import WorkspaceLiteSerializer
|
from .workspace import WorkspaceLiteSerializer
|
||||||
from .project import ProjectLiteSerializer
|
from .project import ProjectLiteSerializer
|
||||||
from plane.db.models import Cycle, CycleIssue, CycleFavorite
|
from plane.db.models import (
|
||||||
|
Cycle,
|
||||||
|
CycleIssue,
|
||||||
|
CycleFavorite,
|
||||||
|
CycleUserProperties,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class CycleWriteSerializer(BaseSerializer):
|
class CycleWriteSerializer(BaseSerializer):
|
||||||
@ -17,7 +22,9 @@ class CycleWriteSerializer(BaseSerializer):
|
|||||||
and data.get("end_date", None) is not None
|
and data.get("end_date", None) is not None
|
||||||
and data.get("start_date", None) > data.get("end_date", None)
|
and data.get("start_date", None) > data.get("end_date", None)
|
||||||
):
|
):
|
||||||
raise serializers.ValidationError("Start date cannot exceed end date")
|
raise serializers.ValidationError(
|
||||||
|
"Start date cannot exceed end date"
|
||||||
|
)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -26,7 +33,6 @@ class CycleWriteSerializer(BaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class CycleSerializer(BaseSerializer):
|
class CycleSerializer(BaseSerializer):
|
||||||
owned_by = UserLiteSerializer(read_only=True)
|
|
||||||
is_favorite = serializers.BooleanField(read_only=True)
|
is_favorite = serializers.BooleanField(read_only=True)
|
||||||
total_issues = serializers.IntegerField(read_only=True)
|
total_issues = serializers.IntegerField(read_only=True)
|
||||||
cancelled_issues = serializers.IntegerField(read_only=True)
|
cancelled_issues = serializers.IntegerField(read_only=True)
|
||||||
@ -38,7 +44,9 @@ class CycleSerializer(BaseSerializer):
|
|||||||
total_estimates = serializers.IntegerField(read_only=True)
|
total_estimates = serializers.IntegerField(read_only=True)
|
||||||
completed_estimates = serializers.IntegerField(read_only=True)
|
completed_estimates = serializers.IntegerField(read_only=True)
|
||||||
started_estimates = serializers.IntegerField(read_only=True)
|
started_estimates = serializers.IntegerField(read_only=True)
|
||||||
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
|
workspace_detail = WorkspaceLiteSerializer(
|
||||||
|
read_only=True, source="workspace"
|
||||||
|
)
|
||||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||||
status = serializers.CharField(read_only=True)
|
status = serializers.CharField(read_only=True)
|
||||||
|
|
||||||
@ -48,7 +56,9 @@ class CycleSerializer(BaseSerializer):
|
|||||||
and data.get("end_date", None) is not None
|
and data.get("end_date", None) is not None
|
||||||
and data.get("start_date", None) > data.get("end_date", None)
|
and data.get("start_date", None) > data.get("end_date", None)
|
||||||
):
|
):
|
||||||
raise serializers.ValidationError("Start date cannot exceed end date")
|
raise serializers.ValidationError(
|
||||||
|
"Start date cannot exceed end date"
|
||||||
|
)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def get_assignees(self, obj):
|
def get_assignees(self, obj):
|
||||||
@ -106,3 +116,14 @@ class CycleFavoriteSerializer(BaseSerializer):
|
|||||||
"project",
|
"project",
|
||||||
"user",
|
"user",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class CycleUserPropertiesSerializer(BaseSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = CycleUserProperties
|
||||||
|
fields = "__all__"
|
||||||
|
read_only_fields = [
|
||||||
|
"workspace",
|
||||||
|
"project",
|
||||||
|
"cycle" "user",
|
||||||
|
]
|
||||||
|
26
apiserver/plane/app/serializers/dashboard.py
Normal file
26
apiserver/plane/app/serializers/dashboard.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# 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,12 +2,18 @@
|
|||||||
from .base import BaseSerializer
|
from .base import BaseSerializer
|
||||||
|
|
||||||
from plane.db.models import Estimate, EstimatePoint
|
from plane.db.models import Estimate, EstimatePoint
|
||||||
from plane.app.serializers import WorkspaceLiteSerializer, ProjectLiteSerializer
|
from plane.app.serializers import (
|
||||||
|
WorkspaceLiteSerializer,
|
||||||
|
ProjectLiteSerializer,
|
||||||
|
)
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
|
||||||
class EstimateSerializer(BaseSerializer):
|
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")
|
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -20,13 +26,14 @@ class EstimateSerializer(BaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class EstimatePointSerializer(BaseSerializer):
|
class EstimatePointSerializer(BaseSerializer):
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
if not data:
|
if not data:
|
||||||
raise serializers.ValidationError("Estimate points are required")
|
raise serializers.ValidationError("Estimate points are required")
|
||||||
value = data.get("value")
|
value = data.get("value")
|
||||||
if value and len(value) > 20:
|
if value and len(value) > 20:
|
||||||
raise serializers.ValidationError("Value can't be more than 20 characters")
|
raise serializers.ValidationError(
|
||||||
|
"Value can't be more than 20 characters"
|
||||||
|
)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -41,7 +48,9 @@ class EstimatePointSerializer(BaseSerializer):
|
|||||||
|
|
||||||
class EstimateReadSerializer(BaseSerializer):
|
class EstimateReadSerializer(BaseSerializer):
|
||||||
points = EstimatePointSerializer(read_only=True, many=True)
|
points = EstimatePointSerializer(read_only=True, many=True)
|
||||||
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
|
workspace_detail = WorkspaceLiteSerializer(
|
||||||
|
read_only=True, source="workspace"
|
||||||
|
)
|
||||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -52,3 +61,18 @@ class EstimateReadSerializer(BaseSerializer):
|
|||||||
"name",
|
"name",
|
||||||
"description",
|
"description",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class WorkspaceEstimateSerializer(BaseSerializer):
|
||||||
|
points = EstimatePointSerializer(read_only=True, many=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Estimate
|
||||||
|
fields = "__all__"
|
||||||
|
read_only_fields = [
|
||||||
|
"points",
|
||||||
|
"name",
|
||||||
|
"description",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -5,7 +5,9 @@ from .user import UserLiteSerializer
|
|||||||
|
|
||||||
|
|
||||||
class ExporterHistorySerializer(BaseSerializer):
|
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:
|
class Meta:
|
||||||
model = ExporterHistory
|
model = ExporterHistory
|
||||||
|
@ -7,9 +7,13 @@ from plane.db.models import Importer
|
|||||||
|
|
||||||
|
|
||||||
class ImporterSerializer(BaseSerializer):
|
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)
|
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:
|
class Meta:
|
||||||
model = Importer
|
model = Importer
|
||||||
|
@ -46,10 +46,13 @@ class InboxIssueLiteSerializer(BaseSerializer):
|
|||||||
class IssueStateInboxSerializer(BaseSerializer):
|
class IssueStateInboxSerializer(BaseSerializer):
|
||||||
state_detail = StateLiteSerializer(read_only=True, source="state")
|
state_detail = StateLiteSerializer(read_only=True, source="state")
|
||||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||||
label_details = LabelLiteSerializer(read_only=True, source="labels", many=True)
|
label_details = LabelLiteSerializer(
|
||||||
assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
|
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)
|
sub_issues_count = serializers.IntegerField(read_only=True)
|
||||||
bridge_id = serializers.UUIDField(read_only=True)
|
|
||||||
issue_inbox = InboxIssueLiteSerializer(read_only=True, many=True)
|
issue_inbox = InboxIssueLiteSerializer(read_only=True, many=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -13,7 +13,9 @@ class IntegrationSerializer(BaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class WorkspaceIntegrationSerializer(BaseSerializer):
|
class WorkspaceIntegrationSerializer(BaseSerializer):
|
||||||
integration_detail = IntegrationSerializer(read_only=True, source="integration")
|
integration_detail = IntegrationSerializer(
|
||||||
|
read_only=True, source="integration"
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = WorkspaceIntegration
|
model = WorkspaceIntegration
|
||||||
|
@ -30,6 +30,8 @@ from plane.db.models import (
|
|||||||
CommentReaction,
|
CommentReaction,
|
||||||
IssueVote,
|
IssueVote,
|
||||||
IssueRelation,
|
IssueRelation,
|
||||||
|
State,
|
||||||
|
Project,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -69,19 +71,26 @@ class IssueProjectLiteSerializer(BaseSerializer):
|
|||||||
##TODO: Find a better way to write this serializer
|
##TODO: Find a better way to write this serializer
|
||||||
## Find a better approach to save manytomany?
|
## Find a better approach to save manytomany?
|
||||||
class IssueCreateSerializer(BaseSerializer):
|
class IssueCreateSerializer(BaseSerializer):
|
||||||
state_detail = StateSerializer(read_only=True, source="state")
|
# ids
|
||||||
created_by_detail = UserLiteSerializer(read_only=True, source="created_by")
|
state_id = serializers.PrimaryKeyRelatedField(
|
||||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
source="state",
|
||||||
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
|
queryset=State.objects.all(),
|
||||||
|
required=False,
|
||||||
assignees = serializers.ListField(
|
allow_null=True,
|
||||||
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
|
)
|
||||||
|
parent_id = serializers.PrimaryKeyRelatedField(
|
||||||
|
source="parent",
|
||||||
|
queryset=Issue.objects.all(),
|
||||||
|
required=False,
|
||||||
|
allow_null=True,
|
||||||
|
)
|
||||||
|
label_ids = serializers.ListField(
|
||||||
|
child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()),
|
||||||
write_only=True,
|
write_only=True,
|
||||||
required=False,
|
required=False,
|
||||||
)
|
)
|
||||||
|
assignee_ids = serializers.ListField(
|
||||||
labels = serializers.ListField(
|
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
|
||||||
child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()),
|
|
||||||
write_only=True,
|
write_only=True,
|
||||||
required=False,
|
required=False,
|
||||||
)
|
)
|
||||||
@ -100,8 +109,10 @@ class IssueCreateSerializer(BaseSerializer):
|
|||||||
|
|
||||||
def to_representation(self, instance):
|
def to_representation(self, instance):
|
||||||
data = super().to_representation(instance)
|
data = super().to_representation(instance)
|
||||||
data['assignees'] = [str(assignee.id) for assignee in instance.assignees.all()]
|
assignee_ids = self.initial_data.get("assignee_ids")
|
||||||
data['labels'] = [str(label.id) for label in instance.labels.all()]
|
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 []
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
@ -110,12 +121,14 @@ class IssueCreateSerializer(BaseSerializer):
|
|||||||
and data.get("target_date", None) is not None
|
and data.get("target_date", None) is not None
|
||||||
and data.get("start_date", None) > data.get("target_date", None)
|
and data.get("start_date", None) > data.get("target_date", None)
|
||||||
):
|
):
|
||||||
raise serializers.ValidationError("Start date cannot exceed target date")
|
raise serializers.ValidationError(
|
||||||
|
"Start date cannot exceed target date"
|
||||||
|
)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
assignees = validated_data.pop("assignees", None)
|
assignees = validated_data.pop("assignee_ids", None)
|
||||||
labels = validated_data.pop("labels", None)
|
labels = validated_data.pop("label_ids", None)
|
||||||
|
|
||||||
project_id = self.context["project_id"]
|
project_id = self.context["project_id"]
|
||||||
workspace_id = self.context["workspace_id"]
|
workspace_id = self.context["workspace_id"]
|
||||||
@ -173,8 +186,8 @@ class IssueCreateSerializer(BaseSerializer):
|
|||||||
return issue
|
return issue
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
assignees = validated_data.pop("assignees", None)
|
assignees = validated_data.pop("assignee_ids", None)
|
||||||
labels = validated_data.pop("labels", None)
|
labels = validated_data.pop("label_ids", None)
|
||||||
|
|
||||||
# Related models
|
# Related models
|
||||||
project_id = instance.project_id
|
project_id = instance.project_id
|
||||||
@ -225,14 +238,15 @@ class IssueActivitySerializer(BaseSerializer):
|
|||||||
actor_detail = UserLiteSerializer(read_only=True, source="actor")
|
actor_detail = UserLiteSerializer(read_only=True, source="actor")
|
||||||
issue_detail = IssueFlatSerializer(read_only=True, source="issue")
|
issue_detail = IssueFlatSerializer(read_only=True, source="issue")
|
||||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||||
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
|
workspace_detail = WorkspaceLiteSerializer(
|
||||||
|
read_only=True, source="workspace"
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IssueActivity
|
model = IssueActivity
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class IssuePropertySerializer(BaseSerializer):
|
class IssuePropertySerializer(BaseSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IssueProperty
|
model = IssueProperty
|
||||||
@ -245,12 +259,17 @@ class IssuePropertySerializer(BaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class LabelSerializer(BaseSerializer):
|
class LabelSerializer(BaseSerializer):
|
||||||
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
|
|
||||||
project_detail = ProjectLiteSerializer(source="project", read_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Label
|
model = Label
|
||||||
fields = "__all__"
|
fields = [
|
||||||
|
"parent",
|
||||||
|
"name",
|
||||||
|
"color",
|
||||||
|
"id",
|
||||||
|
"project_id",
|
||||||
|
"workspace_id",
|
||||||
|
"sort_order",
|
||||||
|
]
|
||||||
read_only_fields = [
|
read_only_fields = [
|
||||||
"workspace",
|
"workspace",
|
||||||
"project",
|
"project",
|
||||||
@ -268,7 +287,6 @@ class LabelLiteSerializer(BaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class IssueLabelSerializer(BaseSerializer):
|
class IssueLabelSerializer(BaseSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IssueLabel
|
model = IssueLabel
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
@ -279,33 +297,50 @@ class IssueLabelSerializer(BaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class IssueRelationSerializer(BaseSerializer):
|
class IssueRelationSerializer(BaseSerializer):
|
||||||
issue_detail = IssueProjectLiteSerializer(read_only=True, source="related_issue")
|
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)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IssueRelation
|
model = IssueRelation
|
||||||
fields = [
|
fields = [
|
||||||
"issue_detail",
|
"id",
|
||||||
|
"project_id",
|
||||||
|
"sequence_id",
|
||||||
"relation_type",
|
"relation_type",
|
||||||
"related_issue",
|
"name",
|
||||||
"issue",
|
|
||||||
"id"
|
|
||||||
]
|
]
|
||||||
read_only_fields = [
|
read_only_fields = [
|
||||||
"workspace",
|
"workspace",
|
||||||
"project",
|
"project",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class RelatedIssueSerializer(BaseSerializer):
|
class RelatedIssueSerializer(BaseSerializer):
|
||||||
issue_detail = IssueProjectLiteSerializer(read_only=True, source="issue")
|
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)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IssueRelation
|
model = IssueRelation
|
||||||
fields = [
|
fields = [
|
||||||
"issue_detail",
|
"id",
|
||||||
|
"project_id",
|
||||||
|
"sequence_id",
|
||||||
"relation_type",
|
"relation_type",
|
||||||
"related_issue",
|
"name",
|
||||||
"issue",
|
|
||||||
"id"
|
|
||||||
]
|
]
|
||||||
read_only_fields = [
|
read_only_fields = [
|
||||||
"workspace",
|
"workspace",
|
||||||
@ -400,7 +435,8 @@ class IssueLinkSerializer(BaseSerializer):
|
|||||||
# Validation if url already exists
|
# Validation if url already exists
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
if IssueLink.objects.filter(
|
if IssueLink.objects.filter(
|
||||||
url=validated_data.get("url"), issue_id=validated_data.get("issue_id")
|
url=validated_data.get("url"),
|
||||||
|
issue_id=validated_data.get("issue_id"),
|
||||||
).exists():
|
).exists():
|
||||||
raise serializers.ValidationError(
|
raise serializers.ValidationError(
|
||||||
{"error": "URL already exists for this Issue"}
|
{"error": "URL already exists for this Issue"}
|
||||||
@ -424,7 +460,6 @@ class IssueAttachmentSerializer(BaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class IssueReactionSerializer(BaseSerializer):
|
class IssueReactionSerializer(BaseSerializer):
|
||||||
|
|
||||||
actor_detail = UserLiteSerializer(read_only=True, source="actor")
|
actor_detail = UserLiteSerializer(read_only=True, source="actor")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -438,19 +473,6 @@ class IssueReactionSerializer(BaseSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class CommentReactionLiteSerializer(BaseSerializer):
|
|
||||||
actor_detail = UserLiteSerializer(read_only=True, source="actor")
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = CommentReaction
|
|
||||||
fields = [
|
|
||||||
"id",
|
|
||||||
"reaction",
|
|
||||||
"comment",
|
|
||||||
"actor_detail",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class CommentReactionSerializer(BaseSerializer):
|
class CommentReactionSerializer(BaseSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CommentReaction
|
model = CommentReaction
|
||||||
@ -459,12 +481,18 @@ class CommentReactionSerializer(BaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class IssueVoteSerializer(BaseSerializer):
|
class IssueVoteSerializer(BaseSerializer):
|
||||||
|
|
||||||
actor_detail = UserLiteSerializer(read_only=True, source="actor")
|
actor_detail = UserLiteSerializer(read_only=True, source="actor")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IssueVote
|
model = IssueVote
|
||||||
fields = ["issue", "vote", "workspace", "project", "actor", "actor_detail"]
|
fields = [
|
||||||
|
"issue",
|
||||||
|
"vote",
|
||||||
|
"workspace",
|
||||||
|
"project",
|
||||||
|
"actor",
|
||||||
|
"actor_detail",
|
||||||
|
]
|
||||||
read_only_fields = fields
|
read_only_fields = fields
|
||||||
|
|
||||||
|
|
||||||
@ -472,8 +500,12 @@ class IssueCommentSerializer(BaseSerializer):
|
|||||||
actor_detail = UserLiteSerializer(read_only=True, source="actor")
|
actor_detail = UserLiteSerializer(read_only=True, source="actor")
|
||||||
issue_detail = IssueFlatSerializer(read_only=True, source="issue")
|
issue_detail = IssueFlatSerializer(read_only=True, source="issue")
|
||||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||||
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
|
workspace_detail = WorkspaceLiteSerializer(
|
||||||
comment_reactions = CommentReactionLiteSerializer(read_only=True, many=True)
|
read_only=True, source="workspace"
|
||||||
|
)
|
||||||
|
comment_reactions = CommentReactionSerializer(
|
||||||
|
read_only=True, many=True
|
||||||
|
)
|
||||||
is_member = serializers.BooleanField(read_only=True)
|
is_member = serializers.BooleanField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -507,12 +539,15 @@ class IssueStateFlatSerializer(BaseSerializer):
|
|||||||
|
|
||||||
# Issue Serializer with state details
|
# Issue Serializer with state details
|
||||||
class IssueStateSerializer(DynamicBaseSerializer):
|
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")
|
state_detail = StateLiteSerializer(read_only=True, source="state")
|
||||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||||
assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
|
assignee_details = UserLiteSerializer(
|
||||||
|
read_only=True, source="assignees", many=True
|
||||||
|
)
|
||||||
sub_issues_count = serializers.IntegerField(read_only=True)
|
sub_issues_count = serializers.IntegerField(read_only=True)
|
||||||
bridge_id = serializers.UUIDField(read_only=True)
|
|
||||||
attachment_count = serializers.IntegerField(read_only=True)
|
attachment_count = serializers.IntegerField(read_only=True)
|
||||||
link_count = serializers.IntegerField(read_only=True)
|
link_count = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
@ -521,40 +556,80 @@ class IssueStateSerializer(DynamicBaseSerializer):
|
|||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
class IssueSerializer(BaseSerializer):
|
class IssueSerializer(DynamicBaseSerializer):
|
||||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
# ids
|
||||||
state_detail = StateSerializer(read_only=True, source="state")
|
project_id = serializers.PrimaryKeyRelatedField(read_only=True)
|
||||||
parent_detail = IssueStateFlatSerializer(read_only=True, source="parent")
|
state_id = serializers.PrimaryKeyRelatedField(read_only=True)
|
||||||
label_details = LabelSerializer(read_only=True, source="labels", many=True)
|
parent_id = serializers.PrimaryKeyRelatedField(read_only=True)
|
||||||
assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
|
cycle_id = serializers.PrimaryKeyRelatedField(read_only=True)
|
||||||
related_issues = IssueRelationSerializer(read_only=True, source="issue_relation", many=True)
|
module_ids = serializers.SerializerMethodField()
|
||||||
issue_relations = RelatedIssueSerializer(read_only=True, source="issue_related", many=True)
|
|
||||||
issue_cycle = IssueCycleDetailSerializer(read_only=True)
|
# Many to many
|
||||||
issue_module = IssueModuleDetailSerializer(read_only=True)
|
label_ids = serializers.PrimaryKeyRelatedField(
|
||||||
issue_link = IssueLinkSerializer(read_only=True, many=True)
|
read_only=True, many=True, source="labels"
|
||||||
issue_attachment = IssueAttachmentSerializer(read_only=True, many=True)
|
)
|
||||||
|
assignee_ids = serializers.PrimaryKeyRelatedField(
|
||||||
|
read_only=True, many=True, source="assignees"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Count items
|
||||||
sub_issues_count = serializers.IntegerField(read_only=True)
|
sub_issues_count = serializers.IntegerField(read_only=True)
|
||||||
issue_reactions = IssueReactionSerializer(read_only=True, many=True)
|
attachment_count = serializers.IntegerField(read_only=True)
|
||||||
|
link_count = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
|
# is_subscribed
|
||||||
|
is_subscribed = serializers.BooleanField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Issue
|
model = Issue
|
||||||
fields = "__all__"
|
fields = [
|
||||||
read_only_fields = [
|
"id",
|
||||||
"workspace",
|
"name",
|
||||||
"project",
|
"state_id",
|
||||||
"created_by",
|
"description_html",
|
||||||
"updated_by",
|
"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",
|
"created_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
|
"created_by",
|
||||||
|
"updated_by",
|
||||||
|
"attachment_count",
|
||||||
|
"link_count",
|
||||||
|
"is_subscribed",
|
||||||
|
"is_draft",
|
||||||
|
"archived_at",
|
||||||
]
|
]
|
||||||
|
read_only_fields = fields
|
||||||
|
|
||||||
|
def get_module_ids(self, obj):
|
||||||
|
# Access the prefetched modules and extract module IDs
|
||||||
|
return [module for module in obj.issue_module.values_list("module_id", flat=True)]
|
||||||
|
|
||||||
|
|
||||||
class IssueLiteSerializer(DynamicBaseSerializer):
|
class IssueLiteSerializer(DynamicBaseSerializer):
|
||||||
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
|
workspace_detail = WorkspaceLiteSerializer(
|
||||||
|
read_only=True, source="workspace"
|
||||||
|
)
|
||||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||||
state_detail = StateLiteSerializer(read_only=True, source="state")
|
state_detail = StateLiteSerializer(read_only=True, source="state")
|
||||||
label_details = LabelLiteSerializer(read_only=True, source="labels", many=True)
|
label_details = LabelLiteSerializer(
|
||||||
assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
|
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)
|
sub_issues_count = serializers.IntegerField(read_only=True)
|
||||||
cycle_id = serializers.UUIDField(read_only=True)
|
cycle_id = serializers.UUIDField(read_only=True)
|
||||||
module_id = serializers.UUIDField(read_only=True)
|
module_id = serializers.UUIDField(read_only=True)
|
||||||
@ -581,7 +656,9 @@ class IssueLiteSerializer(DynamicBaseSerializer):
|
|||||||
class IssuePublicSerializer(BaseSerializer):
|
class IssuePublicSerializer(BaseSerializer):
|
||||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||||
state_detail = StateLiteSerializer(read_only=True, source="state")
|
state_detail = StateLiteSerializer(read_only=True, source="state")
|
||||||
reactions = IssueReactionSerializer(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)
|
votes = IssueVoteSerializer(read_only=True, many=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -604,7 +681,6 @@ class IssuePublicSerializer(BaseSerializer):
|
|||||||
read_only_fields = fields
|
read_only_fields = fields
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class IssueSubscriberSerializer(BaseSerializer):
|
class IssueSubscriberSerializer(BaseSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IssueSubscriber
|
model = IssueSubscriber
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from .base import BaseSerializer
|
from .base import BaseSerializer, DynamicBaseSerializer
|
||||||
from .user import UserLiteSerializer
|
from .user import UserLiteSerializer
|
||||||
from .project import ProjectLiteSerializer
|
from .project import ProjectLiteSerializer
|
||||||
from .workspace import WorkspaceLiteSerializer
|
from .workspace import WorkspaceLiteSerializer
|
||||||
@ -14,6 +14,7 @@ from plane.db.models import (
|
|||||||
ModuleIssue,
|
ModuleIssue,
|
||||||
ModuleLink,
|
ModuleLink,
|
||||||
ModuleFavorite,
|
ModuleFavorite,
|
||||||
|
ModuleUserProperties,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -25,7 +26,9 @@ class ModuleWriteSerializer(BaseSerializer):
|
|||||||
)
|
)
|
||||||
|
|
||||||
project_detail = ProjectLiteSerializer(source="project", read_only=True)
|
project_detail = ProjectLiteSerializer(source="project", read_only=True)
|
||||||
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
|
workspace_detail = WorkspaceLiteSerializer(
|
||||||
|
source="workspace", read_only=True
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Module
|
model = Module
|
||||||
@ -41,12 +44,18 @@ class ModuleWriteSerializer(BaseSerializer):
|
|||||||
|
|
||||||
def to_representation(self, instance):
|
def to_representation(self, instance):
|
||||||
data = super().to_representation(instance)
|
data = super().to_representation(instance)
|
||||||
data['members'] = [str(member.id) for member in instance.members.all()]
|
data["members"] = [str(member.id) for member in instance.members.all()]
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
if data.get("start_date", None) is not None and data.get("target_date", None) is not None and data.get("start_date", None) > data.get("target_date", None):
|
if (
|
||||||
raise serializers.ValidationError("Start date cannot exceed target date")
|
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
|
return data
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
@ -151,7 +160,8 @@ class ModuleLinkSerializer(BaseSerializer):
|
|||||||
# Validation if url already exists
|
# Validation if url already exists
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
if ModuleLink.objects.filter(
|
if ModuleLink.objects.filter(
|
||||||
url=validated_data.get("url"), module_id=validated_data.get("module_id")
|
url=validated_data.get("url"),
|
||||||
|
module_id=validated_data.get("module_id"),
|
||||||
).exists():
|
).exists():
|
||||||
raise serializers.ValidationError(
|
raise serializers.ValidationError(
|
||||||
{"error": "URL already exists for this Issue"}
|
{"error": "URL already exists for this Issue"}
|
||||||
@ -159,10 +169,12 @@ class ModuleLinkSerializer(BaseSerializer):
|
|||||||
return ModuleLink.objects.create(**validated_data)
|
return ModuleLink.objects.create(**validated_data)
|
||||||
|
|
||||||
|
|
||||||
class ModuleSerializer(BaseSerializer):
|
class ModuleSerializer(DynamicBaseSerializer):
|
||||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||||
lead_detail = UserLiteSerializer(read_only=True, source="lead")
|
lead_detail = UserLiteSerializer(read_only=True, source="lead")
|
||||||
members_detail = UserLiteSerializer(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)
|
link_module = ModuleLinkSerializer(read_only=True, many=True)
|
||||||
is_favorite = serializers.BooleanField(read_only=True)
|
is_favorite = serializers.BooleanField(read_only=True)
|
||||||
total_issues = serializers.IntegerField(read_only=True)
|
total_issues = serializers.IntegerField(read_only=True)
|
||||||
@ -196,3 +208,10 @@ class ModuleFavoriteSerializer(BaseSerializer):
|
|||||||
"project",
|
"project",
|
||||||
"user",
|
"user",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleUserPropertiesSerializer(BaseSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = ModuleUserProperties
|
||||||
|
fields = "__all__"
|
||||||
|
read_only_fields = ["workspace", "project", "module", "user"]
|
||||||
|
@ -1,12 +1,21 @@
|
|||||||
# Module imports
|
# Module imports
|
||||||
from .base import BaseSerializer
|
from .base import BaseSerializer
|
||||||
from .user import UserLiteSerializer
|
from .user import UserLiteSerializer
|
||||||
from plane.db.models import Notification
|
from plane.db.models import Notification, UserNotificationPreference
|
||||||
|
|
||||||
|
|
||||||
class NotificationSerializer(BaseSerializer):
|
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:
|
class Meta:
|
||||||
model = Notification
|
model = Notification
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
|
class UserNotificationPreferenceSerializer(BaseSerializer):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = UserNotificationPreference
|
||||||
|
fields = "__all__"
|
||||||
|
@ -6,19 +6,31 @@ from .base import BaseSerializer
|
|||||||
from .issue import IssueFlatSerializer, LabelLiteSerializer
|
from .issue import IssueFlatSerializer, LabelLiteSerializer
|
||||||
from .workspace import WorkspaceLiteSerializer
|
from .workspace import WorkspaceLiteSerializer
|
||||||
from .project import ProjectLiteSerializer
|
from .project import ProjectLiteSerializer
|
||||||
from plane.db.models import Page, PageLog, PageFavorite, PageLabel, Label, Issue, Module
|
from plane.db.models import (
|
||||||
|
Page,
|
||||||
|
PageLog,
|
||||||
|
PageFavorite,
|
||||||
|
PageLabel,
|
||||||
|
Label,
|
||||||
|
Issue,
|
||||||
|
Module,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class PageSerializer(BaseSerializer):
|
class PageSerializer(BaseSerializer):
|
||||||
is_favorite = serializers.BooleanField(read_only=True)
|
is_favorite = serializers.BooleanField(read_only=True)
|
||||||
label_details = LabelLiteSerializer(read_only=True, source="labels", many=True)
|
label_details = LabelLiteSerializer(
|
||||||
|
read_only=True, source="labels", many=True
|
||||||
|
)
|
||||||
labels = serializers.ListField(
|
labels = serializers.ListField(
|
||||||
child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()),
|
child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()),
|
||||||
write_only=True,
|
write_only=True,
|
||||||
required=False,
|
required=False,
|
||||||
)
|
)
|
||||||
project_detail = ProjectLiteSerializer(source="project", read_only=True)
|
project_detail = ProjectLiteSerializer(source="project", read_only=True)
|
||||||
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
|
workspace_detail = WorkspaceLiteSerializer(
|
||||||
|
source="workspace", read_only=True
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Page
|
model = Page
|
||||||
@ -28,9 +40,10 @@ class PageSerializer(BaseSerializer):
|
|||||||
"project",
|
"project",
|
||||||
"owned_by",
|
"owned_by",
|
||||||
]
|
]
|
||||||
|
|
||||||
def to_representation(self, instance):
|
def to_representation(self, instance):
|
||||||
data = super().to_representation(instance)
|
data = super().to_representation(instance)
|
||||||
data['labels'] = [str(label.id) for label in instance.labels.all()]
|
data["labels"] = [str(label.id) for label in instance.labels.all()]
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
@ -94,7 +107,7 @@ class SubPageSerializer(BaseSerializer):
|
|||||||
|
|
||||||
def get_entity_details(self, obj):
|
def get_entity_details(self, obj):
|
||||||
entity_name = obj.entity_name
|
entity_name = obj.entity_name
|
||||||
if entity_name == 'forward_link' or entity_name == 'back_link':
|
if entity_name == "forward_link" or entity_name == "back_link":
|
||||||
try:
|
try:
|
||||||
page = Page.objects.get(pk=obj.entity_identifier)
|
page = Page.objects.get(pk=obj.entity_identifier)
|
||||||
return PageSerializer(page).data
|
return PageSerializer(page).data
|
||||||
@ -104,7 +117,6 @@ class SubPageSerializer(BaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class PageLogSerializer(BaseSerializer):
|
class PageLogSerializer(BaseSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PageLog
|
model = PageLog
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
@ -4,7 +4,10 @@ from rest_framework import serializers
|
|||||||
# Module imports
|
# Module imports
|
||||||
from .base import BaseSerializer, DynamicBaseSerializer
|
from .base import BaseSerializer, DynamicBaseSerializer
|
||||||
from plane.app.serializers.workspace import WorkspaceLiteSerializer
|
from plane.app.serializers.workspace import WorkspaceLiteSerializer
|
||||||
from plane.app.serializers.user import UserLiteSerializer, UserAdminLiteSerializer
|
from plane.app.serializers.user import (
|
||||||
|
UserLiteSerializer,
|
||||||
|
UserAdminLiteSerializer,
|
||||||
|
)
|
||||||
from plane.db.models import (
|
from plane.db.models import (
|
||||||
Project,
|
Project,
|
||||||
ProjectMember,
|
ProjectMember,
|
||||||
@ -17,7 +20,9 @@ from plane.db.models import (
|
|||||||
|
|
||||||
|
|
||||||
class ProjectSerializer(BaseSerializer):
|
class ProjectSerializer(BaseSerializer):
|
||||||
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
|
workspace_detail = WorkspaceLiteSerializer(
|
||||||
|
source="workspace", read_only=True
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Project
|
model = Project
|
||||||
@ -29,12 +34,16 @@ class ProjectSerializer(BaseSerializer):
|
|||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
identifier = validated_data.get("identifier", "").strip().upper()
|
identifier = validated_data.get("identifier", "").strip().upper()
|
||||||
if identifier == "":
|
if identifier == "":
|
||||||
raise serializers.ValidationError(detail="Project Identifier is required")
|
raise serializers.ValidationError(
|
||||||
|
detail="Project Identifier is required"
|
||||||
|
)
|
||||||
|
|
||||||
if ProjectIdentifier.objects.filter(
|
if ProjectIdentifier.objects.filter(
|
||||||
name=identifier, workspace_id=self.context["workspace_id"]
|
name=identifier, workspace_id=self.context["workspace_id"]
|
||||||
).exists():
|
).exists():
|
||||||
raise serializers.ValidationError(detail="Project Identifier is taken")
|
raise serializers.ValidationError(
|
||||||
|
detail="Project Identifier is taken"
|
||||||
|
)
|
||||||
project = Project.objects.create(
|
project = Project.objects.create(
|
||||||
**validated_data, workspace_id=self.context["workspace_id"]
|
**validated_data, workspace_id=self.context["workspace_id"]
|
||||||
)
|
)
|
||||||
@ -73,7 +82,9 @@ class ProjectSerializer(BaseSerializer):
|
|||||||
return project
|
return project
|
||||||
|
|
||||||
# If not same fail update
|
# 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):
|
class ProjectLiteSerializer(BaseSerializer):
|
||||||
@ -160,6 +171,12 @@ class ProjectMemberAdminSerializer(BaseSerializer):
|
|||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectMemberRoleSerializer(DynamicBaseSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = ProjectMember
|
||||||
|
fields = ("id", "role", "member", "project")
|
||||||
|
|
||||||
|
|
||||||
class ProjectMemberInviteSerializer(BaseSerializer):
|
class ProjectMemberInviteSerializer(BaseSerializer):
|
||||||
project = ProjectLiteSerializer(read_only=True)
|
project = ProjectLiteSerializer(read_only=True)
|
||||||
workspace = WorkspaceLiteSerializer(read_only=True)
|
workspace = WorkspaceLiteSerializer(read_only=True)
|
||||||
@ -197,7 +214,9 @@ class ProjectMemberLiteSerializer(BaseSerializer):
|
|||||||
|
|
||||||
class ProjectDeployBoardSerializer(BaseSerializer):
|
class ProjectDeployBoardSerializer(BaseSerializer):
|
||||||
project_details = ProjectLiteSerializer(read_only=True, source="project")
|
project_details = ProjectLiteSerializer(read_only=True, source="project")
|
||||||
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
|
workspace_detail = WorkspaceLiteSerializer(
|
||||||
|
read_only=True, source="workspace"
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ProjectDeployBoard
|
model = ProjectDeployBoard
|
||||||
|
@ -6,10 +6,19 @@ from plane.db.models import State
|
|||||||
|
|
||||||
|
|
||||||
class StateSerializer(BaseSerializer):
|
class StateSerializer(BaseSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = State
|
model = State
|
||||||
fields = "__all__"
|
fields = [
|
||||||
|
"id",
|
||||||
|
"project_id",
|
||||||
|
"workspace_id",
|
||||||
|
"name",
|
||||||
|
"color",
|
||||||
|
"group",
|
||||||
|
"default",
|
||||||
|
"description",
|
||||||
|
"sequence",
|
||||||
|
]
|
||||||
read_only_fields = [
|
read_only_fields = [
|
||||||
"workspace",
|
"workspace",
|
||||||
"project",
|
"project",
|
||||||
|
@ -99,7 +99,9 @@ class UserMeSettingsSerializer(BaseSerializer):
|
|||||||
).first()
|
).first()
|
||||||
return {
|
return {
|
||||||
"last_workspace_id": obj.last_workspace_id,
|
"last_workspace_id": obj.last_workspace_id,
|
||||||
"last_workspace_slug": workspace.slug if workspace is not None else "",
|
"last_workspace_slug": workspace.slug
|
||||||
|
if workspace is not None
|
||||||
|
else "",
|
||||||
"fallback_workspace_id": obj.last_workspace_id,
|
"fallback_workspace_id": obj.last_workspace_id,
|
||||||
"fallback_workspace_slug": workspace.slug
|
"fallback_workspace_slug": workspace.slug
|
||||||
if workspace is not None
|
if workspace is not None
|
||||||
@ -109,7 +111,8 @@ class UserMeSettingsSerializer(BaseSerializer):
|
|||||||
else:
|
else:
|
||||||
fallback_workspace = (
|
fallback_workspace = (
|
||||||
Workspace.objects.filter(
|
Workspace.objects.filter(
|
||||||
workspace_member__member_id=obj.id, workspace_member__is_active=True
|
workspace_member__member_id=obj.id,
|
||||||
|
workspace_member__is_active=True,
|
||||||
)
|
)
|
||||||
.order_by("created_at")
|
.order_by("created_at")
|
||||||
.first()
|
.first()
|
||||||
@ -180,7 +183,9 @@ class ChangePasswordSerializer(serializers.Serializer):
|
|||||||
|
|
||||||
if data.get("new_password") != data.get("confirm_password"):
|
if data.get("new_password") != data.get("confirm_password"):
|
||||||
raise serializers.ValidationError(
|
raise serializers.ValidationError(
|
||||||
{"error": "Confirm password should be same as the new password."}
|
{
|
||||||
|
"error": "Confirm password should be same as the new password."
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
@ -190,4 +195,5 @@ class ResetPasswordSerializer(serializers.Serializer):
|
|||||||
"""
|
"""
|
||||||
Serializer for password change endpoint.
|
Serializer for password change endpoint.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
new_password = serializers.CharField(required=True, min_length=8)
|
new_password = serializers.CharField(required=True, min_length=8)
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from .base import BaseSerializer
|
from .base import BaseSerializer, DynamicBaseSerializer
|
||||||
from .workspace import WorkspaceLiteSerializer
|
from .workspace import WorkspaceLiteSerializer
|
||||||
from .project import ProjectLiteSerializer
|
from .project import ProjectLiteSerializer
|
||||||
from plane.db.models import GlobalView, IssueView, IssueViewFavorite
|
from plane.db.models import GlobalView, IssueView, IssueViewFavorite
|
||||||
@ -10,7 +10,9 @@ from plane.utils.issue_filters import issue_filters
|
|||||||
|
|
||||||
|
|
||||||
class GlobalViewSerializer(BaseSerializer):
|
class GlobalViewSerializer(BaseSerializer):
|
||||||
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
|
workspace_detail = WorkspaceLiteSerializer(
|
||||||
|
source="workspace", read_only=True
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = GlobalView
|
model = GlobalView
|
||||||
@ -38,10 +40,12 @@ class GlobalViewSerializer(BaseSerializer):
|
|||||||
return super().update(instance, validated_data)
|
return super().update(instance, validated_data)
|
||||||
|
|
||||||
|
|
||||||
class IssueViewSerializer(BaseSerializer):
|
class IssueViewSerializer(DynamicBaseSerializer):
|
||||||
is_favorite = serializers.BooleanField(read_only=True)
|
is_favorite = serializers.BooleanField(read_only=True)
|
||||||
project_detail = ProjectLiteSerializer(source="project", read_only=True)
|
project_detail = ProjectLiteSerializer(source="project", read_only=True)
|
||||||
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
|
workspace_detail = WorkspaceLiteSerializer(
|
||||||
|
source="workspace", read_only=True
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IssueView
|
model = IssueView
|
||||||
|
@ -12,6 +12,7 @@ from .base import DynamicBaseSerializer
|
|||||||
from plane.db.models import Webhook, WebhookLog
|
from plane.db.models import Webhook, WebhookLog
|
||||||
from plane.db.models.webhook import validate_domain, validate_schema
|
from plane.db.models.webhook import validate_domain, validate_schema
|
||||||
|
|
||||||
|
|
||||||
class WebhookSerializer(DynamicBaseSerializer):
|
class WebhookSerializer(DynamicBaseSerializer):
|
||||||
url = serializers.URLField(validators=[validate_schema, validate_domain])
|
url = serializers.URLField(validators=[validate_schema, validate_domain])
|
||||||
|
|
||||||
@ -21,32 +22,49 @@ class WebhookSerializer(DynamicBaseSerializer):
|
|||||||
# Extract the hostname from the URL
|
# Extract the hostname from the URL
|
||||||
hostname = urlparse(url).hostname
|
hostname = urlparse(url).hostname
|
||||||
if not hostname:
|
if not hostname:
|
||||||
raise serializers.ValidationError({"url": "Invalid URL: No hostname found."})
|
raise serializers.ValidationError(
|
||||||
|
{"url": "Invalid URL: No hostname found."}
|
||||||
|
)
|
||||||
|
|
||||||
# Resolve the hostname to IP addresses
|
# Resolve the hostname to IP addresses
|
||||||
try:
|
try:
|
||||||
ip_addresses = socket.getaddrinfo(hostname, None)
|
ip_addresses = socket.getaddrinfo(hostname, None)
|
||||||
except socket.gaierror:
|
except socket.gaierror:
|
||||||
raise serializers.ValidationError({"url": "Hostname could not be resolved."})
|
raise serializers.ValidationError(
|
||||||
|
{"url": "Hostname could not be resolved."}
|
||||||
|
)
|
||||||
|
|
||||||
if not ip_addresses:
|
if not ip_addresses:
|
||||||
raise serializers.ValidationError({"url": "No IP addresses found for the hostname."})
|
raise serializers.ValidationError(
|
||||||
|
{"url": "No IP addresses found for the hostname."}
|
||||||
|
)
|
||||||
|
|
||||||
for addr in ip_addresses:
|
for addr in ip_addresses:
|
||||||
ip = ipaddress.ip_address(addr[4][0])
|
ip = ipaddress.ip_address(addr[4][0])
|
||||||
if ip.is_private or ip.is_loopback:
|
if ip.is_private or ip.is_loopback:
|
||||||
raise serializers.ValidationError({"url": "URL resolves to a blocked IP address."})
|
raise serializers.ValidationError(
|
||||||
|
{"url": "URL resolves to a blocked IP address."}
|
||||||
|
)
|
||||||
|
|
||||||
# Additional validation for multiple request domains and their subdomains
|
# Additional validation for multiple request domains and their subdomains
|
||||||
request = self.context.get('request')
|
request = self.context.get("request")
|
||||||
disallowed_domains = ['plane.so',] # Add your disallowed domains here
|
disallowed_domains = [
|
||||||
|
"plane.so",
|
||||||
|
] # Add your disallowed domains here
|
||||||
if request:
|
if request:
|
||||||
request_host = request.get_host().split(':')[0] # Remove port if present
|
request_host = request.get_host().split(":")[
|
||||||
|
0
|
||||||
|
] # Remove port if present
|
||||||
disallowed_domains.append(request_host)
|
disallowed_domains.append(request_host)
|
||||||
|
|
||||||
# Check if hostname is a subdomain or exact match of any disallowed domain
|
# 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):
|
if any(
|
||||||
raise serializers.ValidationError({"url": "URL domain or its subdomain is not allowed."})
|
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)
|
return Webhook.objects.create(**validated_data)
|
||||||
|
|
||||||
@ -56,32 +74,49 @@ class WebhookSerializer(DynamicBaseSerializer):
|
|||||||
# Extract the hostname from the URL
|
# Extract the hostname from the URL
|
||||||
hostname = urlparse(url).hostname
|
hostname = urlparse(url).hostname
|
||||||
if not hostname:
|
if not hostname:
|
||||||
raise serializers.ValidationError({"url": "Invalid URL: No hostname found."})
|
raise serializers.ValidationError(
|
||||||
|
{"url": "Invalid URL: No hostname found."}
|
||||||
|
)
|
||||||
|
|
||||||
# Resolve the hostname to IP addresses
|
# Resolve the hostname to IP addresses
|
||||||
try:
|
try:
|
||||||
ip_addresses = socket.getaddrinfo(hostname, None)
|
ip_addresses = socket.getaddrinfo(hostname, None)
|
||||||
except socket.gaierror:
|
except socket.gaierror:
|
||||||
raise serializers.ValidationError({"url": "Hostname could not be resolved."})
|
raise serializers.ValidationError(
|
||||||
|
{"url": "Hostname could not be resolved."}
|
||||||
|
)
|
||||||
|
|
||||||
if not ip_addresses:
|
if not ip_addresses:
|
||||||
raise serializers.ValidationError({"url": "No IP addresses found for the hostname."})
|
raise serializers.ValidationError(
|
||||||
|
{"url": "No IP addresses found for the hostname."}
|
||||||
|
)
|
||||||
|
|
||||||
for addr in ip_addresses:
|
for addr in ip_addresses:
|
||||||
ip = ipaddress.ip_address(addr[4][0])
|
ip = ipaddress.ip_address(addr[4][0])
|
||||||
if ip.is_private or ip.is_loopback:
|
if ip.is_private or ip.is_loopback:
|
||||||
raise serializers.ValidationError({"url": "URL resolves to a blocked IP address."})
|
raise serializers.ValidationError(
|
||||||
|
{"url": "URL resolves to a blocked IP address."}
|
||||||
|
)
|
||||||
|
|
||||||
# Additional validation for multiple request domains and their subdomains
|
# Additional validation for multiple request domains and their subdomains
|
||||||
request = self.context.get('request')
|
request = self.context.get("request")
|
||||||
disallowed_domains = ['plane.so',] # Add your disallowed domains here
|
disallowed_domains = [
|
||||||
|
"plane.so",
|
||||||
|
] # Add your disallowed domains here
|
||||||
if request:
|
if request:
|
||||||
request_host = request.get_host().split(':')[0] # Remove port if present
|
request_host = request.get_host().split(":")[
|
||||||
|
0
|
||||||
|
] # Remove port if present
|
||||||
disallowed_domains.append(request_host)
|
disallowed_domains.append(request_host)
|
||||||
|
|
||||||
# Check if hostname is a subdomain or exact match of any disallowed domain
|
# 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):
|
if any(
|
||||||
raise serializers.ValidationError({"url": "URL domain or its subdomain is not allowed."})
|
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)
|
return super().update(instance, validated_data)
|
||||||
|
|
||||||
@ -95,12 +130,7 @@ class WebhookSerializer(DynamicBaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class WebhookLogSerializer(DynamicBaseSerializer):
|
class WebhookLogSerializer(DynamicBaseSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = WebhookLog
|
model = WebhookLog
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
read_only_fields = [
|
read_only_fields = ["workspace", "webhook"]
|
||||||
"workspace",
|
|
||||||
"webhook"
|
|
||||||
]
|
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from .base import BaseSerializer
|
from .base import BaseSerializer, DynamicBaseSerializer
|
||||||
from .user import UserLiteSerializer, UserAdminLiteSerializer
|
from .user import UserLiteSerializer, UserAdminLiteSerializer
|
||||||
|
|
||||||
from plane.db.models import (
|
from plane.db.models import (
|
||||||
@ -13,10 +13,11 @@ from plane.db.models import (
|
|||||||
TeamMember,
|
TeamMember,
|
||||||
WorkspaceMemberInvite,
|
WorkspaceMemberInvite,
|
||||||
WorkspaceTheme,
|
WorkspaceTheme,
|
||||||
|
WorkspaceUserProperties,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class WorkSpaceSerializer(BaseSerializer):
|
class WorkSpaceSerializer(DynamicBaseSerializer):
|
||||||
owner = UserLiteSerializer(read_only=True)
|
owner = UserLiteSerializer(read_only=True)
|
||||||
total_members = serializers.IntegerField(read_only=True)
|
total_members = serializers.IntegerField(read_only=True)
|
||||||
total_issues = serializers.IntegerField(read_only=True)
|
total_issues = serializers.IntegerField(read_only=True)
|
||||||
@ -50,6 +51,7 @@ class WorkSpaceSerializer(BaseSerializer):
|
|||||||
"owner",
|
"owner",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class WorkspaceLiteSerializer(BaseSerializer):
|
class WorkspaceLiteSerializer(BaseSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Workspace
|
model = Workspace
|
||||||
@ -61,8 +63,7 @@ class WorkspaceLiteSerializer(BaseSerializer):
|
|||||||
read_only_fields = fields
|
read_only_fields = fields
|
||||||
|
|
||||||
|
|
||||||
|
class WorkSpaceMemberSerializer(DynamicBaseSerializer):
|
||||||
class WorkSpaceMemberSerializer(BaseSerializer):
|
|
||||||
member = UserLiteSerializer(read_only=True)
|
member = UserLiteSerializer(read_only=True)
|
||||||
workspace = WorkspaceLiteSerializer(read_only=True)
|
workspace = WorkspaceLiteSerializer(read_only=True)
|
||||||
|
|
||||||
@ -72,13 +73,12 @@ class WorkSpaceMemberSerializer(BaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class WorkspaceMemberMeSerializer(BaseSerializer):
|
class WorkspaceMemberMeSerializer(BaseSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = WorkspaceMember
|
model = WorkspaceMember
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
class WorkspaceMemberAdminSerializer(BaseSerializer):
|
class WorkspaceMemberAdminSerializer(DynamicBaseSerializer):
|
||||||
member = UserAdminLiteSerializer(read_only=True)
|
member = UserAdminLiteSerializer(read_only=True)
|
||||||
workspace = WorkspaceLiteSerializer(read_only=True)
|
workspace = WorkspaceLiteSerializer(read_only=True)
|
||||||
|
|
||||||
@ -108,7 +108,9 @@ class WorkSpaceMemberInviteSerializer(BaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class TeamSerializer(BaseSerializer):
|
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(
|
members = serializers.ListField(
|
||||||
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
|
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
|
||||||
write_only=True,
|
write_only=True,
|
||||||
@ -145,7 +147,9 @@ class TeamSerializer(BaseSerializer):
|
|||||||
members = validated_data.pop("members")
|
members = validated_data.pop("members")
|
||||||
TeamMember.objects.filter(team=instance).delete()
|
TeamMember.objects.filter(team=instance).delete()
|
||||||
team_members = [
|
team_members = [
|
||||||
TeamMember(member=member, team=instance, workspace=instance.workspace)
|
TeamMember(
|
||||||
|
member=member, team=instance, workspace=instance.workspace
|
||||||
|
)
|
||||||
for member in members
|
for member in members
|
||||||
]
|
]
|
||||||
TeamMember.objects.bulk_create(team_members, batch_size=10)
|
TeamMember.objects.bulk_create(team_members, batch_size=10)
|
||||||
@ -161,3 +165,13 @@ class WorkspaceThemeSerializer(BaseSerializer):
|
|||||||
"workspace",
|
"workspace",
|
||||||
"actor",
|
"actor",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class WorkspaceUserPropertiesSerializer(BaseSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = WorkspaceUserProperties
|
||||||
|
fields = "__all__"
|
||||||
|
read_only_fields = [
|
||||||
|
"workspace",
|
||||||
|
"user",
|
||||||
|
]
|
||||||
|
@ -3,6 +3,7 @@ from .asset import urlpatterns as asset_urls
|
|||||||
from .authentication import urlpatterns as authentication_urls
|
from .authentication import urlpatterns as authentication_urls
|
||||||
from .config import urlpatterns as configuration_urls
|
from .config import urlpatterns as configuration_urls
|
||||||
from .cycle import urlpatterns as cycle_urls
|
from .cycle import urlpatterns as cycle_urls
|
||||||
|
from .dashboard import urlpatterns as dashboard_urls
|
||||||
from .estimate import urlpatterns as estimate_urls
|
from .estimate import urlpatterns as estimate_urls
|
||||||
from .external import urlpatterns as external_urls
|
from .external import urlpatterns as external_urls
|
||||||
from .importer import urlpatterns as importer_urls
|
from .importer import urlpatterns as importer_urls
|
||||||
@ -28,6 +29,7 @@ urlpatterns = [
|
|||||||
*authentication_urls,
|
*authentication_urls,
|
||||||
*configuration_urls,
|
*configuration_urls,
|
||||||
*cycle_urls,
|
*cycle_urls,
|
||||||
|
*dashboard_urls,
|
||||||
*estimate_urls,
|
*estimate_urls,
|
||||||
*external_urls,
|
*external_urls,
|
||||||
*importer_urls,
|
*importer_urls,
|
||||||
|
@ -31,8 +31,14 @@ urlpatterns = [
|
|||||||
path("sign-in/", SignInEndpoint.as_view(), name="sign-in"),
|
path("sign-in/", SignInEndpoint.as_view(), name="sign-in"),
|
||||||
path("sign-out/", SignOutEndpoint.as_view(), name="sign-out"),
|
path("sign-out/", SignOutEndpoint.as_view(), name="sign-out"),
|
||||||
# magic sign in
|
# magic sign in
|
||||||
path("magic-generate/", MagicGenerateEndpoint.as_view(), name="magic-generate"),
|
path(
|
||||||
path("magic-sign-in/", MagicSignInEndpoint.as_view(), name="magic-sign-in"),
|
"magic-generate/",
|
||||||
|
MagicGenerateEndpoint.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("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
|
||||||
# Password Manipulation
|
# Password Manipulation
|
||||||
path(
|
path(
|
||||||
@ -52,6 +58,8 @@ urlpatterns = [
|
|||||||
),
|
),
|
||||||
# API Tokens
|
# API Tokens
|
||||||
path("api-tokens/", ApiTokenEndpoint.as_view(), name="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
|
## End API Tokens
|
||||||
]
|
]
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
|
|
||||||
from plane.app.views import ConfigurationEndpoint
|
from plane.app.views import ConfigurationEndpoint, MobileConfigurationEndpoint
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path(
|
path(
|
||||||
@ -9,4 +9,9 @@ urlpatterns = [
|
|||||||
ConfigurationEndpoint.as_view(),
|
ConfigurationEndpoint.as_view(),
|
||||||
name="configuration",
|
name="configuration",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"mobile-configs/",
|
||||||
|
MobileConfigurationEndpoint.as_view(),
|
||||||
|
name="configuration",
|
||||||
|
),
|
||||||
]
|
]
|
@ -7,6 +7,7 @@ from plane.app.views import (
|
|||||||
CycleDateCheckEndpoint,
|
CycleDateCheckEndpoint,
|
||||||
CycleFavoriteViewSet,
|
CycleFavoriteViewSet,
|
||||||
TransferCycleIssueEndpoint,
|
TransferCycleIssueEndpoint,
|
||||||
|
CycleUserPropertiesEndpoint,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -44,7 +45,7 @@ urlpatterns = [
|
|||||||
name="project-issue-cycle",
|
name="project-issue-cycle",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/cycle-issues/<uuid:pk>/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/cycle-issues/<uuid:issue_id>/",
|
||||||
CycleIssueViewSet.as_view(
|
CycleIssueViewSet.as_view(
|
||||||
{
|
{
|
||||||
"get": "retrieve",
|
"get": "retrieve",
|
||||||
@ -84,4 +85,9 @@ urlpatterns = [
|
|||||||
TransferCycleIssueEndpoint.as_view(),
|
TransferCycleIssueEndpoint.as_view(),
|
||||||
name="transfer-issues",
|
name="transfer-issues",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/user-properties/",
|
||||||
|
CycleUserPropertiesEndpoint.as_view(),
|
||||||
|
name="cycle-user-filters",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
23
apiserver/plane/app/urls/dashboard.py
Normal file
23
apiserver/plane/app/urls/dashboard.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
|
||||||
|
from plane.app.views import DashboardEndpoint, WidgetsEndpoint
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/dashboard/",
|
||||||
|
DashboardEndpoint.as_view(),
|
||||||
|
name="dashboard",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/dashboard/<uuid:dashboard_id>/",
|
||||||
|
DashboardEndpoint.as_view(),
|
||||||
|
name="dashboard",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"dashboard/<uuid:dashboard_id>/widgets/<uuid:widget_id>/",
|
||||||
|
WidgetsEndpoint.as_view(),
|
||||||
|
name="widgets",
|
||||||
|
),
|
||||||
|
]
|
@ -40,7 +40,7 @@ urlpatterns = [
|
|||||||
name="inbox-issue",
|
name="inbox-issue",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/<uuid:inbox_id>/inbox-issues/<uuid:pk>/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/<uuid:inbox_id>/inbox-issues/<uuid:issue_id>/",
|
||||||
InboxIssueViewSet.as_view(
|
InboxIssueViewSet.as_view(
|
||||||
{
|
{
|
||||||
"get": "retrieve",
|
"get": "retrieve",
|
||||||
|
@ -235,7 +235,7 @@ urlpatterns = [
|
|||||||
## End Comment Reactions
|
## End Comment Reactions
|
||||||
## IssueProperty
|
## IssueProperty
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-display-properties/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/user-properties/",
|
||||||
IssueUserDisplayPropertyEndpoint.as_view(),
|
IssueUserDisplayPropertyEndpoint.as_view(),
|
||||||
name="project-issue-display-properties",
|
name="project-issue-display-properties",
|
||||||
),
|
),
|
||||||
@ -275,16 +275,17 @@ urlpatterns = [
|
|||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-relation/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-relation/",
|
||||||
IssueRelationViewSet.as_view(
|
IssueRelationViewSet.as_view(
|
||||||
{
|
{
|
||||||
|
"get": "list",
|
||||||
"post": "create",
|
"post": "create",
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
name="issue-relation",
|
name="issue-relation",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-relation/<uuid:pk>/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/remove-relation/",
|
||||||
IssueRelationViewSet.as_view(
|
IssueRelationViewSet.as_view(
|
||||||
{
|
{
|
||||||
"delete": "destroy",
|
"post": "remove_relation",
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
name="issue-relation",
|
name="issue-relation",
|
||||||
|
@ -7,6 +7,7 @@ from plane.app.views import (
|
|||||||
ModuleLinkViewSet,
|
ModuleLinkViewSet,
|
||||||
ModuleFavoriteViewSet,
|
ModuleFavoriteViewSet,
|
||||||
BulkImportModulesEndpoint,
|
BulkImportModulesEndpoint,
|
||||||
|
ModuleUserPropertiesEndpoint,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -34,17 +35,26 @@ urlpatterns = [
|
|||||||
name="project-modules",
|
name="project-modules",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/module-issues/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/modules/",
|
||||||
ModuleIssueViewSet.as_view(
|
ModuleIssueViewSet.as_view(
|
||||||
{
|
{
|
||||||
|
"post": "create_issue_modules",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="issue-module",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/issues/",
|
||||||
|
ModuleIssueViewSet.as_view(
|
||||||
|
{
|
||||||
|
"post": "create_module_issues",
|
||||||
"get": "list",
|
"get": "list",
|
||||||
"post": "create",
|
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
name="project-module-issues",
|
name="project-module-issues",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/module-issues/<uuid:pk>/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/issues/<uuid:issue_id>/",
|
||||||
ModuleIssueViewSet.as_view(
|
ModuleIssueViewSet.as_view(
|
||||||
{
|
{
|
||||||
"get": "retrieve",
|
"get": "retrieve",
|
||||||
@ -101,4 +111,9 @@ urlpatterns = [
|
|||||||
BulkImportModulesEndpoint.as_view(),
|
BulkImportModulesEndpoint.as_view(),
|
||||||
name="bulk-modules-create",
|
name="bulk-modules-create",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/user-properties/",
|
||||||
|
ModuleUserPropertiesEndpoint.as_view(),
|
||||||
|
name="cycle-user-filters",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
@ -5,6 +5,7 @@ from plane.app.views import (
|
|||||||
NotificationViewSet,
|
NotificationViewSet,
|
||||||
UnreadNotificationEndpoint,
|
UnreadNotificationEndpoint,
|
||||||
MarkAllReadNotificationViewSet,
|
MarkAllReadNotificationViewSet,
|
||||||
|
UserNotificationPreferenceEndpoint,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -63,4 +64,9 @@ urlpatterns = [
|
|||||||
),
|
),
|
||||||
name="mark-all-read-notifications",
|
name="mark-all-read-notifications",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"users/me/notification-preferences/",
|
||||||
|
UserNotificationPreferenceEndpoint.as_view(),
|
||||||
|
name="user-notification-preferences",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
@ -18,6 +18,10 @@ from plane.app.views import (
|
|||||||
WorkspaceUserProfileEndpoint,
|
WorkspaceUserProfileEndpoint,
|
||||||
WorkspaceUserProfileIssuesEndpoint,
|
WorkspaceUserProfileIssuesEndpoint,
|
||||||
WorkspaceLabelsEndpoint,
|
WorkspaceLabelsEndpoint,
|
||||||
|
WorkspaceProjectMemberEndpoint,
|
||||||
|
WorkspaceUserPropertiesEndpoint,
|
||||||
|
WorkspaceStatesEndpoint,
|
||||||
|
WorkspaceEstimatesEndpoint,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -92,6 +96,11 @@ urlpatterns = [
|
|||||||
WorkSpaceMemberViewSet.as_view({"get": "list"}),
|
WorkSpaceMemberViewSet.as_view({"get": "list"}),
|
||||||
name="workspace-member",
|
name="workspace-member",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/project-members/",
|
||||||
|
WorkspaceProjectMemberEndpoint.as_view(),
|
||||||
|
name="workspace-member-roles",
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/members/<uuid:pk>/",
|
"workspaces/<str:slug>/members/<uuid:pk>/",
|
||||||
WorkSpaceMemberViewSet.as_view(
|
WorkSpaceMemberViewSet.as_view(
|
||||||
@ -195,4 +204,19 @@ urlpatterns = [
|
|||||||
WorkspaceLabelsEndpoint.as_view(),
|
WorkspaceLabelsEndpoint.as_view(),
|
||||||
name="workspace-labels",
|
name="workspace-labels",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/user-properties/",
|
||||||
|
WorkspaceUserPropertiesEndpoint.as_view(),
|
||||||
|
name="workspace-user-filters",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/states/",
|
||||||
|
WorkspaceStatesEndpoint.as_view(),
|
||||||
|
name="workspace-state",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/estimates/",
|
||||||
|
WorkspaceEstimatesEndpoint.as_view(),
|
||||||
|
name="workspace-estimate",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
@ -192,7 +192,7 @@ from plane.app.views import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
#TODO: Delete this file
|
# TODO: Delete this file
|
||||||
# This url file has been deprecated use apiserver/plane/urls folder to create new urls
|
# This url file has been deprecated use apiserver/plane/urls folder to create new urls
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
@ -204,10 +204,14 @@ urlpatterns = [
|
|||||||
path("sign-out/", SignOutEndpoint.as_view(), name="sign-out"),
|
path("sign-out/", SignOutEndpoint.as_view(), name="sign-out"),
|
||||||
# Magic Sign In/Up
|
# Magic Sign In/Up
|
||||||
path(
|
path(
|
||||||
"magic-generate/", 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(
|
||||||
path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
|
"magic-sign-in/", MagicSignInEndpoint.as_view(), name="magic-sign-in"
|
||||||
|
),
|
||||||
|
path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
|
||||||
# Email verification
|
# Email verification
|
||||||
path("email-verify/", VerifyEmailEndpoint.as_view(), name="email-verify"),
|
path("email-verify/", VerifyEmailEndpoint.as_view(), name="email-verify"),
|
||||||
path(
|
path(
|
||||||
@ -272,7 +276,9 @@ urlpatterns = [
|
|||||||
# user workspace invitations
|
# user workspace invitations
|
||||||
path(
|
path(
|
||||||
"users/me/invitations/workspaces/",
|
"users/me/invitations/workspaces/",
|
||||||
UserWorkspaceInvitationsEndpoint.as_view({"get": "list", "post": "create"}),
|
UserWorkspaceInvitationsEndpoint.as_view(
|
||||||
|
{"get": "list", "post": "create"}
|
||||||
|
),
|
||||||
name="user-workspace-invitations",
|
name="user-workspace-invitations",
|
||||||
),
|
),
|
||||||
# user workspace invitation
|
# user workspace invitation
|
||||||
@ -311,7 +317,9 @@ urlpatterns = [
|
|||||||
# user project invitations
|
# user project invitations
|
||||||
path(
|
path(
|
||||||
"users/me/invitations/projects/",
|
"users/me/invitations/projects/",
|
||||||
UserProjectInvitationsViewset.as_view({"get": "list", "post": "create"}),
|
UserProjectInvitationsViewset.as_view(
|
||||||
|
{"get": "list", "post": "create"}
|
||||||
|
),
|
||||||
name="user-project-invitaions",
|
name="user-project-invitaions",
|
||||||
),
|
),
|
||||||
## Workspaces ##
|
## Workspaces ##
|
||||||
@ -1238,7 +1246,7 @@ urlpatterns = [
|
|||||||
"post": "unarchive",
|
"post": "unarchive",
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
name="project-page-unarchive"
|
name="project-page-unarchive",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-pages/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-pages/",
|
||||||
@ -1264,19 +1272,22 @@ urlpatterns = [
|
|||||||
{
|
{
|
||||||
"post": "unlock",
|
"post": "unlock",
|
||||||
}
|
}
|
||||||
)
|
),
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/transactions/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/transactions/",
|
||||||
PageLogEndpoint.as_view(), name="page-transactions"
|
PageLogEndpoint.as_view(),
|
||||||
|
name="page-transactions",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/transactions/<uuid:transaction>/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/transactions/<uuid:transaction>/",
|
||||||
PageLogEndpoint.as_view(), name="page-transactions"
|
PageLogEndpoint.as_view(),
|
||||||
|
name="page-transactions",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/sub-pages/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/sub-pages/",
|
||||||
SubPagesEndpoint.as_view(), name="sub-page"
|
SubPagesEndpoint.as_view(),
|
||||||
|
name="sub-page",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/",
|
||||||
@ -1326,7 +1337,9 @@ urlpatterns = [
|
|||||||
## End Pages
|
## End Pages
|
||||||
# API Tokens
|
# API Tokens
|
||||||
path("api-tokens/", ApiTokenEndpoint.as_view(), name="api-tokens"),
|
path("api-tokens/", ApiTokenEndpoint.as_view(), name="api-tokens"),
|
||||||
path("api-tokens/<uuid:pk>/", ApiTokenEndpoint.as_view(), name="api-tokens"),
|
path(
|
||||||
|
"api-tokens/<uuid:pk>/", ApiTokenEndpoint.as_view(), name="api-tokens"
|
||||||
|
),
|
||||||
## End API Tokens
|
## End API Tokens
|
||||||
# Integrations
|
# Integrations
|
||||||
path(
|
path(
|
||||||
|
@ -45,6 +45,10 @@ from .workspace import (
|
|||||||
WorkspaceUserProfileEndpoint,
|
WorkspaceUserProfileEndpoint,
|
||||||
WorkspaceUserProfileIssuesEndpoint,
|
WorkspaceUserProfileIssuesEndpoint,
|
||||||
WorkspaceLabelsEndpoint,
|
WorkspaceLabelsEndpoint,
|
||||||
|
WorkspaceProjectMemberEndpoint,
|
||||||
|
WorkspaceUserPropertiesEndpoint,
|
||||||
|
WorkspaceStatesEndpoint,
|
||||||
|
WorkspaceEstimatesEndpoint,
|
||||||
)
|
)
|
||||||
from .state import StateViewSet
|
from .state import StateViewSet
|
||||||
from .view import (
|
from .view import (
|
||||||
@ -59,6 +63,7 @@ from .cycle import (
|
|||||||
CycleDateCheckEndpoint,
|
CycleDateCheckEndpoint,
|
||||||
CycleFavoriteViewSet,
|
CycleFavoriteViewSet,
|
||||||
TransferCycleIssueEndpoint,
|
TransferCycleIssueEndpoint,
|
||||||
|
CycleUserPropertiesEndpoint,
|
||||||
)
|
)
|
||||||
from .asset import FileAssetEndpoint, UserAssetsEndpoint, FileAssetViewSet
|
from .asset import FileAssetEndpoint, UserAssetsEndpoint, FileAssetViewSet
|
||||||
from .issue import (
|
from .issue import (
|
||||||
@ -103,6 +108,7 @@ from .module import (
|
|||||||
ModuleIssueViewSet,
|
ModuleIssueViewSet,
|
||||||
ModuleLinkViewSet,
|
ModuleLinkViewSet,
|
||||||
ModuleFavoriteViewSet,
|
ModuleFavoriteViewSet,
|
||||||
|
ModuleUserPropertiesEndpoint,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .api import ApiTokenEndpoint
|
from .api import ApiTokenEndpoint
|
||||||
@ -136,7 +142,11 @@ from .page import (
|
|||||||
from .search import GlobalSearchEndpoint, IssueSearchEndpoint
|
from .search import GlobalSearchEndpoint, IssueSearchEndpoint
|
||||||
|
|
||||||
|
|
||||||
from .external import GPTIntegrationEndpoint, ReleaseNotesEndpoint, UnsplashEndpoint
|
from .external import (
|
||||||
|
GPTIntegrationEndpoint,
|
||||||
|
ReleaseNotesEndpoint,
|
||||||
|
UnsplashEndpoint,
|
||||||
|
)
|
||||||
|
|
||||||
from .estimate import (
|
from .estimate import (
|
||||||
ProjectEstimatePointEndpoint,
|
ProjectEstimatePointEndpoint,
|
||||||
@ -157,14 +167,20 @@ from .notification import (
|
|||||||
NotificationViewSet,
|
NotificationViewSet,
|
||||||
UnreadNotificationEndpoint,
|
UnreadNotificationEndpoint,
|
||||||
MarkAllReadNotificationViewSet,
|
MarkAllReadNotificationViewSet,
|
||||||
|
UserNotificationPreferenceEndpoint,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .exporter import ExportIssuesEndpoint
|
from .exporter import ExportIssuesEndpoint
|
||||||
|
|
||||||
from .config import ConfigurationEndpoint
|
from .config import ConfigurationEndpoint, MobileConfigurationEndpoint
|
||||||
|
|
||||||
from .webhook import (
|
from .webhook import (
|
||||||
WebhookEndpoint,
|
WebhookEndpoint,
|
||||||
WebhookLogsEndpoint,
|
WebhookLogsEndpoint,
|
||||||
WebhookSecretRegenerateEndpoint,
|
WebhookSecretRegenerateEndpoint,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from .dashboard import (
|
||||||
|
DashboardEndpoint,
|
||||||
|
WidgetsEndpoint
|
||||||
|
)
|
@ -61,7 +61,9 @@ class AnalyticsEndpoint(BaseAPIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# If segment is present it cannot be same as x-axis
|
# If segment is present it cannot be same as x-axis
|
||||||
if segment and (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(
|
return Response(
|
||||||
{
|
{
|
||||||
"error": "Both segment and x axis cannot be same and segment should be valid"
|
"error": "Both segment and x axis cannot be same and segment should be valid"
|
||||||
@ -110,7 +112,9 @@ class AnalyticsEndpoint(BaseAPIView):
|
|||||||
if x_axis in ["assignees__id"] or segment in ["assignees__id"]:
|
if x_axis in ["assignees__id"] or segment in ["assignees__id"]:
|
||||||
assignee_details = (
|
assignee_details = (
|
||||||
Issue.issue_objects.filter(
|
Issue.issue_objects.filter(
|
||||||
workspace__slug=slug, **filters, assignees__avatar__isnull=False
|
workspace__slug=slug,
|
||||||
|
**filters,
|
||||||
|
assignees__avatar__isnull=False,
|
||||||
)
|
)
|
||||||
.order_by("assignees__id")
|
.order_by("assignees__id")
|
||||||
.distinct("assignees__id")
|
.distinct("assignees__id")
|
||||||
@ -124,7 +128,9 @@ class AnalyticsEndpoint(BaseAPIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
cycle_details = {}
|
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 = (
|
cycle_details = (
|
||||||
Issue.issue_objects.filter(
|
Issue.issue_objects.filter(
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
@ -186,7 +192,9 @@ class AnalyticViewViewset(BaseViewSet):
|
|||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.filter_queryset(
|
return self.filter_queryset(
|
||||||
super().get_queryset().filter(workspace__slug=self.kwargs.get("slug"))
|
super()
|
||||||
|
.get_queryset()
|
||||||
|
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -196,7 +204,9 @@ class SavedAnalyticEndpoint(BaseAPIView):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def get(self, request, slug, analytic_id):
|
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
|
filter = analytic_view.query
|
||||||
queryset = Issue.issue_objects.filter(**filter)
|
queryset = Issue.issue_objects.filter(**filter)
|
||||||
@ -266,7 +276,9 @@ class ExportAnalyticsEndpoint(BaseAPIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# If segment is present it cannot be same as x-axis
|
# If segment is present it cannot be same as x-axis
|
||||||
if segment and (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(
|
return Response(
|
||||||
{
|
{
|
||||||
"error": "Both segment and x axis cannot be same and segment should be valid"
|
"error": "Both segment and x axis cannot be same and segment should be valid"
|
||||||
@ -293,7 +305,9 @@ class DefaultAnalyticsEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
def get(self, request, slug):
|
def get(self, request, slug):
|
||||||
filters = issue_filters(request.GET, "GET")
|
filters = issue_filters(request.GET, "GET")
|
||||||
base_issues = Issue.issue_objects.filter(workspace__slug=slug, **filters)
|
base_issues = Issue.issue_objects.filter(
|
||||||
|
workspace__slug=slug, **filters
|
||||||
|
)
|
||||||
|
|
||||||
total_issues = base_issues.count()
|
total_issues = base_issues.count()
|
||||||
|
|
||||||
@ -306,7 +320,9 @@ class DefaultAnalyticsEndpoint(BaseAPIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
open_issues_groups = ["backlog", "unstarted", "started"]
|
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 = open_issues_queryset.count()
|
||||||
open_issues_classified = (
|
open_issues_classified = (
|
||||||
@ -361,10 +377,12 @@ class DefaultAnalyticsEndpoint(BaseAPIView):
|
|||||||
.order_by("-count")
|
.order_by("-count")
|
||||||
)
|
)
|
||||||
|
|
||||||
open_estimate_sum = open_issues_queryset.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"
|
"sum"
|
||||||
]
|
]
|
||||||
total_estimate_sum = base_issues.aggregate(sum=Sum("estimate_point"))["sum"]
|
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
|
@ -71,7 +71,9 @@ class ApiTokenEndpoint(BaseAPIView):
|
|||||||
user=request.user,
|
user=request.user,
|
||||||
pk=pk,
|
pk=pk,
|
||||||
)
|
)
|
||||||
serializer = APITokenSerializer(api_token, data=request.data, partial=True)
|
serializer = APITokenSerializer(
|
||||||
|
api_token, data=request.data, partial=True
|
||||||
|
)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
serializer.save()
|
serializer.save()
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
@ -10,7 +10,11 @@ from plane.app.serializers import FileAssetSerializer
|
|||||||
|
|
||||||
|
|
||||||
class FileAssetEndpoint(BaseAPIView):
|
class FileAssetEndpoint(BaseAPIView):
|
||||||
parser_classes = (MultiPartParser, FormParser, JSONParser,)
|
parser_classes = (
|
||||||
|
MultiPartParser,
|
||||||
|
FormParser,
|
||||||
|
JSONParser,
|
||||||
|
)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
A viewset for viewing and editing task instances.
|
A viewset for viewing and editing task instances.
|
||||||
@ -20,10 +24,18 @@ class FileAssetEndpoint(BaseAPIView):
|
|||||||
asset_key = str(workspace_id) + "/" + asset_key
|
asset_key = str(workspace_id) + "/" + asset_key
|
||||||
files = FileAsset.objects.filter(asset=asset_key)
|
files = FileAsset.objects.filter(asset=asset_key)
|
||||||
if files.exists():
|
if files.exists():
|
||||||
serializer = FileAssetSerializer(files, context={"request": request}, many=True)
|
serializer = FileAssetSerializer(
|
||||||
return Response({"data": serializer.data, "status": True}, status=status.HTTP_200_OK)
|
files, context={"request": request}, many=True
|
||||||
|
)
|
||||||
|
return Response(
|
||||||
|
{"data": serializer.data, "status": True},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
return Response({"error": "Asset key does not exist", "status": False}, status=status.HTTP_200_OK)
|
return Response(
|
||||||
|
{"error": "Asset key does not exist", "status": False},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
|
||||||
def post(self, request, slug):
|
def post(self, request, slug):
|
||||||
serializer = FileAssetSerializer(data=request.data)
|
serializer = FileAssetSerializer(data=request.data)
|
||||||
@ -43,7 +55,6 @@ class FileAssetEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
|
|
||||||
class FileAssetViewSet(BaseViewSet):
|
class FileAssetViewSet(BaseViewSet):
|
||||||
|
|
||||||
def restore(self, request, workspace_id, asset_key):
|
def restore(self, request, workspace_id, asset_key):
|
||||||
asset_key = str(workspace_id) + "/" + asset_key
|
asset_key = str(workspace_id) + "/" + asset_key
|
||||||
file_asset = FileAsset.objects.get(asset=asset_key)
|
file_asset = FileAsset.objects.get(asset=asset_key)
|
||||||
@ -56,12 +67,22 @@ class UserAssetsEndpoint(BaseAPIView):
|
|||||||
parser_classes = (MultiPartParser, FormParser)
|
parser_classes = (MultiPartParser, FormParser)
|
||||||
|
|
||||||
def get(self, request, asset_key):
|
def get(self, request, asset_key):
|
||||||
files = FileAsset.objects.filter(asset=asset_key, created_by=request.user)
|
files = FileAsset.objects.filter(
|
||||||
|
asset=asset_key, created_by=request.user
|
||||||
|
)
|
||||||
if files.exists():
|
if files.exists():
|
||||||
serializer = FileAssetSerializer(files, context={"request": request})
|
serializer = FileAssetSerializer(
|
||||||
return Response({"data": serializer.data, "status": True}, status=status.HTTP_200_OK)
|
files, context={"request": request}
|
||||||
|
)
|
||||||
|
return Response(
|
||||||
|
{"data": serializer.data, "status": True},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
return Response({"error": "Asset key does not exist", "status": False}, status=status.HTTP_200_OK)
|
return Response(
|
||||||
|
{"error": "Asset key does not exist", "status": False},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
serializer = FileAssetSerializer(data=request.data)
|
serializer = FileAssetSerializer(data=request.data)
|
||||||
@ -70,9 +91,10 @@ class UserAssetsEndpoint(BaseAPIView):
|
|||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
def delete(self, request, asset_key):
|
def delete(self, request, asset_key):
|
||||||
file_asset = FileAsset.objects.get(asset=asset_key, created_by=request.user)
|
file_asset = FileAsset.objects.get(
|
||||||
|
asset=asset_key, created_by=request.user
|
||||||
|
)
|
||||||
file_asset.is_deleted = True
|
file_asset.is_deleted = True
|
||||||
file_asset.save()
|
file_asset.save()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
@ -128,7 +128,8 @@ class ForgotPasswordEndpoint(BaseAPIView):
|
|||||||
status=status.HTTP_200_OK,
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Please check the email"}, status=status.HTTP_400_BAD_REQUEST
|
{"error": "Please check the email"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -167,7 +168,9 @@ class ResetPasswordEndpoint(BaseAPIView):
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Response(data, status=status.HTTP_200_OK)
|
return Response(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 DjangoUnicodeDecodeError as indentifier:
|
except DjangoUnicodeDecodeError as indentifier:
|
||||||
return Response(
|
return Response(
|
||||||
@ -191,7 +194,8 @@ class ChangePasswordEndpoint(BaseAPIView):
|
|||||||
user.is_password_autoset = False
|
user.is_password_autoset = False
|
||||||
user.save()
|
user.save()
|
||||||
return Response(
|
return Response(
|
||||||
{"message": "Password updated successfully"}, status=status.HTTP_200_OK
|
{"message": "Password updated successfully"},
|
||||||
|
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)
|
||||||
|
|
||||||
@ -213,7 +217,8 @@ class SetUserPasswordEndpoint(BaseAPIView):
|
|||||||
# Check password validation
|
# Check password validation
|
||||||
if not password and len(str(password)) < 8:
|
if not password and len(str(password)) < 8:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Password is not valid"}, status=status.HTTP_400_BAD_REQUEST
|
{"error": "Password is not valid"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Set the user password
|
# Set the user password
|
||||||
@ -281,7 +286,9 @@ class MagicGenerateEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
if data["current_attempt"] > 2:
|
if data["current_attempt"] > 2:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Max attempts exhausted. Please try again later."},
|
{
|
||||||
|
"error": "Max attempts exhausted. Please try again later."
|
||||||
|
},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -339,7 +346,8 @@ class EmailCheckEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
if not email:
|
if not email:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Email is required"}, status=status.HTTP_400_BAD_REQUEST
|
{"error": "Email is required"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
# validate the email
|
# validate the email
|
||||||
@ -347,7 +355,8 @@ class EmailCheckEndpoint(BaseAPIView):
|
|||||||
validate_email(email)
|
validate_email(email)
|
||||||
except ValidationError:
|
except ValidationError:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Email is not valid"}, status=status.HTTP_400_BAD_REQUEST
|
{"error": "Email is not valid"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check if the user exists
|
# Check if the user exists
|
||||||
@ -399,13 +408,18 @@ class EmailCheckEndpoint(BaseAPIView):
|
|||||||
key, token, current_attempt = generate_magic_token(email=email)
|
key, token, current_attempt = generate_magic_token(email=email)
|
||||||
if not current_attempt:
|
if not current_attempt:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Max attempts exhausted. Please try again later."},
|
{
|
||||||
|
"error": "Max attempts exhausted. Please try again later."
|
||||||
|
},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
# Trigger the email
|
# Trigger the email
|
||||||
magic_link.delay(email, "magic_" + str(email), token, current_site)
|
magic_link.delay(email, "magic_" + str(email), token, current_site)
|
||||||
return Response(
|
return Response(
|
||||||
{"is_password_autoset": user.is_password_autoset, "is_existing": False},
|
{
|
||||||
|
"is_password_autoset": user.is_password_autoset,
|
||||||
|
"is_existing": False,
|
||||||
|
},
|
||||||
status=status.HTTP_200_OK,
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -433,7 +447,9 @@ class EmailCheckEndpoint(BaseAPIView):
|
|||||||
key, token, current_attempt = generate_magic_token(email=email)
|
key, token, current_attempt = generate_magic_token(email=email)
|
||||||
if not current_attempt:
|
if not current_attempt:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Max attempts exhausted. Please try again later."},
|
{
|
||||||
|
"error": "Max attempts exhausted. Please try again later."
|
||||||
|
},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -73,7 +73,7 @@ class SignUpEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
# get configuration values
|
# get configuration values
|
||||||
# Get configuration values
|
# Get configuration values
|
||||||
ENABLE_SIGNUP, = get_configuration_value(
|
(ENABLE_SIGNUP,) = get_configuration_value(
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"key": "ENABLE_SIGNUP",
|
"key": "ENABLE_SIGNUP",
|
||||||
@ -173,7 +173,7 @@ class SignInEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
# Create the user
|
# Create the user
|
||||||
else:
|
else:
|
||||||
ENABLE_SIGNUP, = get_configuration_value(
|
(ENABLE_SIGNUP,) = get_configuration_value(
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"key": "ENABLE_SIGNUP",
|
"key": "ENABLE_SIGNUP",
|
||||||
@ -325,7 +325,7 @@ class MagicSignInEndpoint(BaseAPIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
user_token = request.data.get("token", "").strip()
|
user_token = request.data.get("token", "").strip()
|
||||||
key = request.data.get("key", False).strip().lower()
|
key = request.data.get("key", "").strip().lower()
|
||||||
|
|
||||||
if not key or user_token == "":
|
if not key or user_token == "":
|
||||||
return Response(
|
return Response(
|
||||||
@ -364,8 +364,10 @@ class MagicSignInEndpoint(BaseAPIView):
|
|||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
# Check if user has any accepted invites for workspace and add them to workspace
|
# Check if user has any accepted invites for workspace and add them to workspace
|
||||||
workspace_member_invites = WorkspaceMemberInvite.objects.filter(
|
workspace_member_invites = (
|
||||||
email=user.email, accepted=True
|
WorkspaceMemberInvite.objects.filter(
|
||||||
|
email=user.email, accepted=True
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
WorkspaceMember.objects.bulk_create(
|
WorkspaceMember.objects.bulk_create(
|
||||||
@ -431,7 +433,9 @@ class MagicSignInEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Your login code was incorrect. Please try again."},
|
{
|
||||||
|
"error": "Your login code was incorrect. Please try again."
|
||||||
|
},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -46,7 +46,9 @@ class WebhookMixin:
|
|||||||
bulk = False
|
bulk = False
|
||||||
|
|
||||||
def finalize_response(self, request, response, *args, **kwargs):
|
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
|
# Check for the case should webhook be sent
|
||||||
if (
|
if (
|
||||||
@ -88,7 +90,9 @@ class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
|
|||||||
return self.model.objects.all()
|
return self.model.objects.all()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
capture_exception(e)
|
capture_exception(e)
|
||||||
raise APIException("Please check the view", status.HTTP_400_BAD_REQUEST)
|
raise APIException(
|
||||||
|
"Please check the view", status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
def handle_exception(self, exc):
|
def handle_exception(self, exc):
|
||||||
"""
|
"""
|
||||||
@ -99,6 +103,7 @@ class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
|
|||||||
response = super().handle_exception(exc)
|
response = super().handle_exception(exc)
|
||||||
return response
|
return response
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
print(e) if settings.DEBUG else print("Server Error")
|
||||||
if isinstance(e, IntegrityError):
|
if isinstance(e, IntegrityError):
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "The payload is not valid"},
|
{"error": "The payload is not valid"},
|
||||||
@ -112,23 +117,23 @@ class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if isinstance(e, ObjectDoesNotExist):
|
if isinstance(e, ObjectDoesNotExist):
|
||||||
model_name = str(exc).split(" matching query does not exist.")[0]
|
|
||||||
return Response(
|
return Response(
|
||||||
{"error": f"{model_name} does not exist."},
|
{"error": f"The required object does not exist."},
|
||||||
status=status.HTTP_404_NOT_FOUND,
|
status=status.HTTP_404_NOT_FOUND,
|
||||||
)
|
)
|
||||||
|
|
||||||
if isinstance(e, KeyError):
|
if isinstance(e, KeyError):
|
||||||
capture_exception(e)
|
capture_exception(e)
|
||||||
return Response(
|
return Response(
|
||||||
{"error": f"key {e} does not exist"},
|
{"error": f"The required key does not exist."},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
print(e) if settings.DEBUG else print("Server Error")
|
|
||||||
capture_exception(e)
|
capture_exception(e)
|
||||||
return Response({"error": "Something went wrong please try again later"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
return Response(
|
||||||
|
{"error": "Something went wrong please try again later"},
|
||||||
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
)
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
try:
|
try:
|
||||||
@ -159,6 +164,24 @@ class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
|
|||||||
if resolve(self.request.path_info).url_name == "project":
|
if resolve(self.request.path_info).url_name == "project":
|
||||||
return self.kwargs.get("pk", None)
|
return self.kwargs.get("pk", None)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fields(self):
|
||||||
|
fields = [
|
||||||
|
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
|
||||||
|
]
|
||||||
|
return expand if expand else None
|
||||||
|
|
||||||
|
|
||||||
class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
|
class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
@ -201,20 +224,24 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if isinstance(e, ObjectDoesNotExist):
|
if isinstance(e, ObjectDoesNotExist):
|
||||||
model_name = str(exc).split(" matching query does not exist.")[0]
|
|
||||||
return Response(
|
return Response(
|
||||||
{"error": f"{model_name} does not exist."},
|
{"error": f"The required object does not exist."},
|
||||||
status=status.HTTP_404_NOT_FOUND,
|
status=status.HTTP_404_NOT_FOUND,
|
||||||
)
|
)
|
||||||
|
|
||||||
if isinstance(e, KeyError):
|
if isinstance(e, KeyError):
|
||||||
return Response({"error": f"key {e} does not exist"}, status=status.HTTP_400_BAD_REQUEST)
|
return Response(
|
||||||
|
{"error": f"The required key does not exist."},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
print(e)
|
print(e)
|
||||||
capture_exception(e)
|
capture_exception(e)
|
||||||
return Response({"error": "Something went wrong please try again later"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
return Response(
|
||||||
|
{"error": "Something went wrong please try again later"},
|
||||||
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
)
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
try:
|
try:
|
||||||
@ -239,3 +266,21 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
|
|||||||
@property
|
@property
|
||||||
def project_id(self):
|
def project_id(self):
|
||||||
return self.kwargs.get("project_id", None)
|
return self.kwargs.get("project_id", None)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fields(self):
|
||||||
|
fields = [
|
||||||
|
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
|
||||||
|
]
|
||||||
|
return expand if expand else None
|
||||||
|
@ -20,7 +20,6 @@ class ConfigurationEndpoint(BaseAPIView):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
|
|
||||||
# Get all the configuration
|
# Get all the configuration
|
||||||
(
|
(
|
||||||
GOOGLE_CLIENT_ID,
|
GOOGLE_CLIENT_ID,
|
||||||
@ -90,8 +89,16 @@ class ConfigurationEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
data = {}
|
data = {}
|
||||||
# Authentication
|
# Authentication
|
||||||
data["google_client_id"] = GOOGLE_CLIENT_ID if GOOGLE_CLIENT_ID and GOOGLE_CLIENT_ID != "\"\"" else None
|
data["google_client_id"] = (
|
||||||
data["github_client_id"] = GITHUB_CLIENT_ID if GITHUB_CLIENT_ID and GITHUB_CLIENT_ID != "\"\"" else None
|
GOOGLE_CLIENT_ID
|
||||||
|
if GOOGLE_CLIENT_ID and GOOGLE_CLIENT_ID != '""'
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
data["github_client_id"] = (
|
||||||
|
GITHUB_CLIENT_ID
|
||||||
|
if GITHUB_CLIENT_ID and GITHUB_CLIENT_ID != '""'
|
||||||
|
else None
|
||||||
|
)
|
||||||
data["github_app_name"] = GITHUB_APP_NAME
|
data["github_app_name"] = GITHUB_APP_NAME
|
||||||
data["magic_login"] = (
|
data["magic_login"] = (
|
||||||
bool(EMAIL_HOST_USER) and bool(EMAIL_HOST_PASSWORD)
|
bool(EMAIL_HOST_USER) and bool(EMAIL_HOST_PASSWORD)
|
||||||
@ -112,9 +119,129 @@ class ConfigurationEndpoint(BaseAPIView):
|
|||||||
data["has_openai_configured"] = bool(OPENAI_API_KEY)
|
data["has_openai_configured"] = bool(OPENAI_API_KEY)
|
||||||
|
|
||||||
# File size settings
|
# File size settings
|
||||||
data["file_size_limit"] = float(os.environ.get("FILE_SIZE_LIMIT", 5242880))
|
data["file_size_limit"] = float(
|
||||||
|
os.environ.get("FILE_SIZE_LIMIT", 5242880)
|
||||||
|
)
|
||||||
|
|
||||||
# is self managed
|
# is smtp configured
|
||||||
data["is_self_managed"] = bool(int(os.environ.get("IS_SELF_MANAGED", "1")))
|
data["is_smtp_configured"] = bool(EMAIL_HOST_USER) and bool(
|
||||||
|
EMAIL_HOST_PASSWORD
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
class MobileConfigurationEndpoint(BaseAPIView):
|
||||||
|
permission_classes = [
|
||||||
|
AllowAny,
|
||||||
|
]
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
(
|
||||||
|
GOOGLE_CLIENT_ID,
|
||||||
|
GOOGLE_SERVER_CLIENT_ID,
|
||||||
|
GOOGLE_IOS_CLIENT_ID,
|
||||||
|
EMAIL_HOST_USER,
|
||||||
|
EMAIL_HOST_PASSWORD,
|
||||||
|
ENABLE_MAGIC_LINK_LOGIN,
|
||||||
|
ENABLE_EMAIL_PASSWORD,
|
||||||
|
POSTHOG_API_KEY,
|
||||||
|
POSTHOG_HOST,
|
||||||
|
UNSPLASH_ACCESS_KEY,
|
||||||
|
OPENAI_API_KEY,
|
||||||
|
) = get_configuration_value(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"key": "GOOGLE_CLIENT_ID",
|
||||||
|
"default": os.environ.get("GOOGLE_CLIENT_ID", None),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "GOOGLE_SERVER_CLIENT_ID",
|
||||||
|
"default": os.environ.get("GOOGLE_SERVER_CLIENT_ID", None),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "GOOGLE_IOS_CLIENT_ID",
|
||||||
|
"default": os.environ.get("GOOGLE_IOS_CLIENT_ID", None),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "EMAIL_HOST_USER",
|
||||||
|
"default": os.environ.get("EMAIL_HOST_USER", None),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "EMAIL_HOST_PASSWORD",
|
||||||
|
"default": os.environ.get("EMAIL_HOST_PASSWORD", None),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "ENABLE_MAGIC_LINK_LOGIN",
|
||||||
|
"default": os.environ.get("ENABLE_MAGIC_LINK_LOGIN", "1"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "ENABLE_EMAIL_PASSWORD",
|
||||||
|
"default": os.environ.get("ENABLE_EMAIL_PASSWORD", "1"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "POSTHOG_API_KEY",
|
||||||
|
"default": os.environ.get("POSTHOG_API_KEY", "1"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "POSTHOG_HOST",
|
||||||
|
"default": os.environ.get("POSTHOG_HOST", "1"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "UNSPLASH_ACCESS_KEY",
|
||||||
|
"default": os.environ.get("UNSPLASH_ACCESS_KEY", "1"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "OPENAI_API_KEY",
|
||||||
|
"default": os.environ.get("OPENAI_API_KEY", "1"),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
data = {}
|
||||||
|
# Authentication
|
||||||
|
data["google_client_id"] = (
|
||||||
|
GOOGLE_CLIENT_ID
|
||||||
|
if GOOGLE_CLIENT_ID and GOOGLE_CLIENT_ID != '""'
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
data["google_server_client_id"] = (
|
||||||
|
GOOGLE_SERVER_CLIENT_ID
|
||||||
|
if GOOGLE_SERVER_CLIENT_ID and GOOGLE_SERVER_CLIENT_ID != '""'
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
data["google_ios_client_id"] = (
|
||||||
|
(GOOGLE_IOS_CLIENT_ID)[::-1]
|
||||||
|
if GOOGLE_IOS_CLIENT_ID is not None
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
# Posthog
|
||||||
|
data["posthog_api_key"] = POSTHOG_API_KEY
|
||||||
|
data["posthog_host"] = POSTHOG_HOST
|
||||||
|
|
||||||
|
data["magic_login"] = (
|
||||||
|
bool(EMAIL_HOST_USER) and bool(EMAIL_HOST_PASSWORD)
|
||||||
|
) and ENABLE_MAGIC_LINK_LOGIN == "1"
|
||||||
|
|
||||||
|
data["email_password_login"] = ENABLE_EMAIL_PASSWORD == "1"
|
||||||
|
|
||||||
|
# Posthog
|
||||||
|
data["posthog_api_key"] = POSTHOG_API_KEY
|
||||||
|
data["posthog_host"] = POSTHOG_HOST
|
||||||
|
|
||||||
|
# Unsplash
|
||||||
|
data["has_unsplash_configured"] = bool(UNSPLASH_ACCESS_KEY)
|
||||||
|
|
||||||
|
# Open AI settings
|
||||||
|
data["has_openai_configured"] = bool(OPENAI_API_KEY)
|
||||||
|
|
||||||
|
# File size settings
|
||||||
|
data["file_size_limit"] = float(
|
||||||
|
os.environ.get("FILE_SIZE_LIMIT", 5242880)
|
||||||
|
)
|
||||||
|
|
||||||
|
# is smtp configured
|
||||||
|
data["is_smtp_configured"] = not (
|
||||||
|
bool(EMAIL_HOST_USER) and bool(EMAIL_HOST_PASSWORD)
|
||||||
|
)
|
||||||
|
|
||||||
return Response(data, status=status.HTTP_200_OK)
|
return Response(data, status=status.HTTP_200_OK)
|
||||||
|
@ -14,7 +14,7 @@ from django.db.models import (
|
|||||||
Case,
|
Case,
|
||||||
When,
|
When,
|
||||||
Value,
|
Value,
|
||||||
CharField
|
CharField,
|
||||||
)
|
)
|
||||||
from django.core import serializers
|
from django.core import serializers
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
@ -31,10 +31,15 @@ from plane.app.serializers import (
|
|||||||
CycleSerializer,
|
CycleSerializer,
|
||||||
CycleIssueSerializer,
|
CycleIssueSerializer,
|
||||||
CycleFavoriteSerializer,
|
CycleFavoriteSerializer,
|
||||||
|
IssueSerializer,
|
||||||
IssueStateSerializer,
|
IssueStateSerializer,
|
||||||
CycleWriteSerializer,
|
CycleWriteSerializer,
|
||||||
|
CycleUserPropertiesSerializer,
|
||||||
|
)
|
||||||
|
from plane.app.permissions import (
|
||||||
|
ProjectEntityPermission,
|
||||||
|
ProjectLitePermission,
|
||||||
)
|
)
|
||||||
from plane.app.permissions import ProjectEntityPermission
|
|
||||||
from plane.db.models import (
|
from plane.db.models import (
|
||||||
User,
|
User,
|
||||||
Cycle,
|
Cycle,
|
||||||
@ -44,9 +49,10 @@ from plane.db.models import (
|
|||||||
IssueLink,
|
IssueLink,
|
||||||
IssueAttachment,
|
IssueAttachment,
|
||||||
Label,
|
Label,
|
||||||
|
CycleUserProperties,
|
||||||
|
IssueSubscriber,
|
||||||
)
|
)
|
||||||
from plane.bgtasks.issue_activites_task import issue_activity
|
from plane.bgtasks.issue_activites_task import issue_activity
|
||||||
from plane.utils.grouper import group_results
|
|
||||||
from plane.utils.issue_filters import issue_filters
|
from plane.utils.issue_filters import issue_filters
|
||||||
from plane.utils.analytics_plot import burndown_plot
|
from plane.utils.analytics_plot import burndown_plot
|
||||||
|
|
||||||
@ -61,7 +67,8 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
serializer.save(
|
serializer.save(
|
||||||
project_id=self.kwargs.get("project_id"), owned_by=self.request.user
|
project_id=self.kwargs.get("project_id"),
|
||||||
|
owned_by=self.request.user,
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
@ -140,7 +147,9 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.annotate(total_estimates=Sum("issue_cycle__issue__estimate_point"))
|
.annotate(
|
||||||
|
total_estimates=Sum("issue_cycle__issue__estimate_point")
|
||||||
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
completed_estimates=Sum(
|
completed_estimates=Sum(
|
||||||
"issue_cycle__issue__estimate_point",
|
"issue_cycle__issue__estimate_point",
|
||||||
@ -164,20 +173,17 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
.annotate(
|
.annotate(
|
||||||
status=Case(
|
status=Case(
|
||||||
When(
|
When(
|
||||||
Q(start_date__lte=timezone.now()) & Q(end_date__gte=timezone.now()),
|
Q(start_date__lte=timezone.now())
|
||||||
then=Value("CURRENT")
|
& Q(end_date__gte=timezone.now()),
|
||||||
|
then=Value("CURRENT"),
|
||||||
),
|
),
|
||||||
When(
|
When(
|
||||||
start_date__gt=timezone.now(),
|
start_date__gt=timezone.now(), then=Value("UPCOMING")
|
||||||
then=Value("UPCOMING")
|
|
||||||
),
|
|
||||||
When(
|
|
||||||
end_date__lt=timezone.now(),
|
|
||||||
then=Value("COMPLETED")
|
|
||||||
),
|
),
|
||||||
|
When(end_date__lt=timezone.now(), then=Value("COMPLETED")),
|
||||||
When(
|
When(
|
||||||
Q(start_date__isnull=True) & Q(end_date__isnull=True),
|
Q(start_date__isnull=True) & Q(end_date__isnull=True),
|
||||||
then=Value("DRAFT")
|
then=Value("DRAFT"),
|
||||||
),
|
),
|
||||||
default=Value("DRAFT"),
|
default=Value("DRAFT"),
|
||||||
output_field=CharField(),
|
output_field=CharField(),
|
||||||
@ -186,13 +192,17 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
.prefetch_related(
|
.prefetch_related(
|
||||||
Prefetch(
|
Prefetch(
|
||||||
"issue_cycle__issue__assignees",
|
"issue_cycle__issue__assignees",
|
||||||
queryset=User.objects.only("avatar", "first_name", "id").distinct(),
|
queryset=User.objects.only(
|
||||||
|
"avatar", "first_name", "id"
|
||||||
|
).distinct(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.prefetch_related(
|
.prefetch_related(
|
||||||
Prefetch(
|
Prefetch(
|
||||||
"issue_cycle__issue__labels",
|
"issue_cycle__issue__labels",
|
||||||
queryset=Label.objects.only("name", "color", "id").distinct(),
|
queryset=Label.objects.only(
|
||||||
|
"name", "color", "id"
|
||||||
|
).distinct(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.order_by("-is_favorite", "name")
|
.order_by("-is_favorite", "name")
|
||||||
@ -202,6 +212,11 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
def list(self, request, slug, project_id):
|
def list(self, request, slug, project_id):
|
||||||
queryset = self.get_queryset()
|
queryset = self.get_queryset()
|
||||||
cycle_view = request.GET.get("cycle_view", "all")
|
cycle_view = request.GET.get("cycle_view", "all")
|
||||||
|
fields = [
|
||||||
|
field
|
||||||
|
for field in request.GET.get("fields", "").split(",")
|
||||||
|
if field
|
||||||
|
]
|
||||||
|
|
||||||
queryset = queryset.order_by("-is_favorite", "-created_at")
|
queryset = queryset.order_by("-is_favorite", "-created_at")
|
||||||
|
|
||||||
@ -298,7 +313,9 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
"completion_chart": {},
|
"completion_chart": {},
|
||||||
}
|
}
|
||||||
if data[0]["start_date"] and data[0]["end_date"]:
|
if data[0]["start_date"] and data[0]["end_date"]:
|
||||||
data[0]["distribution"]["completion_chart"] = burndown_plot(
|
data[0]["distribution"][
|
||||||
|
"completion_chart"
|
||||||
|
] = burndown_plot(
|
||||||
queryset=queryset.first(),
|
queryset=queryset.first(),
|
||||||
slug=slug,
|
slug=slug,
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
@ -307,44 +324,8 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
|
|
||||||
return Response(data, status=status.HTTP_200_OK)
|
return Response(data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
# Upcoming Cycles
|
cycles = CycleSerializer(queryset, many=True).data
|
||||||
if cycle_view == "upcoming":
|
return Response(cycles, status=status.HTTP_200_OK)
|
||||||
queryset = queryset.filter(start_date__gt=timezone.now())
|
|
||||||
return Response(
|
|
||||||
CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK
|
|
||||||
)
|
|
||||||
|
|
||||||
# Completed Cycles
|
|
||||||
if cycle_view == "completed":
|
|
||||||
queryset = queryset.filter(end_date__lt=timezone.now())
|
|
||||||
return Response(
|
|
||||||
CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK
|
|
||||||
)
|
|
||||||
|
|
||||||
# Draft Cycles
|
|
||||||
if cycle_view == "draft":
|
|
||||||
queryset = queryset.filter(
|
|
||||||
end_date=None,
|
|
||||||
start_date=None,
|
|
||||||
)
|
|
||||||
|
|
||||||
return Response(
|
|
||||||
CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK
|
|
||||||
)
|
|
||||||
|
|
||||||
# Incomplete Cycles
|
|
||||||
if cycle_view == "incomplete":
|
|
||||||
queryset = queryset.filter(
|
|
||||||
Q(end_date__gte=timezone.now().date()) | Q(end_date__isnull=True),
|
|
||||||
)
|
|
||||||
return Response(
|
|
||||||
CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK
|
|
||||||
)
|
|
||||||
|
|
||||||
# If no matching view is found return all cycles
|
|
||||||
return Response(
|
|
||||||
CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK
|
|
||||||
)
|
|
||||||
|
|
||||||
def create(self, request, slug, project_id):
|
def create(self, request, slug, project_id):
|
||||||
if (
|
if (
|
||||||
@ -360,8 +341,18 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
owned_by=request.user,
|
owned_by=request.user,
|
||||||
)
|
)
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
cycle = (
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
self.get_queryset()
|
||||||
|
.filter(pk=serializer.data["id"])
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
serializer = CycleSerializer(cycle)
|
||||||
|
return Response(
|
||||||
|
serializer.data, status=status.HTTP_201_CREATED
|
||||||
|
)
|
||||||
|
return Response(
|
||||||
|
serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
@ -371,15 +362,22 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def partial_update(self, request, slug, project_id, pk):
|
def partial_update(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
|
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:
|
if "sort_order" in request_data:
|
||||||
# Can only change sort order
|
# Can only change sort order
|
||||||
request_data = {
|
request_data = {
|
||||||
"sort_order": request_data.get("sort_order", cycle.sort_order)
|
"sort_order": request_data.get(
|
||||||
|
"sort_order", cycle.sort_order
|
||||||
|
)
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
return Response(
|
return Response(
|
||||||
@ -389,7 +387,9 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
serializer = CycleWriteSerializer(cycle, data=request.data, partial=True)
|
serializer = CycleWriteSerializer(
|
||||||
|
cycle, data=request.data, partial=True
|
||||||
|
)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
serializer.save()
|
serializer.save()
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
@ -410,7 +410,13 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
.annotate(assignee_id=F("assignees__id"))
|
.annotate(assignee_id=F("assignees__id"))
|
||||||
.annotate(avatar=F("assignees__avatar"))
|
.annotate(avatar=F("assignees__avatar"))
|
||||||
.annotate(display_name=F("assignees__display_name"))
|
.annotate(display_name=F("assignees__display_name"))
|
||||||
.values("first_name", "last_name", "assignee_id", "avatar", "display_name")
|
.values(
|
||||||
|
"first_name",
|
||||||
|
"last_name",
|
||||||
|
"assignee_id",
|
||||||
|
"avatar",
|
||||||
|
"display_name",
|
||||||
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
total_issues=Count(
|
total_issues=Count(
|
||||||
"assignee_id",
|
"assignee_id",
|
||||||
@ -489,7 +495,10 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
|
|
||||||
if queryset.start_date and queryset.end_date:
|
if queryset.start_date and queryset.end_date:
|
||||||
data["distribution"]["completion_chart"] = burndown_plot(
|
data["distribution"]["completion_chart"] = burndown_plot(
|
||||||
queryset=queryset, slug=slug, project_id=project_id, cycle_id=pk
|
queryset=queryset,
|
||||||
|
slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
cycle_id=pk,
|
||||||
)
|
)
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
@ -499,11 +508,13 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
|
|
||||||
def destroy(self, request, slug, project_id, pk):
|
def destroy(self, request, slug, project_id, pk):
|
||||||
cycle_issues = list(
|
cycle_issues = list(
|
||||||
CycleIssue.objects.filter(cycle_id=self.kwargs.get("pk")).values_list(
|
CycleIssue.objects.filter(
|
||||||
"issue", flat=True
|
cycle_id=self.kwargs.get("pk")
|
||||||
)
|
).values_list("issue", flat=True)
|
||||||
|
)
|
||||||
|
cycle = Cycle.objects.get(
|
||||||
|
workspace__slug=slug, project_id=project_id, pk=pk
|
||||||
)
|
)
|
||||||
cycle = Cycle.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
|
|
||||||
|
|
||||||
issue_activity.delay(
|
issue_activity.delay(
|
||||||
type="cycle.activity.deleted",
|
type="cycle.activity.deleted",
|
||||||
@ -519,6 +530,8 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
project_id=str(project_id),
|
project_id=str(project_id),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
epoch=int(timezone.now().timestamp()),
|
epoch=int(timezone.now().timestamp()),
|
||||||
|
notification=True,
|
||||||
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
)
|
)
|
||||||
# Delete the cycle
|
# Delete the cycle
|
||||||
cycle.delete()
|
cycle.delete()
|
||||||
@ -546,7 +559,9 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
|
|||||||
super()
|
super()
|
||||||
.get_queryset()
|
.get_queryset()
|
||||||
.annotate(
|
.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()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
@ -565,28 +580,30 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
|
|||||||
|
|
||||||
@method_decorator(gzip_page)
|
@method_decorator(gzip_page)
|
||||||
def list(self, request, slug, project_id, cycle_id):
|
def list(self, request, slug, project_id, cycle_id):
|
||||||
fields = [field for field in request.GET.get("fields", "").split(",") if field]
|
fields = [
|
||||||
|
field
|
||||||
|
for field in request.GET.get("fields", "").split(",")
|
||||||
|
if field
|
||||||
|
]
|
||||||
order_by = request.GET.get("order_by", "created_at")
|
order_by = request.GET.get("order_by", "created_at")
|
||||||
filters = issue_filters(request.query_params, "GET")
|
filters = issue_filters(request.query_params, "GET")
|
||||||
issues = (
|
issues = (
|
||||||
Issue.issue_objects.filter(issue_cycle__cycle_id=cycle_id)
|
Issue.issue_objects.filter(issue_cycle__cycle_id=cycle_id)
|
||||||
.annotate(
|
.annotate(
|
||||||
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
|
sub_issues_count=Issue.issue_objects.filter(
|
||||||
|
parent=OuterRef("id")
|
||||||
|
)
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
)
|
)
|
||||||
.annotate(bridge_id=F("issue_cycle__id"))
|
|
||||||
.filter(project_id=project_id)
|
.filter(project_id=project_id)
|
||||||
.filter(workspace__slug=slug)
|
.filter(workspace__slug=slug)
|
||||||
.select_related("project")
|
.select_related("workspace", "project", "state", "parent")
|
||||||
.select_related("workspace")
|
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||||
.select_related("state")
|
|
||||||
.select_related("parent")
|
|
||||||
.prefetch_related("assignees")
|
|
||||||
.prefetch_related("labels")
|
|
||||||
.order_by(order_by)
|
.order_by(order_by)
|
||||||
.filter(**filters)
|
.filter(**filters)
|
||||||
|
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||||
.annotate(
|
.annotate(
|
||||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||||
.order_by()
|
.order_by()
|
||||||
@ -594,32 +611,43 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
|
|||||||
.values("count")
|
.values("count")
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id"))
|
attachment_count=IssueAttachment.objects.filter(
|
||||||
|
issue=OuterRef("id")
|
||||||
|
)
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
)
|
)
|
||||||
|
.annotate(
|
||||||
|
is_subscribed=Exists(
|
||||||
|
IssueSubscriber.objects.filter(
|
||||||
|
subscriber=self.request.user, issue_id=OuterRef("id")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
serializer = IssueSerializer(
|
||||||
issues = IssueStateSerializer(
|
|
||||||
issues, many=True, fields=fields if fields else None
|
issues, many=True, fields=fields if fields else None
|
||||||
).data
|
)
|
||||||
issue_dict = {str(issue["id"]): issue for issue in issues}
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
return Response(issue_dict, status=status.HTTP_200_OK)
|
|
||||||
|
|
||||||
def create(self, request, slug, project_id, cycle_id):
|
def create(self, request, slug, project_id, cycle_id):
|
||||||
issues = request.data.get("issues", [])
|
issues = request.data.get("issues", [])
|
||||||
|
|
||||||
if not len(issues):
|
if not len(issues):
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Issues are required"}, status=status.HTTP_400_BAD_REQUEST
|
{"error": "Issues are required"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
cycle = Cycle.objects.get(
|
cycle = Cycle.objects.get(
|
||||||
workspace__slug=slug, project_id=project_id, pk=cycle_id
|
workspace__slug=slug, project_id=project_id, pk=cycle_id
|
||||||
)
|
)
|
||||||
|
|
||||||
if 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(
|
return Response(
|
||||||
{
|
{
|
||||||
"error": "The Cycle has already been completed so no new issues can be added"
|
"error": "The Cycle has already been completed so no new issues can be added"
|
||||||
@ -690,19 +718,27 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
|
|||||||
}
|
}
|
||||||
),
|
),
|
||||||
epoch=int(timezone.now().timestamp()),
|
epoch=int(timezone.now().timestamp()),
|
||||||
|
notification=True,
|
||||||
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Return all Cycle Issues
|
# Return all Cycle Issues
|
||||||
|
issues = self.get_queryset().values_list("issue_id", flat=True)
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
CycleIssueSerializer(self.get_queryset(), many=True).data,
|
IssueSerializer(
|
||||||
|
Issue.objects.filter(pk__in=issues), many=True
|
||||||
|
).data,
|
||||||
status=status.HTTP_200_OK,
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
|
|
||||||
def destroy(self, request, slug, project_id, cycle_id, pk):
|
def destroy(self, request, slug, project_id, cycle_id, issue_id):
|
||||||
cycle_issue = CycleIssue.objects.get(
|
cycle_issue = CycleIssue.objects.get(
|
||||||
pk=pk, workspace__slug=slug, project_id=project_id, cycle_id=cycle_id
|
issue_id=issue_id,
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
cycle_id=cycle_id,
|
||||||
)
|
)
|
||||||
issue_id = cycle_issue.issue_id
|
|
||||||
issue_activity.delay(
|
issue_activity.delay(
|
||||||
type="cycle.activity.deleted",
|
type="cycle.activity.deleted",
|
||||||
requested_data=json.dumps(
|
requested_data=json.dumps(
|
||||||
@ -712,10 +748,12 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
|
|||||||
}
|
}
|
||||||
),
|
),
|
||||||
actor_id=str(self.request.user.id),
|
actor_id=str(self.request.user.id),
|
||||||
issue_id=str(cycle_issue.issue_id),
|
issue_id=str(issue_id),
|
||||||
project_id=str(self.kwargs.get("project_id", None)),
|
project_id=str(self.kwargs.get("project_id", None)),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
epoch=int(timezone.now().timestamp()),
|
epoch=int(timezone.now().timestamp()),
|
||||||
|
notification=True,
|
||||||
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
)
|
)
|
||||||
cycle_issue.delete()
|
cycle_issue.delete()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
@ -834,3 +872,41 @@ class TransferCycleIssueEndpoint(BaseAPIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
return Response({"message": "Success"}, status=status.HTTP_200_OK)
|
return Response({"message": "Success"}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
class CycleUserPropertiesEndpoint(BaseAPIView):
|
||||||
|
permission_classes = [
|
||||||
|
ProjectLitePermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
def patch(self, request, slug, project_id, cycle_id):
|
||||||
|
cycle_properties = CycleUserProperties.objects.get(
|
||||||
|
user=request.user,
|
||||||
|
cycle_id=cycle_id,
|
||||||
|
project_id=project_id,
|
||||||
|
workspace__slug=slug,
|
||||||
|
)
|
||||||
|
|
||||||
|
cycle_properties.filters = request.data.get(
|
||||||
|
"filters", cycle_properties.filters
|
||||||
|
)
|
||||||
|
cycle_properties.display_filters = request.data.get(
|
||||||
|
"display_filters", cycle_properties.display_filters
|
||||||
|
)
|
||||||
|
cycle_properties.display_properties = request.data.get(
|
||||||
|
"display_properties", cycle_properties.display_properties
|
||||||
|
)
|
||||||
|
cycle_properties.save()
|
||||||
|
|
||||||
|
serializer = CycleUserPropertiesSerializer(cycle_properties)
|
||||||
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
def get(self, request, slug, project_id, cycle_id):
|
||||||
|
cycle_properties, _ = CycleUserProperties.objects.get_or_create(
|
||||||
|
user=request.user,
|
||||||
|
project_id=project_id,
|
||||||
|
cycle_id=cycle_id,
|
||||||
|
workspace__slug=slug,
|
||||||
|
)
|
||||||
|
serializer = CycleUserPropertiesSerializer(cycle_properties)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
656
apiserver/plane/app/views/dashboard.py
Normal file
656
apiserver/plane/app/views/dashboard.py
Normal file
@ -0,0 +1,656 @@
|
|||||||
|
# Django imports
|
||||||
|
from django.db.models import (
|
||||||
|
Q,
|
||||||
|
Case,
|
||||||
|
When,
|
||||||
|
Value,
|
||||||
|
CharField,
|
||||||
|
Count,
|
||||||
|
F,
|
||||||
|
Exists,
|
||||||
|
OuterRef,
|
||||||
|
Max,
|
||||||
|
Subquery,
|
||||||
|
JSONField,
|
||||||
|
Func,
|
||||||
|
Prefetch,
|
||||||
|
)
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
# Third Party imports
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import status
|
||||||
|
|
||||||
|
# Module imports
|
||||||
|
from . import BaseAPIView
|
||||||
|
from plane.db.models import (
|
||||||
|
Issue,
|
||||||
|
IssueActivity,
|
||||||
|
ProjectMember,
|
||||||
|
Widget,
|
||||||
|
DashboardWidget,
|
||||||
|
Dashboard,
|
||||||
|
Project,
|
||||||
|
IssueLink,
|
||||||
|
IssueAttachment,
|
||||||
|
IssueRelation,
|
||||||
|
)
|
||||||
|
from plane.app.serializers import (
|
||||||
|
IssueActivitySerializer,
|
||||||
|
IssueSerializer,
|
||||||
|
DashboardSerializer,
|
||||||
|
WidgetSerializer,
|
||||||
|
)
|
||||||
|
from plane.utils.issue_filters import issue_filters
|
||||||
|
|
||||||
|
|
||||||
|
def dashboard_overview_stats(self, request, slug):
|
||||||
|
assigned_issues = Issue.issue_objects.filter(
|
||||||
|
project__project_projectmember__is_active=True,
|
||||||
|
project__project_projectmember__member=request.user,
|
||||||
|
workspace__slug=slug,
|
||||||
|
assignees__in=[request.user],
|
||||||
|
).count()
|
||||||
|
|
||||||
|
pending_issues_count = Issue.issue_objects.filter(
|
||||||
|
~Q(state__group__in=["completed", "cancelled"]),
|
||||||
|
project__project_projectmember__is_active=True,
|
||||||
|
project__project_projectmember__member=request.user,
|
||||||
|
workspace__slug=slug,
|
||||||
|
assignees__in=[request.user],
|
||||||
|
).count()
|
||||||
|
|
||||||
|
created_issues_count = Issue.issue_objects.filter(
|
||||||
|
workspace__slug=slug,
|
||||||
|
project__project_projectmember__is_active=True,
|
||||||
|
project__project_projectmember__member=request.user,
|
||||||
|
created_by_id=request.user.id,
|
||||||
|
).count()
|
||||||
|
|
||||||
|
completed_issues_count = Issue.issue_objects.filter(
|
||||||
|
workspace__slug=slug,
|
||||||
|
project__project_projectmember__is_active=True,
|
||||||
|
project__project_projectmember__member=request.user,
|
||||||
|
assignees__in=[request.user],
|
||||||
|
state__group="completed",
|
||||||
|
).count()
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"assigned_issues_count": assigned_issues,
|
||||||
|
"pending_issues_count": pending_issues_count,
|
||||||
|
"completed_issues_count": completed_issues_count,
|
||||||
|
"created_issues_count": created_issues_count,
|
||||||
|
},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def dashboard_assigned_issues(self, request, slug):
|
||||||
|
filters = issue_filters(request.query_params, "GET")
|
||||||
|
issue_type = request.GET.get("issue_type", None)
|
||||||
|
|
||||||
|
# get all the assigned issues
|
||||||
|
assigned_issues = (
|
||||||
|
Issue.issue_objects.filter(
|
||||||
|
workspace__slug=slug,
|
||||||
|
project__project_projectmember__member=request.user,
|
||||||
|
project__project_projectmember__is_active=True,
|
||||||
|
assignees__in=[request.user],
|
||||||
|
)
|
||||||
|
.filter(**filters)
|
||||||
|
.select_related("workspace", "project", "state", "parent")
|
||||||
|
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||||
|
.prefetch_related(
|
||||||
|
Prefetch(
|
||||||
|
"issue_relation",
|
||||||
|
queryset=IssueRelation.objects.select_related(
|
||||||
|
"related_issue"
|
||||||
|
).select_related("issue"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||||
|
.annotate(
|
||||||
|
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||||
|
.order_by()
|
||||||
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
|
.values("count")
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
attachment_count=IssueAttachment.objects.filter(
|
||||||
|
issue=OuterRef("id")
|
||||||
|
)
|
||||||
|
.order_by()
|
||||||
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
|
.values("count")
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
|
||||||
|
.order_by()
|
||||||
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
|
.values("count")
|
||||||
|
)
|
||||||
|
.order_by("created_at")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Priority Ordering
|
||||||
|
priority_order = ["urgent", "high", "medium", "low", "none"]
|
||||||
|
assigned_issues = assigned_issues.annotate(
|
||||||
|
priority_order=Case(
|
||||||
|
*[
|
||||||
|
When(priority=p, then=Value(i))
|
||||||
|
for i, p in enumerate(priority_order)
|
||||||
|
],
|
||||||
|
output_field=CharField(),
|
||||||
|
)
|
||||||
|
).order_by("priority_order")
|
||||||
|
|
||||||
|
if issue_type == "completed":
|
||||||
|
completed_issues_count = assigned_issues.filter(
|
||||||
|
state__group__in=["completed"]
|
||||||
|
).count()
|
||||||
|
completed_issues = assigned_issues.filter(
|
||||||
|
state__group__in=["completed"]
|
||||||
|
)[:5]
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"issues": IssueSerializer(
|
||||||
|
completed_issues, many=True, expand=self.expand
|
||||||
|
).data,
|
||||||
|
"count": completed_issues_count,
|
||||||
|
},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
|
||||||
|
if issue_type == "overdue":
|
||||||
|
overdue_issues_count = assigned_issues.filter(
|
||||||
|
state__group__in=["backlog", "unstarted", "started"],
|
||||||
|
target_date__lt=timezone.now()
|
||||||
|
).count()
|
||||||
|
overdue_issues = assigned_issues.filter(
|
||||||
|
state__group__in=["backlog", "unstarted", "started"],
|
||||||
|
target_date__lt=timezone.now()
|
||||||
|
)[:5]
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"issues": IssueSerializer(
|
||||||
|
overdue_issues, many=True, expand=self.expand
|
||||||
|
).data,
|
||||||
|
"count": overdue_issues_count,
|
||||||
|
},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
|
||||||
|
if issue_type == "upcoming":
|
||||||
|
upcoming_issues_count = assigned_issues.filter(
|
||||||
|
state__group__in=["backlog", "unstarted", "started"],
|
||||||
|
target_date__gte=timezone.now()
|
||||||
|
).count()
|
||||||
|
upcoming_issues = assigned_issues.filter(
|
||||||
|
state__group__in=["backlog", "unstarted", "started"],
|
||||||
|
target_date__gte=timezone.now()
|
||||||
|
)[:5]
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"issues": IssueSerializer(
|
||||||
|
upcoming_issues, many=True, expand=self.expand
|
||||||
|
).data,
|
||||||
|
"count": upcoming_issues_count,
|
||||||
|
},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
{"error": "Please specify a valid issue type"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def dashboard_created_issues(self, request, slug):
|
||||||
|
filters = issue_filters(request.query_params, "GET")
|
||||||
|
issue_type = request.GET.get("issue_type", None)
|
||||||
|
|
||||||
|
# get all the assigned issues
|
||||||
|
created_issues = (
|
||||||
|
Issue.issue_objects.filter(
|
||||||
|
workspace__slug=slug,
|
||||||
|
project__project_projectmember__member=request.user,
|
||||||
|
project__project_projectmember__is_active=True,
|
||||||
|
created_by=request.user,
|
||||||
|
)
|
||||||
|
.filter(**filters)
|
||||||
|
.select_related("workspace", "project", "state", "parent")
|
||||||
|
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||||
|
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||||
|
.annotate(
|
||||||
|
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||||
|
.order_by()
|
||||||
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
|
.values("count")
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
attachment_count=IssueAttachment.objects.filter(
|
||||||
|
issue=OuterRef("id")
|
||||||
|
)
|
||||||
|
.order_by()
|
||||||
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
|
.values("count")
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
|
||||||
|
.order_by()
|
||||||
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
|
.values("count")
|
||||||
|
)
|
||||||
|
.order_by("created_at")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Priority Ordering
|
||||||
|
priority_order = ["urgent", "high", "medium", "low", "none"]
|
||||||
|
created_issues = created_issues.annotate(
|
||||||
|
priority_order=Case(
|
||||||
|
*[
|
||||||
|
When(priority=p, then=Value(i))
|
||||||
|
for i, p in enumerate(priority_order)
|
||||||
|
],
|
||||||
|
output_field=CharField(),
|
||||||
|
)
|
||||||
|
).order_by("priority_order")
|
||||||
|
|
||||||
|
if issue_type == "completed":
|
||||||
|
completed_issues_count = created_issues.filter(
|
||||||
|
state__group__in=["completed"]
|
||||||
|
).count()
|
||||||
|
completed_issues = created_issues.filter(
|
||||||
|
state__group__in=["completed"]
|
||||||
|
)[:5]
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"issues": IssueSerializer(completed_issues, many=True).data,
|
||||||
|
"count": completed_issues_count,
|
||||||
|
},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
|
||||||
|
if issue_type == "overdue":
|
||||||
|
overdue_issues_count = created_issues.filter(
|
||||||
|
state__group__in=["backlog", "unstarted", "started"],
|
||||||
|
target_date__lt=timezone.now()
|
||||||
|
).count()
|
||||||
|
overdue_issues = created_issues.filter(
|
||||||
|
state__group__in=["backlog", "unstarted", "started"],
|
||||||
|
target_date__lt=timezone.now()
|
||||||
|
)[:5]
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"issues": IssueSerializer(overdue_issues, many=True).data,
|
||||||
|
"count": overdue_issues_count,
|
||||||
|
},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
|
||||||
|
if issue_type == "upcoming":
|
||||||
|
upcoming_issues_count = created_issues.filter(
|
||||||
|
state__group__in=["backlog", "unstarted", "started"],
|
||||||
|
target_date__gte=timezone.now()
|
||||||
|
).count()
|
||||||
|
upcoming_issues = created_issues.filter(
|
||||||
|
state__group__in=["backlog", "unstarted", "started"],
|
||||||
|
target_date__gte=timezone.now()
|
||||||
|
)[:5]
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"issues": IssueSerializer(upcoming_issues, many=True).data,
|
||||||
|
"count": upcoming_issues_count,
|
||||||
|
},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
{"error": "Please specify a valid issue type"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def dashboard_issues_by_state_groups(self, request, slug):
|
||||||
|
filters = issue_filters(request.query_params, "GET")
|
||||||
|
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
|
||||||
|
issues_by_state_groups = (
|
||||||
|
Issue.issue_objects.filter(
|
||||||
|
workspace__slug=slug,
|
||||||
|
project__project_projectmember__is_active=True,
|
||||||
|
project__project_projectmember__member=request.user,
|
||||||
|
assignees__in=[request.user],
|
||||||
|
)
|
||||||
|
.filter(**filters)
|
||||||
|
.values("state__group")
|
||||||
|
.annotate(count=Count("id"))
|
||||||
|
)
|
||||||
|
|
||||||
|
# default state
|
||||||
|
all_groups = {state: 0 for state in state_order}
|
||||||
|
|
||||||
|
# Update counts for existing groups
|
||||||
|
for entry in issues_by_state_groups:
|
||||||
|
all_groups[entry["state__group"]] = entry["count"]
|
||||||
|
|
||||||
|
# Prepare output including all groups with their counts
|
||||||
|
output_data = [
|
||||||
|
{"state": group, "count": count} for group, count in all_groups.items()
|
||||||
|
]
|
||||||
|
|
||||||
|
return Response(output_data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
def dashboard_issues_by_priority(self, request, slug):
|
||||||
|
filters = issue_filters(request.query_params, "GET")
|
||||||
|
priority_order = ["urgent", "high", "medium", "low", "none"]
|
||||||
|
|
||||||
|
issues_by_priority = (
|
||||||
|
Issue.issue_objects.filter(
|
||||||
|
workspace__slug=slug,
|
||||||
|
project__project_projectmember__is_active=True,
|
||||||
|
project__project_projectmember__member=request.user,
|
||||||
|
assignees__in=[request.user],
|
||||||
|
)
|
||||||
|
.filter(**filters)
|
||||||
|
.values("priority")
|
||||||
|
.annotate(count=Count("id"))
|
||||||
|
)
|
||||||
|
|
||||||
|
# default priority
|
||||||
|
all_groups = {priority: 0 for priority in priority_order}
|
||||||
|
|
||||||
|
# Update counts for existing groups
|
||||||
|
for entry in issues_by_priority:
|
||||||
|
all_groups[entry["priority"]] = entry["count"]
|
||||||
|
|
||||||
|
# Prepare output including all groups with their counts
|
||||||
|
output_data = [
|
||||||
|
{"priority": group, "count": count}
|
||||||
|
for group, count in all_groups.items()
|
||||||
|
]
|
||||||
|
|
||||||
|
return Response(output_data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
def dashboard_recent_activity(self, request, slug):
|
||||||
|
queryset = IssueActivity.objects.filter(
|
||||||
|
~Q(field__in=["comment", "vote", "reaction", "draft"]),
|
||||||
|
workspace__slug=slug,
|
||||||
|
project__project_projectmember__member=request.user,
|
||||||
|
project__project_projectmember__is_active=True,
|
||||||
|
actor=request.user,
|
||||||
|
).select_related("actor", "workspace", "issue", "project")[:8]
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
IssueActivitySerializer(queryset, many=True).data,
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def dashboard_recent_projects(self, request, slug):
|
||||||
|
project_ids = (
|
||||||
|
IssueActivity.objects.filter(
|
||||||
|
workspace__slug=slug,
|
||||||
|
project__project_projectmember__member=request.user,
|
||||||
|
project__project_projectmember__is_active=True,
|
||||||
|
actor=request.user,
|
||||||
|
)
|
||||||
|
.values_list("project_id", flat=True)
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
|
||||||
|
# Extract project IDs from the recent projects
|
||||||
|
unique_project_ids = set(project_id for project_id in project_ids)
|
||||||
|
|
||||||
|
# Fetch additional projects only if needed
|
||||||
|
if len(unique_project_ids) < 4:
|
||||||
|
additional_projects = Project.objects.filter(
|
||||||
|
project_projectmember__member=request.user,
|
||||||
|
project_projectmember__is_active=True,
|
||||||
|
workspace__slug=slug,
|
||||||
|
).exclude(id__in=unique_project_ids)
|
||||||
|
|
||||||
|
# Append additional project IDs to the existing list
|
||||||
|
unique_project_ids.update(additional_projects.values_list("id", flat=True))
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
list(unique_project_ids)[:4],
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def dashboard_recent_collaborators(self, request, slug):
|
||||||
|
# Fetch all project IDs where the user belongs to
|
||||||
|
user_projects = Project.objects.filter(
|
||||||
|
project_projectmember__member=request.user,
|
||||||
|
project_projectmember__is_active=True,
|
||||||
|
workspace__slug=slug,
|
||||||
|
).values_list("id", flat=True)
|
||||||
|
|
||||||
|
# Fetch all users who have performed an activity in the projects where the user exists
|
||||||
|
users_with_activities = (
|
||||||
|
IssueActivity.objects.filter(
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id__in=user_projects,
|
||||||
|
)
|
||||||
|
.values("actor")
|
||||||
|
.exclude(actor=request.user)
|
||||||
|
.annotate(num_activities=Count("actor"))
|
||||||
|
.order_by("-num_activities")
|
||||||
|
)[:7]
|
||||||
|
|
||||||
|
# Get the count of active issues for each user in users_with_activities
|
||||||
|
users_with_active_issues = []
|
||||||
|
for user_activity in users_with_activities:
|
||||||
|
user_id = user_activity["actor"]
|
||||||
|
active_issue_count = Issue.objects.filter(
|
||||||
|
assignees__in=[user_id],
|
||||||
|
state__group__in=["unstarted", "started"],
|
||||||
|
).count()
|
||||||
|
users_with_active_issues.append(
|
||||||
|
{"user_id": user_id, "active_issue_count": active_issue_count}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Insert the logged-in user's ID and their active issue count at the beginning
|
||||||
|
active_issue_count = Issue.objects.filter(
|
||||||
|
assignees__in=[request.user],
|
||||||
|
state__group__in=["unstarted", "started"],
|
||||||
|
).count()
|
||||||
|
|
||||||
|
if users_with_activities.count() < 7:
|
||||||
|
# Calculate the additional collaborators needed
|
||||||
|
additional_collaborators_needed = 7 - users_with_activities.count()
|
||||||
|
|
||||||
|
# Fetch additional collaborators from the project_member table
|
||||||
|
additional_collaborators = list(
|
||||||
|
set(
|
||||||
|
ProjectMember.objects.filter(
|
||||||
|
~Q(member=request.user),
|
||||||
|
project_id__in=user_projects,
|
||||||
|
workspace__slug=slug,
|
||||||
|
)
|
||||||
|
.exclude(
|
||||||
|
member__in=[
|
||||||
|
user["actor"] for user in users_with_activities
|
||||||
|
]
|
||||||
|
)
|
||||||
|
.values_list("member", flat=True)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
additional_collaborators = additional_collaborators[
|
||||||
|
:additional_collaborators_needed
|
||||||
|
]
|
||||||
|
|
||||||
|
# Append additional collaborators to the list
|
||||||
|
for collaborator_id in additional_collaborators:
|
||||||
|
active_issue_count = Issue.objects.filter(
|
||||||
|
assignees__in=[collaborator_id],
|
||||||
|
state__group__in=["unstarted", "started"],
|
||||||
|
).count()
|
||||||
|
users_with_active_issues.append(
|
||||||
|
{
|
||||||
|
"user_id": str(collaborator_id),
|
||||||
|
"active_issue_count": active_issue_count,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
users_with_active_issues.insert(
|
||||||
|
0,
|
||||||
|
{"user_id": request.user.id, "active_issue_count": active_issue_count},
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(users_with_active_issues, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
class DashboardEndpoint(BaseAPIView):
|
||||||
|
def create(self, request, slug):
|
||||||
|
serializer = DashboardSerializer(data=request.data)
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save()
|
||||||
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
def patch(self, request, slug, pk):
|
||||||
|
serializer = DashboardSerializer(data=request.data, partial=True)
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save()
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
def delete(self, request, slug, pk):
|
||||||
|
serializer = DashboardSerializer(data=request.data, partial=True)
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save()
|
||||||
|
return Response(serializer.data, status=status.HTTP_204_NO_CONTENT)
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
def get(self, request, slug, dashboard_id=None):
|
||||||
|
if not dashboard_id:
|
||||||
|
dashboard_type = request.GET.get("dashboard_type", None)
|
||||||
|
if dashboard_type == "home":
|
||||||
|
dashboard, created = Dashboard.objects.get_or_create(
|
||||||
|
type_identifier=dashboard_type, owned_by=request.user, is_default=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if created:
|
||||||
|
widgets_to_fetch = [
|
||||||
|
"overview_stats",
|
||||||
|
"assigned_issues",
|
||||||
|
"created_issues",
|
||||||
|
"issues_by_state_groups",
|
||||||
|
"issues_by_priority",
|
||||||
|
"recent_activity",
|
||||||
|
"recent_projects",
|
||||||
|
"recent_collaborators",
|
||||||
|
]
|
||||||
|
|
||||||
|
updated_dashboard_widgets = []
|
||||||
|
for widget_key in widgets_to_fetch:
|
||||||
|
widget = Widget.objects.filter(key=widget_key).values_list("id", flat=True)
|
||||||
|
if widget:
|
||||||
|
updated_dashboard_widgets.append(
|
||||||
|
DashboardWidget(
|
||||||
|
widget_id=widget,
|
||||||
|
dashboard_id=dashboard.id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
DashboardWidget.objects.bulk_create(
|
||||||
|
updated_dashboard_widgets, batch_size=100
|
||||||
|
)
|
||||||
|
|
||||||
|
widgets = (
|
||||||
|
Widget.objects.annotate(
|
||||||
|
is_visible=Exists(
|
||||||
|
DashboardWidget.objects.filter(
|
||||||
|
widget_id=OuterRef("pk"),
|
||||||
|
dashboard_id=dashboard.id,
|
||||||
|
is_visible=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
dashboard_filters=Subquery(
|
||||||
|
DashboardWidget.objects.filter(
|
||||||
|
widget_id=OuterRef("pk"),
|
||||||
|
dashboard_id=dashboard.id,
|
||||||
|
filters__isnull=False,
|
||||||
|
)
|
||||||
|
.exclude(filters={})
|
||||||
|
.values("filters")[:1]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
widget_filters=Case(
|
||||||
|
When(
|
||||||
|
dashboard_filters__isnull=False,
|
||||||
|
then=F("dashboard_filters"),
|
||||||
|
),
|
||||||
|
default=F("filters"),
|
||||||
|
output_field=JSONField(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"dashboard": DashboardSerializer(dashboard).data,
|
||||||
|
"widgets": WidgetSerializer(widgets, many=True).data,
|
||||||
|
},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
return Response(
|
||||||
|
{"error": "Please specify a valid dashboard type"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
widget_key = request.GET.get("widget_key", "overview_stats")
|
||||||
|
|
||||||
|
WIDGETS_MAPPER = {
|
||||||
|
"overview_stats": dashboard_overview_stats,
|
||||||
|
"assigned_issues": dashboard_assigned_issues,
|
||||||
|
"created_issues": dashboard_created_issues,
|
||||||
|
"issues_by_state_groups": dashboard_issues_by_state_groups,
|
||||||
|
"issues_by_priority": dashboard_issues_by_priority,
|
||||||
|
"recent_activity": dashboard_recent_activity,
|
||||||
|
"recent_projects": dashboard_recent_projects,
|
||||||
|
"recent_collaborators": dashboard_recent_collaborators,
|
||||||
|
}
|
||||||
|
|
||||||
|
func = WIDGETS_MAPPER.get(widget_key)
|
||||||
|
if func is not None:
|
||||||
|
response = func(
|
||||||
|
self,
|
||||||
|
request=request,
|
||||||
|
slug=slug,
|
||||||
|
)
|
||||||
|
if isinstance(response, Response):
|
||||||
|
return response
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
{"error": "Please specify a valid widget key"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class WidgetsEndpoint(BaseAPIView):
|
||||||
|
def patch(self, request, dashboard_id, widget_id):
|
||||||
|
dashboard_widget = DashboardWidget.objects.filter(
|
||||||
|
widget_id=widget_id,
|
||||||
|
dashboard_id=dashboard_id,
|
||||||
|
).first()
|
||||||
|
dashboard_widget.is_visible = request.data.get(
|
||||||
|
"is_visible", dashboard_widget.is_visible
|
||||||
|
)
|
||||||
|
dashboard_widget.sort_order = request.data.get(
|
||||||
|
"sort_order", dashboard_widget.sort_order
|
||||||
|
)
|
||||||
|
dashboard_widget.filters = request.data.get(
|
||||||
|
"filters", dashboard_widget.filters
|
||||||
|
)
|
||||||
|
dashboard_widget.save()
|
||||||
|
return Response(
|
||||||
|
{"message": "successfully updated"}, status=status.HTTP_200_OK
|
||||||
|
)
|
@ -19,16 +19,16 @@ class ProjectEstimatePointEndpoint(BaseAPIView):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def get(self, request, slug, project_id):
|
def get(self, request, slug, project_id):
|
||||||
project = Project.objects.get(workspace__slug=slug, pk=project_id)
|
project = Project.objects.get(workspace__slug=slug, pk=project_id)
|
||||||
if project.estimate_id is not None:
|
if project.estimate_id is not None:
|
||||||
estimate_points = EstimatePoint.objects.filter(
|
estimate_points = EstimatePoint.objects.filter(
|
||||||
estimate_id=project.estimate_id,
|
estimate_id=project.estimate_id,
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
)
|
)
|
||||||
serializer = EstimatePointSerializer(estimate_points, many=True)
|
serializer = EstimatePointSerializer(estimate_points, many=True)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
return Response([], status=status.HTTP_200_OK)
|
return Response([], status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
class BulkEstimatePointEndpoint(BaseViewSet):
|
class BulkEstimatePointEndpoint(BaseViewSet):
|
||||||
@ -39,9 +39,13 @@ class BulkEstimatePointEndpoint(BaseViewSet):
|
|||||||
serializer_class = EstimateSerializer
|
serializer_class = EstimateSerializer
|
||||||
|
|
||||||
def list(self, request, slug, project_id):
|
def list(self, request, slug, project_id):
|
||||||
estimates = Estimate.objects.filter(
|
estimates = (
|
||||||
workspace__slug=slug, project_id=project_id
|
Estimate.objects.filter(
|
||||||
).prefetch_related("points").select_related("workspace", "project")
|
workspace__slug=slug, project_id=project_id
|
||||||
|
)
|
||||||
|
.prefetch_related("points")
|
||||||
|
.select_related("workspace", "project")
|
||||||
|
)
|
||||||
serializer = EstimateReadSerializer(estimates, many=True)
|
serializer = EstimateReadSerializer(estimates, many=True)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
@ -54,13 +58,17 @@ class BulkEstimatePointEndpoint(BaseViewSet):
|
|||||||
|
|
||||||
estimate_points = request.data.get("estimate_points", [])
|
estimate_points = request.data.get("estimate_points", [])
|
||||||
|
|
||||||
serializer = EstimatePointSerializer(data=request.data.get("estimate_points"), many=True)
|
serializer = EstimatePointSerializer(
|
||||||
|
data=request.data.get("estimate_points"), many=True
|
||||||
|
)
|
||||||
if not serializer.is_valid():
|
if not serializer.is_valid():
|
||||||
return Response(
|
return Response(
|
||||||
serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
||||||
)
|
)
|
||||||
|
|
||||||
estimate_serializer = EstimateSerializer(data=request.data.get("estimate"))
|
estimate_serializer = EstimateSerializer(
|
||||||
|
data=request.data.get("estimate")
|
||||||
|
)
|
||||||
if not estimate_serializer.is_valid():
|
if not estimate_serializer.is_valid():
|
||||||
return Response(
|
return Response(
|
||||||
estimate_serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
estimate_serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
||||||
@ -135,7 +143,8 @@ class BulkEstimatePointEndpoint(BaseViewSet):
|
|||||||
|
|
||||||
estimate_points = EstimatePoint.objects.filter(
|
estimate_points = EstimatePoint.objects.filter(
|
||||||
pk__in=[
|
pk__in=[
|
||||||
estimate_point.get("id") for estimate_point in estimate_points_data
|
estimate_point.get("id")
|
||||||
|
for estimate_point in estimate_points_data
|
||||||
],
|
],
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
@ -157,10 +166,14 @@ class BulkEstimatePointEndpoint(BaseViewSet):
|
|||||||
updated_estimate_points.append(estimate_point)
|
updated_estimate_points.append(estimate_point)
|
||||||
|
|
||||||
EstimatePoint.objects.bulk_update(
|
EstimatePoint.objects.bulk_update(
|
||||||
updated_estimate_points, ["value"], batch_size=10,
|
updated_estimate_points,
|
||||||
|
["value"],
|
||||||
|
batch_size=10,
|
||||||
)
|
)
|
||||||
|
|
||||||
estimate_point_serializer = EstimatePointSerializer(estimate_points, many=True)
|
estimate_point_serializer = EstimatePointSerializer(
|
||||||
|
estimate_points, many=True
|
||||||
|
)
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
"estimate": estimate_serializer.data,
|
"estimate": estimate_serializer.data,
|
||||||
|
@ -63,9 +63,11 @@ class ExportIssuesEndpoint(BaseAPIView):
|
|||||||
def get(self, request, slug):
|
def get(self, request, slug):
|
||||||
exporter_history = ExporterHistory.objects.filter(
|
exporter_history = ExporterHistory.objects.filter(
|
||||||
workspace__slug=slug
|
workspace__slug=slug
|
||||||
).select_related("workspace","initiated_by")
|
).select_related("workspace", "initiated_by")
|
||||||
|
|
||||||
if request.GET.get("per_page", False) and request.GET.get("cursor", False):
|
if request.GET.get("per_page", False) and request.GET.get(
|
||||||
|
"cursor", False
|
||||||
|
):
|
||||||
return self.paginate(
|
return self.paginate(
|
||||||
request=request,
|
request=request,
|
||||||
queryset=exporter_history,
|
queryset=exporter_history,
|
||||||
|
@ -14,7 +14,10 @@ from django.conf import settings
|
|||||||
from .base import BaseAPIView
|
from .base import BaseAPIView
|
||||||
from plane.app.permissions import ProjectEntityPermission
|
from plane.app.permissions import ProjectEntityPermission
|
||||||
from plane.db.models import Workspace, Project
|
from plane.db.models import Workspace, Project
|
||||||
from plane.app.serializers import ProjectLiteSerializer, WorkspaceLiteSerializer
|
from plane.app.serializers import (
|
||||||
|
ProjectLiteSerializer,
|
||||||
|
WorkspaceLiteSerializer,
|
||||||
|
)
|
||||||
from plane.utils.integrations.github import get_release_notes
|
from plane.utils.integrations.github import get_release_notes
|
||||||
from plane.license.utils.instance_value import get_configuration_value
|
from plane.license.utils.instance_value import get_configuration_value
|
||||||
|
|
||||||
@ -51,7 +54,8 @@ class GPTIntegrationEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
if not task:
|
if not task:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Task is required"}, status=status.HTTP_400_BAD_REQUEST
|
{"error": "Task is required"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
final_text = task + "\n" + prompt
|
final_text = task + "\n" + prompt
|
||||||
@ -89,7 +93,7 @@ class ReleaseNotesEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
class UnsplashEndpoint(BaseAPIView):
|
class UnsplashEndpoint(BaseAPIView):
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
UNSPLASH_ACCESS_KEY, = get_configuration_value(
|
(UNSPLASH_ACCESS_KEY,) = get_configuration_value(
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"key": "UNSPLASH_ACCESS_KEY",
|
"key": "UNSPLASH_ACCESS_KEY",
|
||||||
|
@ -35,14 +35,16 @@ from plane.app.serializers import (
|
|||||||
ModuleSerializer,
|
ModuleSerializer,
|
||||||
)
|
)
|
||||||
from plane.utils.integrations.github import get_github_repo_details
|
from plane.utils.integrations.github import get_github_repo_details
|
||||||
from plane.utils.importers.jira import jira_project_issue_summary
|
from plane.utils.importers.jira import (
|
||||||
|
jira_project_issue_summary,
|
||||||
|
is_allowed_hostname,
|
||||||
|
)
|
||||||
from plane.bgtasks.importer_task import service_importer
|
from plane.bgtasks.importer_task import service_importer
|
||||||
from plane.utils.html_processor import strip_tags
|
from plane.utils.html_processor import strip_tags
|
||||||
from plane.app.permissions import WorkSpaceAdminPermission
|
from plane.app.permissions import WorkSpaceAdminPermission
|
||||||
|
|
||||||
|
|
||||||
class ServiceIssueImportSummaryEndpoint(BaseAPIView):
|
class ServiceIssueImportSummaryEndpoint(BaseAPIView):
|
||||||
|
|
||||||
def get(self, request, slug, service):
|
def get(self, request, slug, service):
|
||||||
if service == "github":
|
if service == "github":
|
||||||
owner = request.GET.get("owner", False)
|
owner = request.GET.get("owner", False)
|
||||||
@ -94,7 +96,8 @@ class ServiceIssueImportSummaryEndpoint(BaseAPIView):
|
|||||||
for key, error_message in params.items():
|
for key, error_message in params.items():
|
||||||
if not request.GET.get(key, False):
|
if not request.GET.get(key, False):
|
||||||
return Response(
|
return Response(
|
||||||
{"error": error_message}, status=status.HTTP_400_BAD_REQUEST
|
{"error": error_message},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
project_key = request.GET.get("project_key", "")
|
project_key = request.GET.get("project_key", "")
|
||||||
@ -122,6 +125,7 @@ class ImportServiceEndpoint(BaseAPIView):
|
|||||||
permission_classes = [
|
permission_classes = [
|
||||||
WorkSpaceAdminPermission,
|
WorkSpaceAdminPermission,
|
||||||
]
|
]
|
||||||
|
|
||||||
def post(self, request, slug, service):
|
def post(self, request, slug, service):
|
||||||
project_id = request.data.get("project_id", False)
|
project_id = request.data.get("project_id", False)
|
||||||
|
|
||||||
@ -174,6 +178,21 @@ class ImportServiceEndpoint(BaseAPIView):
|
|||||||
data = request.data.get("data", False)
|
data = request.data.get("data", False)
|
||||||
metadata = request.data.get("metadata", False)
|
metadata = request.data.get("metadata", False)
|
||||||
config = request.data.get("config", False)
|
config = request.data.get("config", False)
|
||||||
|
|
||||||
|
cloud_hostname = metadata.get("cloud_hostname", False)
|
||||||
|
|
||||||
|
if not cloud_hostname:
|
||||||
|
return Response(
|
||||||
|
{"error": "Cloud hostname is required"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not is_allowed_hostname(cloud_hostname):
|
||||||
|
return Response(
|
||||||
|
{"error": "Hostname is not a valid hostname."},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
if not data or not metadata:
|
if not data or not metadata:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Data, config and metadata are required"},
|
{"error": "Data, config and metadata are required"},
|
||||||
@ -244,7 +263,9 @@ class ImportServiceEndpoint(BaseAPIView):
|
|||||||
importer = Importer.objects.get(
|
importer = Importer.objects.get(
|
||||||
pk=pk, service=service, workspace__slug=slug
|
pk=pk, service=service, workspace__slug=slug
|
||||||
)
|
)
|
||||||
serializer = ImporterSerializer(importer, data=request.data, partial=True)
|
serializer = ImporterSerializer(
|
||||||
|
importer, data=request.data, partial=True
|
||||||
|
)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
serializer.save()
|
serializer.save()
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
@ -280,9 +301,9 @@ class BulkImportIssuesEndpoint(BaseAPIView):
|
|||||||
).first()
|
).first()
|
||||||
|
|
||||||
# Get the maximum sequence_id
|
# Get the maximum sequence_id
|
||||||
last_id = IssueSequence.objects.filter(project_id=project_id).aggregate(
|
last_id = IssueSequence.objects.filter(
|
||||||
largest=Max("sequence")
|
project_id=project_id
|
||||||
)["largest"]
|
).aggregate(largest=Max("sequence"))["largest"]
|
||||||
|
|
||||||
last_id = 1 if last_id is None else last_id + 1
|
last_id = 1 if last_id is None else last_id + 1
|
||||||
|
|
||||||
@ -315,7 +336,9 @@ class BulkImportIssuesEndpoint(BaseAPIView):
|
|||||||
if issue_data.get("state", False)
|
if issue_data.get("state", False)
|
||||||
else default_state.id,
|
else default_state.id,
|
||||||
name=issue_data.get("name", "Issue Created through Bulk"),
|
name=issue_data.get("name", "Issue Created through Bulk"),
|
||||||
description_html=issue_data.get("description_html", "<p></p>"),
|
description_html=issue_data.get(
|
||||||
|
"description_html", "<p></p>"
|
||||||
|
),
|
||||||
description_stripped=(
|
description_stripped=(
|
||||||
None
|
None
|
||||||
if (
|
if (
|
||||||
@ -427,15 +450,21 @@ class BulkImportIssuesEndpoint(BaseAPIView):
|
|||||||
for comment in comments_list
|
for comment in comments_list
|
||||||
]
|
]
|
||||||
|
|
||||||
_ = IssueComment.objects.bulk_create(bulk_issue_comments, batch_size=100)
|
_ = IssueComment.objects.bulk_create(
|
||||||
|
bulk_issue_comments, batch_size=100
|
||||||
|
)
|
||||||
|
|
||||||
# Attach Links
|
# Attach Links
|
||||||
_ = IssueLink.objects.bulk_create(
|
_ = IssueLink.objects.bulk_create(
|
||||||
[
|
[
|
||||||
IssueLink(
|
IssueLink(
|
||||||
issue=issue,
|
issue=issue,
|
||||||
url=issue_data.get("link", {}).get("url", "https://github.com"),
|
url=issue_data.get("link", {}).get(
|
||||||
title=issue_data.get("link", {}).get("title", "Original Issue"),
|
"url", "https://github.com"
|
||||||
|
),
|
||||||
|
title=issue_data.get("link", {}).get(
|
||||||
|
"title", "Original Issue"
|
||||||
|
),
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
workspace_id=project.workspace_id,
|
workspace_id=project.workspace_id,
|
||||||
created_by=request.user,
|
created_by=request.user,
|
||||||
@ -472,7 +501,9 @@ class BulkImportModulesEndpoint(BaseAPIView):
|
|||||||
ignore_conflicts=True,
|
ignore_conflicts=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
modules = Module.objects.filter(id__in=[module.id for module in modules])
|
modules = Module.objects.filter(
|
||||||
|
id__in=[module.id for module in modules]
|
||||||
|
)
|
||||||
|
|
||||||
if len(modules) == len(modules_data):
|
if len(modules) == len(modules_data):
|
||||||
_ = ModuleLink.objects.bulk_create(
|
_ = ModuleLink.objects.bulk_create(
|
||||||
@ -520,6 +551,8 @@ class BulkImportModulesEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
return Response(
|
return Response(
|
||||||
{"message": "Modules created but issues could not be imported"},
|
{
|
||||||
|
"message": "Modules created but issues could not be imported"
|
||||||
|
},
|
||||||
status=status.HTTP_200_OK,
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
|
@ -62,7 +62,9 @@ class InboxViewSet(BaseViewSet):
|
|||||||
serializer.save(project_id=self.kwargs.get("project_id"))
|
serializer.save(project_id=self.kwargs.get("project_id"))
|
||||||
|
|
||||||
def destroy(self, request, slug, project_id, pk):
|
def destroy(self, request, slug, project_id, pk):
|
||||||
inbox = Inbox.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
|
inbox = Inbox.objects.get(
|
||||||
|
workspace__slug=slug, project_id=project_id, pk=pk
|
||||||
|
)
|
||||||
# Handle default inbox delete
|
# Handle default inbox delete
|
||||||
if inbox.is_default:
|
if inbox.is_default:
|
||||||
return Response(
|
return Response(
|
||||||
@ -86,49 +88,14 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.filter_queryset(
|
return (
|
||||||
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=self.kwargs.get("inbox_id"),
|
|
||||||
)
|
|
||||||
.select_related("issue", "workspace", "project")
|
|
||||||
)
|
|
||||||
|
|
||||||
def list(self, request, slug, project_id, inbox_id):
|
|
||||||
filters = issue_filters(request.query_params, "GET")
|
|
||||||
issues = (
|
|
||||||
Issue.objects.filter(
|
Issue.objects.filter(
|
||||||
issue_inbox__inbox_id=inbox_id,
|
project_id=self.kwargs.get("project_id"),
|
||||||
workspace__slug=slug,
|
workspace__slug=self.kwargs.get("slug"),
|
||||||
project_id=project_id,
|
issue_inbox__inbox_id=self.kwargs.get("inbox_id")
|
||||||
)
|
)
|
||||||
.filter(**filters)
|
|
||||||
.annotate(bridge_id=F("issue_inbox__id"))
|
|
||||||
.select_related("workspace", "project", "state", "parent")
|
.select_related("workspace", "project", "state", "parent")
|
||||||
.prefetch_related("assignees", "labels")
|
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||||
.order_by("issue_inbox__snoozed_till", "issue_inbox__status")
|
|
||||||
.annotate(
|
|
||||||
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
|
|
||||||
.order_by()
|
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
|
||||||
.values("count")
|
|
||||||
)
|
|
||||||
.annotate(
|
|
||||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
|
||||||
.order_by()
|
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
|
||||||
.values("count")
|
|
||||||
)
|
|
||||||
.annotate(
|
|
||||||
attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id"))
|
|
||||||
.order_by()
|
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
|
||||||
.values("count")
|
|
||||||
)
|
|
||||||
.prefetch_related(
|
.prefetch_related(
|
||||||
Prefetch(
|
Prefetch(
|
||||||
"issue_inbox",
|
"issue_inbox",
|
||||||
@ -137,8 +104,35 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||||
issues_data = IssueStateInboxSerializer(issues, many=True).data
|
.annotate(
|
||||||
|
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||||
|
.order_by()
|
||||||
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
|
.values("count")
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
attachment_count=IssueAttachment.objects.filter(
|
||||||
|
issue=OuterRef("id")
|
||||||
|
)
|
||||||
|
.order_by()
|
||||||
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
|
.values("count")
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
sub_issues_count=Issue.issue_objects.filter(
|
||||||
|
parent=OuterRef("id")
|
||||||
|
)
|
||||||
|
.order_by()
|
||||||
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
|
.values("count")
|
||||||
|
)
|
||||||
|
).distinct()
|
||||||
|
|
||||||
|
def list(self, request, slug, project_id, inbox_id):
|
||||||
|
filters = issue_filters(request.query_params, "GET")
|
||||||
|
issue_queryset = self.get_queryset().filter(**filters).order_by("issue_inbox__snoozed_till", "issue_inbox__status")
|
||||||
|
issues_data = IssueSerializer(issue_queryset, expand=self.expand, many=True).data
|
||||||
return Response(
|
return Response(
|
||||||
issues_data,
|
issues_data,
|
||||||
status=status.HTTP_200_OK,
|
status=status.HTTP_200_OK,
|
||||||
@ -147,7 +141,8 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
def create(self, request, slug, project_id, inbox_id):
|
def create(self, request, slug, project_id, inbox_id):
|
||||||
if not request.data.get("issue", {}).get("name", False):
|
if not request.data.get("issue", {}).get("name", False):
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST
|
{"error": "Name is required"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check for valid priority
|
# Check for valid priority
|
||||||
@ -159,7 +154,8 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
"none",
|
"none",
|
||||||
]:
|
]:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST
|
{"error": "Invalid priority"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create or get state
|
# Create or get state
|
||||||
@ -192,6 +188,8 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
project_id=str(project_id),
|
project_id=str(project_id),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
epoch=int(timezone.now().timestamp()),
|
epoch=int(timezone.now().timestamp()),
|
||||||
|
notification=True,
|
||||||
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
)
|
)
|
||||||
# create an inbox issue
|
# create an inbox issue
|
||||||
InboxIssue.objects.create(
|
InboxIssue.objects.create(
|
||||||
@ -201,12 +199,16 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
source=request.data.get("source", "in-app"),
|
source=request.data.get("source", "in-app"),
|
||||||
)
|
)
|
||||||
|
|
||||||
serializer = IssueStateInboxSerializer(issue)
|
issue = (self.get_queryset().filter(pk=issue.id).first())
|
||||||
|
serializer = IssueSerializer(issue ,expand=self.expand)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def partial_update(self, request, slug, project_id, inbox_id, pk):
|
def partial_update(self, request, slug, project_id, inbox_id, issue_id):
|
||||||
inbox_issue = InboxIssue.objects.get(
|
inbox_issue = InboxIssue.objects.get(
|
||||||
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
|
issue_id=issue_id,
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
inbox_id=inbox_id,
|
||||||
)
|
)
|
||||||
# Get the project member
|
# Get the project member
|
||||||
project_member = ProjectMember.objects.get(
|
project_member = ProjectMember.objects.get(
|
||||||
@ -229,7 +231,9 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
|
|
||||||
if bool(issue_data):
|
if bool(issue_data):
|
||||||
issue = Issue.objects.get(
|
issue = Issue.objects.get(
|
||||||
pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id
|
pk=inbox_issue.issue_id,
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
)
|
)
|
||||||
# Only allow guests and viewers to edit name and description
|
# Only allow guests and viewers to edit name and description
|
||||||
if project_member.role <= 10:
|
if project_member.role <= 10:
|
||||||
@ -239,7 +243,9 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
"description_html": issue_data.get(
|
"description_html": issue_data.get(
|
||||||
"description_html", issue.description_html
|
"description_html", issue.description_html
|
||||||
),
|
),
|
||||||
"description": issue_data.get("description", issue.description),
|
"description": issue_data.get(
|
||||||
|
"description", issue.description
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
issue_serializer = IssueCreateSerializer(
|
issue_serializer = IssueCreateSerializer(
|
||||||
@ -262,6 +268,8 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
cls=DjangoJSONEncoder,
|
cls=DjangoJSONEncoder,
|
||||||
),
|
),
|
||||||
epoch=int(timezone.now().timestamp()),
|
epoch=int(timezone.now().timestamp()),
|
||||||
|
notification=True,
|
||||||
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
)
|
)
|
||||||
issue_serializer.save()
|
issue_serializer.save()
|
||||||
else:
|
else:
|
||||||
@ -285,7 +293,9 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
)
|
)
|
||||||
state = State.objects.filter(
|
state = State.objects.filter(
|
||||||
group="cancelled", workspace__slug=slug, project_id=project_id
|
group="cancelled",
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
).first()
|
).first()
|
||||||
if state is not None:
|
if state is not None:
|
||||||
issue.state = state
|
issue.state = state
|
||||||
@ -303,32 +313,35 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
if issue.state.name == "Triage":
|
if issue.state.name == "Triage":
|
||||||
# Move to default state
|
# Move to default state
|
||||||
state = State.objects.filter(
|
state = State.objects.filter(
|
||||||
workspace__slug=slug, project_id=project_id, default=True
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
default=True,
|
||||||
).first()
|
).first()
|
||||||
if state is not None:
|
if state is not None:
|
||||||
issue.state = state
|
issue.state = state
|
||||||
issue.save()
|
issue.save()
|
||||||
|
issue = (self.get_queryset().filter(pk=issue_id).first())
|
||||||
|
serializer = IssueSerializer(issue, expand=self.expand)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
else:
|
|
||||||
return Response(
|
return Response(
|
||||||
InboxIssueSerializer(inbox_issue).data, status=status.HTTP_200_OK
|
serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
issue = (self.get_queryset().filter(pk=issue_id).first())
|
||||||
|
serializer = IssueSerializer(issue ,expand=self.expand)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def retrieve(self, request, slug, project_id, inbox_id, pk):
|
def retrieve(self, request, slug, project_id, inbox_id, issue_id):
|
||||||
inbox_issue = InboxIssue.objects.get(
|
issue = self.get_queryset().filter(pk=issue_id).first()
|
||||||
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
|
serializer = IssueSerializer(issue, expand=self.expand,)
|
||||||
)
|
|
||||||
issue = Issue.objects.get(
|
|
||||||
pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id
|
|
||||||
)
|
|
||||||
serializer = IssueStateInboxSerializer(issue)
|
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def destroy(self, request, slug, project_id, inbox_id, pk):
|
def destroy(self, request, slug, project_id, inbox_id, issue_id):
|
||||||
inbox_issue = InboxIssue.objects.get(
|
inbox_issue = InboxIssue.objects.get(
|
||||||
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
|
issue_id=issue_id,
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
inbox_id=inbox_id,
|
||||||
)
|
)
|
||||||
# Get the project member
|
# Get the project member
|
||||||
project_member = ProjectMember.objects.get(
|
project_member = ProjectMember.objects.get(
|
||||||
@ -350,9 +363,8 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
if inbox_issue.status in [-2, -1, 0, 2]:
|
if inbox_issue.status in [-2, -1, 0, 2]:
|
||||||
# Delete the issue also
|
# Delete the issue also
|
||||||
Issue.objects.filter(
|
Issue.objects.filter(
|
||||||
workspace__slug=slug, project_id=project_id, pk=inbox_issue.issue_id
|
workspace__slug=slug, project_id=project_id, pk=issue_id
|
||||||
).delete()
|
).delete()
|
||||||
|
|
||||||
inbox_issue.delete()
|
inbox_issue.delete()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
# Python improts
|
# Python improts
|
||||||
import uuid
|
import uuid
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
# Django imports
|
# Django imports
|
||||||
from django.contrib.auth.hashers import make_password
|
from django.contrib.auth.hashers import make_password
|
||||||
|
|
||||||
@ -19,7 +20,10 @@ from plane.db.models import (
|
|||||||
WorkspaceMember,
|
WorkspaceMember,
|
||||||
APIToken,
|
APIToken,
|
||||||
)
|
)
|
||||||
from plane.app.serializers import IntegrationSerializer, WorkspaceIntegrationSerializer
|
from plane.app.serializers import (
|
||||||
|
IntegrationSerializer,
|
||||||
|
WorkspaceIntegrationSerializer,
|
||||||
|
)
|
||||||
from plane.utils.integrations.github import (
|
from plane.utils.integrations.github import (
|
||||||
get_github_metadata,
|
get_github_metadata,
|
||||||
delete_github_installation,
|
delete_github_installation,
|
||||||
@ -27,6 +31,7 @@ from plane.utils.integrations.github import (
|
|||||||
from plane.app.permissions import WorkSpaceAdminPermission
|
from plane.app.permissions import WorkSpaceAdminPermission
|
||||||
from plane.utils.integrations.slack import slack_oauth
|
from plane.utils.integrations.slack import slack_oauth
|
||||||
|
|
||||||
|
|
||||||
class IntegrationViewSet(BaseViewSet):
|
class IntegrationViewSet(BaseViewSet):
|
||||||
serializer_class = IntegrationSerializer
|
serializer_class = IntegrationSerializer
|
||||||
model = Integration
|
model = Integration
|
||||||
@ -101,7 +106,10 @@ class WorkspaceIntegrationViewSet(BaseViewSet):
|
|||||||
code = request.data.get("code", False)
|
code = request.data.get("code", False)
|
||||||
|
|
||||||
if not code:
|
if not code:
|
||||||
return Response({"error": "Code is required"}, status=status.HTTP_400_BAD_REQUEST)
|
return Response(
|
||||||
|
{"error": "Code is required"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
slack_response = slack_oauth(code=code)
|
slack_response = slack_oauth(code=code)
|
||||||
|
|
||||||
@ -110,7 +118,9 @@ class WorkspaceIntegrationViewSet(BaseViewSet):
|
|||||||
team_id = metadata.get("team", {}).get("id", False)
|
team_id = metadata.get("team", {}).get("id", False)
|
||||||
if not metadata or not access_token or not team_id:
|
if not metadata or not access_token or not team_id:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Slack could not be installed. Please try again later"},
|
{
|
||||||
|
"error": "Slack could not be installed. Please try again later"
|
||||||
|
},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
config = {"team_id": team_id, "access_token": access_token}
|
config = {"team_id": team_id, "access_token": access_token}
|
||||||
|
@ -21,7 +21,10 @@ from plane.app.serializers import (
|
|||||||
GithubCommentSyncSerializer,
|
GithubCommentSyncSerializer,
|
||||||
)
|
)
|
||||||
from plane.utils.integrations.github import get_github_repos
|
from plane.utils.integrations.github import get_github_repos
|
||||||
from plane.app.permissions import ProjectBasePermission, ProjectEntityPermission
|
from plane.app.permissions import (
|
||||||
|
ProjectBasePermission,
|
||||||
|
ProjectEntityPermission,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class GithubRepositoriesEndpoint(BaseAPIView):
|
class GithubRepositoriesEndpoint(BaseAPIView):
|
||||||
@ -185,7 +188,6 @@ class BulkCreateGithubIssueSyncEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
|
|
||||||
class GithubCommentSyncViewSet(BaseViewSet):
|
class GithubCommentSyncViewSet(BaseViewSet):
|
||||||
|
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
ProjectEntityPermission,
|
ProjectEntityPermission,
|
||||||
]
|
]
|
||||||
|
@ -8,9 +8,16 @@ from sentry_sdk import capture_exception
|
|||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from plane.app.views import BaseViewSet, BaseAPIView
|
from plane.app.views import BaseViewSet, BaseAPIView
|
||||||
from plane.db.models import SlackProjectSync, WorkspaceIntegration, ProjectMember
|
from plane.db.models import (
|
||||||
|
SlackProjectSync,
|
||||||
|
WorkspaceIntegration,
|
||||||
|
ProjectMember,
|
||||||
|
)
|
||||||
from plane.app.serializers import SlackProjectSyncSerializer
|
from plane.app.serializers import SlackProjectSyncSerializer
|
||||||
from plane.app.permissions import ProjectBasePermission, ProjectEntityPermission
|
from plane.app.permissions import (
|
||||||
|
ProjectBasePermission,
|
||||||
|
ProjectEntityPermission,
|
||||||
|
)
|
||||||
from plane.utils.integrations.slack import slack_oauth
|
from plane.utils.integrations.slack import slack_oauth
|
||||||
|
|
||||||
|
|
||||||
@ -38,7 +45,8 @@ class SlackProjectSyncViewSet(BaseViewSet):
|
|||||||
|
|
||||||
if not code:
|
if not code:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Code is required"}, status=status.HTTP_400_BAD_REQUEST
|
{"error": "Code is required"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
slack_response = slack_oauth(code=code)
|
slack_response = slack_oauth(code=code)
|
||||||
@ -54,7 +62,9 @@ class SlackProjectSyncViewSet(BaseViewSet):
|
|||||||
access_token=slack_response.get("access_token"),
|
access_token=slack_response.get("access_token"),
|
||||||
scopes=slack_response.get("scope"),
|
scopes=slack_response.get("scope"),
|
||||||
bot_user_id=slack_response.get("bot_user_id"),
|
bot_user_id=slack_response.get("bot_user_id"),
|
||||||
webhook_url=slack_response.get("incoming_webhook", {}).get("url"),
|
webhook_url=slack_response.get("incoming_webhook", {}).get(
|
||||||
|
"url"
|
||||||
|
),
|
||||||
data=slack_response,
|
data=slack_response,
|
||||||
team_id=slack_response.get("team", {}).get("id"),
|
team_id=slack_response.get("team", {}).get("id"),
|
||||||
team_name=slack_response.get("team", {}).get("name"),
|
team_name=slack_response.get("team", {}).get("name"),
|
||||||
@ -62,7 +72,9 @@ class SlackProjectSyncViewSet(BaseViewSet):
|
|||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
)
|
)
|
||||||
_ = ProjectMember.objects.get_or_create(
|
_ = ProjectMember.objects.get_or_create(
|
||||||
member=workspace_integration.actor, role=20, project_id=project_id
|
member=workspace_integration.actor,
|
||||||
|
role=20,
|
||||||
|
project_id=project_id,
|
||||||
)
|
)
|
||||||
serializer = SlackProjectSyncSerializer(slack_project_sync)
|
serializer = SlackProjectSyncSerializer(slack_project_sync)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
@ -74,6 +86,8 @@ class SlackProjectSyncViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
capture_exception(e)
|
capture_exception(e)
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Slack could not be installed. Please try again later"},
|
{
|
||||||
|
"error": "Slack could not be installed. Please try again later"
|
||||||
|
},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -7,6 +7,8 @@ from django.db.models import Prefetch, F, OuterRef, Func, Exists, Count, Q
|
|||||||
from django.core import serializers
|
from django.core import serializers
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.views.decorators.gzip import gzip_page
|
from django.views.decorators.gzip import gzip_page
|
||||||
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
|
|
||||||
|
|
||||||
# Third party imports
|
# Third party imports
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
@ -20,9 +22,13 @@ from plane.app.serializers import (
|
|||||||
ModuleIssueSerializer,
|
ModuleIssueSerializer,
|
||||||
ModuleLinkSerializer,
|
ModuleLinkSerializer,
|
||||||
ModuleFavoriteSerializer,
|
ModuleFavoriteSerializer,
|
||||||
IssueStateSerializer,
|
IssueSerializer,
|
||||||
|
ModuleUserPropertiesSerializer,
|
||||||
|
)
|
||||||
|
from plane.app.permissions import (
|
||||||
|
ProjectEntityPermission,
|
||||||
|
ProjectLitePermission,
|
||||||
)
|
)
|
||||||
from plane.app.permissions import ProjectEntityPermission
|
|
||||||
from plane.db.models import (
|
from plane.db.models import (
|
||||||
Module,
|
Module,
|
||||||
ModuleIssue,
|
ModuleIssue,
|
||||||
@ -32,6 +38,8 @@ from plane.db.models import (
|
|||||||
ModuleFavorite,
|
ModuleFavorite,
|
||||||
IssueLink,
|
IssueLink,
|
||||||
IssueAttachment,
|
IssueAttachment,
|
||||||
|
IssueSubscriber,
|
||||||
|
ModuleUserProperties,
|
||||||
)
|
)
|
||||||
from plane.bgtasks.issue_activites_task import issue_activity
|
from plane.bgtasks.issue_activites_task import issue_activity
|
||||||
from plane.utils.grouper import group_results
|
from plane.utils.grouper import group_results
|
||||||
@ -54,7 +62,6 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
|
|
||||||
subquery = ModuleFavorite.objects.filter(
|
subquery = ModuleFavorite.objects.filter(
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
module_id=OuterRef("pk"),
|
module_id=OuterRef("pk"),
|
||||||
@ -74,7 +81,9 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
.prefetch_related(
|
.prefetch_related(
|
||||||
Prefetch(
|
Prefetch(
|
||||||
"link_module",
|
"link_module",
|
||||||
queryset=ModuleLink.objects.select_related("module", "created_by"),
|
queryset=ModuleLink.objects.select_related(
|
||||||
|
"module", "created_by"
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
@ -136,7 +145,7 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.order_by("-is_favorite","-created_at")
|
.order_by("-is_favorite", "-created_at")
|
||||||
)
|
)
|
||||||
|
|
||||||
def create(self, request, slug, project_id):
|
def create(self, request, slug, project_id):
|
||||||
@ -153,6 +162,18 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
def list(self, request, slug, project_id):
|
||||||
|
queryset = self.get_queryset()
|
||||||
|
fields = [
|
||||||
|
field
|
||||||
|
for field in request.GET.get("fields", "").split(",")
|
||||||
|
if field
|
||||||
|
]
|
||||||
|
modules = ModuleSerializer(
|
||||||
|
queryset, many=True, fields=fields if fields else None
|
||||||
|
).data
|
||||||
|
return Response(modules, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def retrieve(self, request, slug, project_id, pk):
|
def retrieve(self, request, slug, project_id, pk):
|
||||||
queryset = self.get_queryset().get(pk=pk)
|
queryset = self.get_queryset().get(pk=pk)
|
||||||
|
|
||||||
@ -167,7 +188,13 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
.annotate(assignee_id=F("assignees__id"))
|
.annotate(assignee_id=F("assignees__id"))
|
||||||
.annotate(display_name=F("assignees__display_name"))
|
.annotate(display_name=F("assignees__display_name"))
|
||||||
.annotate(avatar=F("assignees__avatar"))
|
.annotate(avatar=F("assignees__avatar"))
|
||||||
.values("first_name", "last_name", "assignee_id", "avatar", "display_name")
|
.values(
|
||||||
|
"first_name",
|
||||||
|
"last_name",
|
||||||
|
"assignee_id",
|
||||||
|
"avatar",
|
||||||
|
"display_name",
|
||||||
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
total_issues=Count(
|
total_issues=Count(
|
||||||
"assignee_id",
|
"assignee_id",
|
||||||
@ -251,7 +278,10 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
|
|
||||||
if queryset.start_date and queryset.target_date:
|
if queryset.start_date and queryset.target_date:
|
||||||
data["distribution"]["completion_chart"] = burndown_plot(
|
data["distribution"]["completion_chart"] = burndown_plot(
|
||||||
queryset=queryset, slug=slug, project_id=project_id, module_id=pk
|
queryset=queryset,
|
||||||
|
slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
module_id=pk,
|
||||||
)
|
)
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
@ -260,25 +290,28 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def destroy(self, request, slug, project_id, pk):
|
def destroy(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(
|
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",
|
|
||||||
requested_data=json.dumps(
|
|
||||||
{
|
|
||||||
"module_id": str(pk),
|
|
||||||
"module_name": str(module.name),
|
|
||||||
"issues": [str(issue_id) for issue_id in module_issues],
|
|
||||||
}
|
|
||||||
),
|
|
||||||
actor_id=str(request.user.id),
|
|
||||||
issue_id=str(pk),
|
|
||||||
project_id=str(project_id),
|
|
||||||
current_instance=None,
|
|
||||||
epoch=int(timezone.now().timestamp()),
|
|
||||||
)
|
)
|
||||||
|
_ = [
|
||||||
|
issue_activity.delay(
|
||||||
|
type="module.activity.deleted",
|
||||||
|
requested_data=json.dumps({"module_id": str(pk)}),
|
||||||
|
actor_id=str(request.user.id),
|
||||||
|
issue_id=str(issue),
|
||||||
|
project_id=project_id,
|
||||||
|
current_instance=json.dumps({"module_name": str(module.name)}),
|
||||||
|
epoch=int(timezone.now().timestamp()),
|
||||||
|
notification=True,
|
||||||
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
)
|
||||||
|
for issue in module_issues
|
||||||
|
]
|
||||||
module.delete()
|
module.delete()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
@ -289,7 +322,6 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
|
|||||||
webhook_event = "module_issue"
|
webhook_event = "module_issue"
|
||||||
bulk = True
|
bulk = True
|
||||||
|
|
||||||
|
|
||||||
filterset_fields = [
|
filterset_fields = [
|
||||||
"issue__labels__id",
|
"issue__labels__id",
|
||||||
"issue__assignees__id",
|
"issue__assignees__id",
|
||||||
@ -299,53 +331,18 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
|
|||||||
ProjectEntityPermission,
|
ProjectEntityPermission,
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
return self.filter_queryset(
|
|
||||||
super()
|
|
||||||
.get_queryset()
|
|
||||||
.annotate(
|
|
||||||
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("issue"))
|
|
||||||
.order_by()
|
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
|
||||||
.values("count")
|
|
||||||
)
|
|
||||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
|
||||||
.filter(project_id=self.kwargs.get("project_id"))
|
|
||||||
.filter(module_id=self.kwargs.get("module_id"))
|
|
||||||
.filter(project__project_projectmember__member=self.request.user)
|
|
||||||
.select_related("project")
|
|
||||||
.select_related("workspace")
|
|
||||||
.select_related("module")
|
|
||||||
.select_related("issue", "issue__state", "issue__project")
|
|
||||||
.prefetch_related("issue__assignees", "issue__labels")
|
|
||||||
.prefetch_related("module__members")
|
|
||||||
.distinct()
|
|
||||||
)
|
|
||||||
|
|
||||||
@method_decorator(gzip_page)
|
def get_queryset(self):
|
||||||
def list(self, request, slug, project_id, module_id):
|
return (
|
||||||
fields = [field for field in request.GET.get("fields", "").split(",") if field]
|
Issue.objects.filter(
|
||||||
order_by = request.GET.get("order_by", "created_at")
|
project_id=self.kwargs.get("project_id"),
|
||||||
filters = issue_filters(request.query_params, "GET")
|
workspace__slug=self.kwargs.get("slug"),
|
||||||
issues = (
|
issue_module__module_id=self.kwargs.get("module_id")
|
||||||
Issue.issue_objects.filter(issue_module__module_id=module_id)
|
|
||||||
.annotate(
|
|
||||||
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
|
|
||||||
.order_by()
|
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
|
||||||
.values("count")
|
|
||||||
)
|
)
|
||||||
.annotate(bridge_id=F("issue_module__id"))
|
.select_related("workspace", "project", "state", "parent")
|
||||||
.filter(project_id=project_id)
|
.prefetch_related("labels", "assignees")
|
||||||
.filter(workspace__slug=slug)
|
.prefetch_related('issue_module__module')
|
||||||
.select_related("project")
|
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||||
.select_related("workspace")
|
|
||||||
.select_related("state")
|
|
||||||
.select_related("parent")
|
|
||||||
.prefetch_related("assignees")
|
|
||||||
.prefetch_related("labels")
|
|
||||||
.order_by(order_by)
|
|
||||||
.filter(**filters)
|
|
||||||
.annotate(
|
.annotate(
|
||||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||||
.order_by()
|
.order_by()
|
||||||
@ -353,114 +350,144 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
|
|||||||
.values("count")
|
.values("count")
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id"))
|
attachment_count=IssueAttachment.objects.filter(
|
||||||
|
issue=OuterRef("id")
|
||||||
|
)
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
)
|
)
|
||||||
)
|
.annotate(
|
||||||
issues = IssueStateSerializer(issues, many=True, fields=fields if fields else None).data
|
sub_issues_count=Issue.issue_objects.filter(
|
||||||
issue_dict = {str(issue["id"]): issue for issue in issues}
|
parent=OuterRef("id")
|
||||||
return Response(issue_dict, status=status.HTTP_200_OK)
|
)
|
||||||
|
.order_by()
|
||||||
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
|
.values("count")
|
||||||
|
)
|
||||||
|
).distinct()
|
||||||
|
|
||||||
def create(self, request, slug, project_id, module_id):
|
@method_decorator(gzip_page)
|
||||||
|
def list(self, request, slug, project_id, module_id):
|
||||||
|
fields = [
|
||||||
|
field
|
||||||
|
for field in request.GET.get("fields", "").split(",")
|
||||||
|
if field
|
||||||
|
]
|
||||||
|
filters = issue_filters(request.query_params, "GET")
|
||||||
|
issue_queryset = self.get_queryset().filter(**filters)
|
||||||
|
serializer = IssueSerializer(
|
||||||
|
issue_queryset, many=True, fields=fields if fields else None
|
||||||
|
)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
# create multiple issues inside a module
|
||||||
|
def create_module_issues(self, request, slug, project_id, module_id):
|
||||||
issues = request.data.get("issues", [])
|
issues = request.data.get("issues", [])
|
||||||
if not len(issues):
|
if not len(issues):
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Issues are required"}, status=status.HTTP_400_BAD_REQUEST
|
{"error": "Issues are required"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
module = Module.objects.get(
|
project = Project.objects.get(pk=project_id)
|
||||||
workspace__slug=slug, project_id=project_id, pk=module_id
|
_ = ModuleIssue.objects.bulk_create(
|
||||||
)
|
[
|
||||||
|
ModuleIssue(
|
||||||
module_issues = list(ModuleIssue.objects.filter(issue_id__in=issues))
|
issue_id=str(issue),
|
||||||
|
module_id=module_id,
|
||||||
update_module_issue_activity = []
|
project_id=project_id,
|
||||||
records_to_update = []
|
workspace_id=project.workspace_id,
|
||||||
record_to_create = []
|
created_by=request.user,
|
||||||
|
updated_by=request.user,
|
||||||
for issue in issues:
|
|
||||||
module_issue = [
|
|
||||||
module_issue
|
|
||||||
for module_issue in module_issues
|
|
||||||
if str(module_issue.issue_id) in issues
|
|
||||||
]
|
|
||||||
|
|
||||||
if len(module_issue):
|
|
||||||
if module_issue[0].module_id != module_id:
|
|
||||||
update_module_issue_activity.append(
|
|
||||||
{
|
|
||||||
"old_module_id": str(module_issue[0].module_id),
|
|
||||||
"new_module_id": str(module_id),
|
|
||||||
"issue_id": str(module_issue[0].issue_id),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
module_issue[0].module_id = module_id
|
|
||||||
records_to_update.append(module_issue[0])
|
|
||||||
else:
|
|
||||||
record_to_create.append(
|
|
||||||
ModuleIssue(
|
|
||||||
module=module,
|
|
||||||
issue_id=issue,
|
|
||||||
project_id=project_id,
|
|
||||||
workspace=module.workspace,
|
|
||||||
created_by=request.user,
|
|
||||||
updated_by=request.user,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
for issue in issues
|
||||||
ModuleIssue.objects.bulk_create(
|
],
|
||||||
record_to_create,
|
|
||||||
batch_size=10,
|
batch_size=10,
|
||||||
ignore_conflicts=True,
|
ignore_conflicts=True,
|
||||||
)
|
)
|
||||||
|
# Bulk Update the activity
|
||||||
|
_ = [
|
||||||
|
issue_activity.delay(
|
||||||
|
type="module.activity.created",
|
||||||
|
requested_data=json.dumps({"module_id": str(module_id)}),
|
||||||
|
actor_id=str(request.user.id),
|
||||||
|
issue_id=str(issue),
|
||||||
|
project_id=project_id,
|
||||||
|
current_instance=None,
|
||||||
|
epoch=int(timezone.now().timestamp()),
|
||||||
|
notification=True,
|
||||||
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
)
|
||||||
|
for issue in issues
|
||||||
|
]
|
||||||
|
issues = (self.get_queryset().filter(pk__in=issues))
|
||||||
|
serializer = IssueSerializer(issues , many=True)
|
||||||
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
ModuleIssue.objects.bulk_update(
|
|
||||||
records_to_update,
|
# create multiple module inside an issue
|
||||||
["module"],
|
def create_issue_modules(self, request, slug, project_id, issue_id):
|
||||||
|
modules = request.data.get("modules", [])
|
||||||
|
if not len(modules):
|
||||||
|
return Response(
|
||||||
|
{"error": "Modules are required"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
project = Project.objects.get(pk=project_id)
|
||||||
|
_ = ModuleIssue.objects.bulk_create(
|
||||||
|
[
|
||||||
|
ModuleIssue(
|
||||||
|
issue_id=issue_id,
|
||||||
|
module_id=module,
|
||||||
|
project_id=project_id,
|
||||||
|
workspace_id=project.workspace_id,
|
||||||
|
created_by=request.user,
|
||||||
|
updated_by=request.user,
|
||||||
|
)
|
||||||
|
for module in modules
|
||||||
|
],
|
||||||
batch_size=10,
|
batch_size=10,
|
||||||
|
ignore_conflicts=True,
|
||||||
)
|
)
|
||||||
|
# Bulk Update the activity
|
||||||
|
_ = [
|
||||||
|
issue_activity.delay(
|
||||||
|
type="module.activity.created",
|
||||||
|
requested_data=json.dumps({"module_id": module}),
|
||||||
|
actor_id=str(request.user.id),
|
||||||
|
issue_id=issue_id,
|
||||||
|
project_id=project_id,
|
||||||
|
current_instance=None,
|
||||||
|
epoch=int(timezone.now().timestamp()),
|
||||||
|
notification=True,
|
||||||
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
)
|
||||||
|
for module in modules
|
||||||
|
]
|
||||||
|
|
||||||
# Capture Issue Activity
|
issue = (self.get_queryset().filter(pk=issue_id).first())
|
||||||
issue_activity.delay(
|
serializer = IssueSerializer(issue)
|
||||||
type="module.activity.created",
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
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)),
|
|
||||||
current_instance=json.dumps(
|
|
||||||
{
|
|
||||||
"updated_module_issues": update_module_issue_activity,
|
|
||||||
"created_module_issues": serializers.serialize(
|
|
||||||
"json", record_to_create
|
|
||||||
),
|
|
||||||
}
|
|
||||||
),
|
|
||||||
epoch=int(timezone.now().timestamp()),
|
|
||||||
)
|
|
||||||
|
|
||||||
return Response(
|
|
||||||
ModuleIssueSerializer(self.get_queryset(), many=True).data,
|
|
||||||
status=status.HTTP_200_OK,
|
|
||||||
)
|
|
||||||
|
|
||||||
def destroy(self, request, slug, project_id, module_id, pk):
|
def destroy(self, request, slug, project_id, module_id, issue_id):
|
||||||
module_issue = ModuleIssue.objects.get(
|
module_issue = ModuleIssue.objects.get(
|
||||||
workspace__slug=slug, project_id=project_id, module_id=module_id, pk=pk
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
module_id=module_id,
|
||||||
|
issue_id=issue_id,
|
||||||
)
|
)
|
||||||
issue_activity.delay(
|
issue_activity.delay(
|
||||||
type="module.activity.deleted",
|
type="module.activity.deleted",
|
||||||
requested_data=json.dumps(
|
requested_data=json.dumps({"module_id": str(module_id)}),
|
||||||
{
|
|
||||||
"module_id": str(module_id),
|
|
||||||
"issues": [str(module_issue.issue_id)],
|
|
||||||
}
|
|
||||||
),
|
|
||||||
actor_id=str(request.user.id),
|
actor_id=str(request.user.id),
|
||||||
issue_id=str(module_issue.issue_id),
|
issue_id=str(issue_id),
|
||||||
project_id=str(project_id),
|
project_id=str(project_id),
|
||||||
current_instance=None,
|
current_instance=json.dumps({"module_name": module_issue.module.name}),
|
||||||
epoch=int(timezone.now().timestamp()),
|
epoch=int(timezone.now().timestamp()),
|
||||||
|
notification=True,
|
||||||
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
)
|
)
|
||||||
module_issue.delete()
|
module_issue.delete()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
@ -522,3 +549,41 @@ class ModuleFavoriteViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
module_favorite.delete()
|
module_favorite.delete()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleUserPropertiesEndpoint(BaseAPIView):
|
||||||
|
permission_classes = [
|
||||||
|
ProjectLitePermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
def patch(self, request, slug, project_id, module_id):
|
||||||
|
module_properties = ModuleUserProperties.objects.get(
|
||||||
|
user=request.user,
|
||||||
|
module_id=module_id,
|
||||||
|
project_id=project_id,
|
||||||
|
workspace__slug=slug,
|
||||||
|
)
|
||||||
|
|
||||||
|
module_properties.filters = request.data.get(
|
||||||
|
"filters", module_properties.filters
|
||||||
|
)
|
||||||
|
module_properties.display_filters = request.data.get(
|
||||||
|
"display_filters", module_properties.display_filters
|
||||||
|
)
|
||||||
|
module_properties.display_properties = request.data.get(
|
||||||
|
"display_properties", module_properties.display_properties
|
||||||
|
)
|
||||||
|
module_properties.save()
|
||||||
|
|
||||||
|
serializer = ModuleUserPropertiesSerializer(module_properties)
|
||||||
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
def get(self, request, slug, project_id, module_id):
|
||||||
|
module_properties, _ = ModuleUserProperties.objects.get_or_create(
|
||||||
|
user=request.user,
|
||||||
|
project_id=project_id,
|
||||||
|
module_id=module_id,
|
||||||
|
workspace__slug=slug,
|
||||||
|
)
|
||||||
|
serializer = ModuleUserPropertiesSerializer(module_properties)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# Django imports
|
# Django imports
|
||||||
from django.db.models import Q
|
from django.db.models import Q, OuterRef, Exists
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
# Third party imports
|
# Third party imports
|
||||||
@ -15,8 +15,9 @@ from plane.db.models import (
|
|||||||
IssueSubscriber,
|
IssueSubscriber,
|
||||||
Issue,
|
Issue,
|
||||||
WorkspaceMember,
|
WorkspaceMember,
|
||||||
|
UserNotificationPreference,
|
||||||
)
|
)
|
||||||
from plane.app.serializers import NotificationSerializer
|
from plane.app.serializers import NotificationSerializer, UserNotificationPreferenceSerializer
|
||||||
|
|
||||||
|
|
||||||
class NotificationViewSet(BaseViewSet, BasePaginator):
|
class NotificationViewSet(BaseViewSet, BasePaginator):
|
||||||
@ -51,8 +52,10 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
|
|||||||
|
|
||||||
# Filters based on query parameters
|
# Filters based on query parameters
|
||||||
snoozed_filters = {
|
snoozed_filters = {
|
||||||
"true": Q(snoozed_till__lt=timezone.now()) | Q(snoozed_till__isnull=False),
|
"true": Q(snoozed_till__lt=timezone.now())
|
||||||
"false": Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True),
|
| Q(snoozed_till__isnull=False),
|
||||||
|
"false": Q(snoozed_till__gte=timezone.now())
|
||||||
|
| Q(snoozed_till__isnull=True),
|
||||||
}
|
}
|
||||||
|
|
||||||
notifications = notifications.filter(snoozed_filters[snoozed])
|
notifications = notifications.filter(snoozed_filters[snoozed])
|
||||||
@ -69,17 +72,39 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
|
|||||||
|
|
||||||
# Subscribed issues
|
# Subscribed issues
|
||||||
if type == "watching":
|
if type == "watching":
|
||||||
issue_ids = IssueSubscriber.objects.filter(
|
issue_ids = (
|
||||||
workspace__slug=slug, subscriber_id=request.user.id
|
IssueSubscriber.objects.filter(
|
||||||
).values_list("issue_id", flat=True)
|
workspace__slug=slug, subscriber_id=request.user.id
|
||||||
notifications = notifications.filter(entity_identifier__in=issue_ids)
|
)
|
||||||
|
.annotate(
|
||||||
|
created=Exists(
|
||||||
|
Issue.objects.filter(
|
||||||
|
created_by=request.user, pk=OuterRef("issue_id")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
assigned=Exists(
|
||||||
|
IssueAssignee.objects.filter(
|
||||||
|
pk=OuterRef("issue_id"), assignee=request.user
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.filter(created=False, assigned=False)
|
||||||
|
.values_list("issue_id", flat=True)
|
||||||
|
)
|
||||||
|
notifications = notifications.filter(
|
||||||
|
entity_identifier__in=issue_ids,
|
||||||
|
)
|
||||||
|
|
||||||
# Assigned Issues
|
# Assigned Issues
|
||||||
if type == "assigned":
|
if type == "assigned":
|
||||||
issue_ids = IssueAssignee.objects.filter(
|
issue_ids = IssueAssignee.objects.filter(
|
||||||
workspace__slug=slug, assignee_id=request.user.id
|
workspace__slug=slug, assignee_id=request.user.id
|
||||||
).values_list("issue_id", flat=True)
|
).values_list("issue_id", flat=True)
|
||||||
notifications = notifications.filter(entity_identifier__in=issue_ids)
|
notifications = notifications.filter(
|
||||||
|
entity_identifier__in=issue_ids
|
||||||
|
)
|
||||||
|
|
||||||
# Created issues
|
# Created issues
|
||||||
if type == "created":
|
if type == "created":
|
||||||
@ -94,10 +119,14 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
|
|||||||
issue_ids = Issue.objects.filter(
|
issue_ids = Issue.objects.filter(
|
||||||
workspace__slug=slug, created_by=request.user
|
workspace__slug=slug, created_by=request.user
|
||||||
).values_list("pk", flat=True)
|
).values_list("pk", flat=True)
|
||||||
notifications = notifications.filter(entity_identifier__in=issue_ids)
|
notifications = notifications.filter(
|
||||||
|
entity_identifier__in=issue_ids
|
||||||
|
)
|
||||||
|
|
||||||
# Pagination
|
# Pagination
|
||||||
if request.GET.get("per_page", False) and request.GET.get("cursor", False):
|
if request.GET.get("per_page", False) and request.GET.get(
|
||||||
|
"cursor", False
|
||||||
|
):
|
||||||
return self.paginate(
|
return self.paginate(
|
||||||
request=request,
|
request=request,
|
||||||
queryset=(notifications),
|
queryset=(notifications),
|
||||||
@ -227,11 +256,13 @@ class MarkAllReadNotificationViewSet(BaseViewSet):
|
|||||||
# Filter for snoozed notifications
|
# Filter for snoozed notifications
|
||||||
if snoozed:
|
if snoozed:
|
||||||
notifications = notifications.filter(
|
notifications = notifications.filter(
|
||||||
Q(snoozed_till__lt=timezone.now()) | Q(snoozed_till__isnull=False)
|
Q(snoozed_till__lt=timezone.now())
|
||||||
|
| Q(snoozed_till__isnull=False)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
notifications = notifications.filter(
|
notifications = notifications.filter(
|
||||||
Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True),
|
Q(snoozed_till__gte=timezone.now())
|
||||||
|
| Q(snoozed_till__isnull=True),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Filter for archived or unarchive
|
# Filter for archived or unarchive
|
||||||
@ -245,14 +276,18 @@ class MarkAllReadNotificationViewSet(BaseViewSet):
|
|||||||
issue_ids = IssueSubscriber.objects.filter(
|
issue_ids = IssueSubscriber.objects.filter(
|
||||||
workspace__slug=slug, subscriber_id=request.user.id
|
workspace__slug=slug, subscriber_id=request.user.id
|
||||||
).values_list("issue_id", flat=True)
|
).values_list("issue_id", flat=True)
|
||||||
notifications = notifications.filter(entity_identifier__in=issue_ids)
|
notifications = notifications.filter(
|
||||||
|
entity_identifier__in=issue_ids
|
||||||
|
)
|
||||||
|
|
||||||
# Assigned Issues
|
# Assigned Issues
|
||||||
if type == "assigned":
|
if type == "assigned":
|
||||||
issue_ids = IssueAssignee.objects.filter(
|
issue_ids = IssueAssignee.objects.filter(
|
||||||
workspace__slug=slug, assignee_id=request.user.id
|
workspace__slug=slug, assignee_id=request.user.id
|
||||||
).values_list("issue_id", flat=True)
|
).values_list("issue_id", flat=True)
|
||||||
notifications = notifications.filter(entity_identifier__in=issue_ids)
|
notifications = notifications.filter(
|
||||||
|
entity_identifier__in=issue_ids
|
||||||
|
)
|
||||||
|
|
||||||
# Created issues
|
# Created issues
|
||||||
if type == "created":
|
if type == "created":
|
||||||
@ -267,7 +302,9 @@ class MarkAllReadNotificationViewSet(BaseViewSet):
|
|||||||
issue_ids = Issue.objects.filter(
|
issue_ids = Issue.objects.filter(
|
||||||
workspace__slug=slug, created_by=request.user
|
workspace__slug=slug, created_by=request.user
|
||||||
).values_list("pk", flat=True)
|
).values_list("pk", flat=True)
|
||||||
notifications = notifications.filter(entity_identifier__in=issue_ids)
|
notifications = notifications.filter(
|
||||||
|
entity_identifier__in=issue_ids
|
||||||
|
)
|
||||||
|
|
||||||
updated_notifications = []
|
updated_notifications = []
|
||||||
for notification in notifications:
|
for notification in notifications:
|
||||||
@ -277,3 +314,31 @@ class MarkAllReadNotificationViewSet(BaseViewSet):
|
|||||||
updated_notifications, ["read_at"], batch_size=100
|
updated_notifications, ["read_at"], batch_size=100
|
||||||
)
|
)
|
||||||
return Response({"message": "Successful"}, status=status.HTTP_200_OK)
|
return Response({"message": "Successful"}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
class UserNotificationPreferenceEndpoint(BaseAPIView):
|
||||||
|
model = UserNotificationPreference
|
||||||
|
serializer_class = UserNotificationPreferenceSerializer
|
||||||
|
|
||||||
|
# request the object
|
||||||
|
def get(self, request):
|
||||||
|
user_notification_preference = UserNotificationPreference.objects.get(
|
||||||
|
user=request.user
|
||||||
|
)
|
||||||
|
serializer = UserNotificationPreferenceSerializer(
|
||||||
|
user_notification_preference
|
||||||
|
)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
# update the object
|
||||||
|
def patch(self, request):
|
||||||
|
user_notification_preference = UserNotificationPreference.objects.get(
|
||||||
|
user=request.user
|
||||||
|
)
|
||||||
|
serializer = UserNotificationPreferenceSerializer(
|
||||||
|
user_notification_preference, data=request.data, partial=True
|
||||||
|
)
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save()
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# Python imports
|
# Python imports
|
||||||
from datetime import timedelta, date, datetime
|
from datetime import date, datetime, timedelta
|
||||||
|
|
||||||
# Django imports
|
# Django imports
|
||||||
from django.db import connection
|
from django.db import connection
|
||||||
@ -7,30 +7,19 @@ from django.db.models import Exists, OuterRef, Q
|
|||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.views.decorators.gzip import gzip_page
|
from django.views.decorators.gzip import gzip_page
|
||||||
|
|
||||||
# Third party imports
|
# Third party imports
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
# Module imports
|
|
||||||
from .base import BaseViewSet, BaseAPIView
|
|
||||||
from plane.app.permissions import ProjectEntityPermission
|
from plane.app.permissions import ProjectEntityPermission
|
||||||
from plane.db.models import (
|
from plane.app.serializers import (IssueLiteSerializer, PageFavoriteSerializer,
|
||||||
Page,
|
PageLogSerializer, PageSerializer,
|
||||||
PageFavorite,
|
SubPageSerializer)
|
||||||
Issue,
|
from plane.db.models import (Issue, IssueActivity, IssueAssignee, Page,
|
||||||
IssueAssignee,
|
PageFavorite, PageLog, ProjectMember)
|
||||||
IssueActivity,
|
|
||||||
PageLog,
|
# Module imports
|
||||||
ProjectMember,
|
from .base import BaseAPIView, BaseViewSet
|
||||||
)
|
|
||||||
from plane.app.serializers import (
|
|
||||||
PageSerializer,
|
|
||||||
PageFavoriteSerializer,
|
|
||||||
PageLogSerializer,
|
|
||||||
IssueLiteSerializer,
|
|
||||||
SubPageSerializer,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def unarchive_archive_page_and_descendants(page_id, archived_at):
|
def unarchive_archive_page_and_descendants(page_id, archived_at):
|
||||||
@ -97,7 +86,9 @@ class PageViewSet(BaseViewSet):
|
|||||||
|
|
||||||
def partial_update(self, request, slug, project_id, pk):
|
def partial_update(self, request, slug, project_id, pk):
|
||||||
try:
|
try:
|
||||||
page = Page.objects.get(pk=pk, workspace__slug=slug, project_id=project_id)
|
page = Page.objects.get(
|
||||||
|
pk=pk, workspace__slug=slug, project_id=project_id
|
||||||
|
)
|
||||||
|
|
||||||
if page.is_locked:
|
if page.is_locked:
|
||||||
return Response(
|
return Response(
|
||||||
@ -127,7 +118,9 @@ class PageViewSet(BaseViewSet):
|
|||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
serializer.save()
|
serializer.save()
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(
|
||||||
|
serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
except Page.DoesNotExist:
|
except Page.DoesNotExist:
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
@ -157,22 +150,26 @@ class PageViewSet(BaseViewSet):
|
|||||||
|
|
||||||
def list(self, request, slug, project_id):
|
def list(self, request, slug, project_id):
|
||||||
queryset = self.get_queryset().filter(archived_at__isnull=True)
|
queryset = self.get_queryset().filter(archived_at__isnull=True)
|
||||||
return Response(
|
pages = PageSerializer(queryset, many=True).data
|
||||||
PageSerializer(queryset, many=True).data, status=status.HTTP_200_OK
|
return Response(pages, status=status.HTTP_200_OK)
|
||||||
)
|
|
||||||
|
|
||||||
def archive(self, request, slug, project_id, page_id):
|
def archive(self, request, slug, project_id, page_id):
|
||||||
page = Page.objects.get(pk=page_id, workspace__slug=slug, project_id=project_id)
|
page = Page.objects.get(
|
||||||
|
pk=page_id, workspace__slug=slug, project_id=project_id
|
||||||
|
)
|
||||||
|
|
||||||
# only the owner and admin can archive the page
|
# only the owner or admin can archive the page
|
||||||
if (
|
if (
|
||||||
ProjectMember.objects.filter(
|
ProjectMember.objects.filter(
|
||||||
project_id=project_id, member=request.user, is_active=True, role__gt=20
|
project_id=project_id,
|
||||||
|
member=request.user,
|
||||||
|
is_active=True,
|
||||||
|
role__lte=15,
|
||||||
).exists()
|
).exists()
|
||||||
or request.user.id != page.owned_by_id
|
and request.user.id != page.owned_by_id
|
||||||
):
|
):
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Only the owner and admin can archive the page"},
|
{"error": "Only the owner or admin can archive the page"},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -181,17 +178,22 @@ class PageViewSet(BaseViewSet):
|
|||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
def unarchive(self, request, slug, project_id, page_id):
|
def unarchive(self, request, slug, project_id, page_id):
|
||||||
page = Page.objects.get(pk=page_id, workspace__slug=slug, project_id=project_id)
|
page = Page.objects.get(
|
||||||
|
pk=page_id, workspace__slug=slug, project_id=project_id
|
||||||
|
)
|
||||||
|
|
||||||
# only the owner and admin can un archive the page
|
# only the owner or admin can un archive the page
|
||||||
if (
|
if (
|
||||||
ProjectMember.objects.filter(
|
ProjectMember.objects.filter(
|
||||||
project_id=project_id, member=request.user, is_active=True, role__gt=20
|
project_id=project_id,
|
||||||
|
member=request.user,
|
||||||
|
is_active=True,
|
||||||
|
role__lte=15,
|
||||||
).exists()
|
).exists()
|
||||||
or request.user.id != page.owned_by_id
|
and request.user.id != page.owned_by_id
|
||||||
):
|
):
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Only the owner and admin can un archive the page"},
|
{"error": "Only the owner or admin can un archive the page"},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -210,17 +212,21 @@ class PageViewSet(BaseViewSet):
|
|||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
).filter(archived_at__isnull=False)
|
).filter(archived_at__isnull=False)
|
||||||
|
|
||||||
return Response(
|
pages = PageSerializer(pages, many=True).data
|
||||||
PageSerializer(pages, many=True).data, status=status.HTTP_200_OK
|
return Response(pages, status=status.HTTP_200_OK)
|
||||||
)
|
|
||||||
|
|
||||||
def destroy(self, request, slug, project_id, pk):
|
def destroy(self, request, slug, project_id, pk):
|
||||||
page = Page.objects.get(pk=pk, workspace__slug=slug, project_id=project_id)
|
page = Page.objects.get(
|
||||||
|
pk=pk, workspace__slug=slug, project_id=project_id
|
||||||
|
)
|
||||||
|
|
||||||
# only the owner and admin can delete the page
|
# only the owner and admin can delete the page
|
||||||
if (
|
if (
|
||||||
ProjectMember.objects.filter(
|
ProjectMember.objects.filter(
|
||||||
project_id=project_id, member=request.user, is_active=True, role__gt=20
|
project_id=project_id,
|
||||||
|
member=request.user,
|
||||||
|
is_active=True,
|
||||||
|
role__gt=20,
|
||||||
).exists()
|
).exists()
|
||||||
or request.user.id != page.owned_by_id
|
or request.user.id != page.owned_by_id
|
||||||
):
|
):
|
||||||
|
@ -36,6 +36,7 @@ from plane.app.serializers import (
|
|||||||
ProjectFavoriteSerializer,
|
ProjectFavoriteSerializer,
|
||||||
ProjectDeployBoardSerializer,
|
ProjectDeployBoardSerializer,
|
||||||
ProjectMemberAdminSerializer,
|
ProjectMemberAdminSerializer,
|
||||||
|
ProjectMemberRoleSerializer,
|
||||||
)
|
)
|
||||||
|
|
||||||
from plane.app.permissions import (
|
from plane.app.permissions import (
|
||||||
@ -67,7 +68,7 @@ from plane.bgtasks.project_invitation_task import project_invitation
|
|||||||
|
|
||||||
|
|
||||||
class ProjectViewSet(WebhookMixin, BaseViewSet):
|
class ProjectViewSet(WebhookMixin, BaseViewSet):
|
||||||
serializer_class = ProjectSerializer
|
serializer_class = ProjectListSerializer
|
||||||
model = Project
|
model = Project
|
||||||
webhook_event = "project"
|
webhook_event = "project"
|
||||||
|
|
||||||
@ -75,19 +76,20 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
|
|||||||
ProjectBasePermission,
|
ProjectBasePermission,
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_serializer_class(self, *args, **kwargs):
|
|
||||||
if self.action in ["update", "partial_update"]:
|
|
||||||
return ProjectSerializer
|
|
||||||
return ProjectDetailSerializer
|
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.filter_queryset(
|
return self.filter_queryset(
|
||||||
super()
|
super()
|
||||||
.get_queryset()
|
.get_queryset()
|
||||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
.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(
|
.select_related(
|
||||||
"workspace", "workspace__owner", "default_assignee", "project_lead"
|
"workspace",
|
||||||
|
"workspace__owner",
|
||||||
|
"default_assignee",
|
||||||
|
"project_lead",
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
is_favorite=Exists(
|
is_favorite=Exists(
|
||||||
@ -159,7 +161,11 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def list(self, request, slug):
|
def list(self, request, slug):
|
||||||
fields = [field for field in request.GET.get("fields", "").split(",") if field]
|
fields = [
|
||||||
|
field
|
||||||
|
for field in request.GET.get("fields", "").split(",")
|
||||||
|
if field
|
||||||
|
]
|
||||||
|
|
||||||
sort_order_query = ProjectMember.objects.filter(
|
sort_order_query = ProjectMember.objects.filter(
|
||||||
member=request.user,
|
member=request.user,
|
||||||
@ -172,7 +178,9 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
|
|||||||
.annotate(sort_order=Subquery(sort_order_query))
|
.annotate(sort_order=Subquery(sort_order_query))
|
||||||
.order_by("sort_order", "name")
|
.order_by("sort_order", "name")
|
||||||
)
|
)
|
||||||
if request.GET.get("per_page", False) and request.GET.get("cursor", False):
|
if request.GET.get("per_page", False) and request.GET.get(
|
||||||
|
"cursor", False
|
||||||
|
):
|
||||||
return self.paginate(
|
return self.paginate(
|
||||||
request=request,
|
request=request,
|
||||||
queryset=(projects),
|
queryset=(projects),
|
||||||
@ -180,12 +188,10 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
|
|||||||
projects, many=True
|
projects, many=True
|
||||||
).data,
|
).data,
|
||||||
)
|
)
|
||||||
|
projects = ProjectListSerializer(
|
||||||
return Response(
|
projects, many=True, fields=fields if fields else None
|
||||||
ProjectListSerializer(
|
).data
|
||||||
projects, many=True, fields=fields if fields else None
|
return Response(projects, status=status.HTTP_200_OK)
|
||||||
).data
|
|
||||||
)
|
|
||||||
|
|
||||||
def create(self, request, slug):
|
def create(self, request, slug):
|
||||||
try:
|
try:
|
||||||
@ -199,7 +205,9 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
|
|||||||
|
|
||||||
# Add the user as Administrator to the project
|
# Add the user as Administrator to the project
|
||||||
project_member = ProjectMember.objects.create(
|
project_member = ProjectMember.objects.create(
|
||||||
project_id=serializer.data["id"], member=request.user, role=20
|
project_id=serializer.data["id"],
|
||||||
|
member=request.user,
|
||||||
|
role=20,
|
||||||
)
|
)
|
||||||
# Also create the issue property for the user
|
# Also create the issue property for the user
|
||||||
_ = IssueProperty.objects.create(
|
_ = IssueProperty.objects.create(
|
||||||
@ -272,9 +280,15 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
project = self.get_queryset().filter(pk=serializer.data["id"]).first()
|
project = (
|
||||||
|
self.get_queryset()
|
||||||
|
.filter(pk=serializer.data["id"])
|
||||||
|
.first()
|
||||||
|
)
|
||||||
serializer = ProjectListSerializer(project)
|
serializer = ProjectListSerializer(project)
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(
|
||||||
|
serializer.data, status=status.HTTP_201_CREATED
|
||||||
|
)
|
||||||
return Response(
|
return Response(
|
||||||
serializer.errors,
|
serializer.errors,
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
@ -287,7 +301,8 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
|
|||||||
)
|
)
|
||||||
except Workspace.DoesNotExist as e:
|
except Workspace.DoesNotExist as e:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Workspace does not exist"}, status=status.HTTP_404_NOT_FOUND
|
{"error": "Workspace does not exist"},
|
||||||
|
status=status.HTTP_404_NOT_FOUND,
|
||||||
)
|
)
|
||||||
except serializers.ValidationError as e:
|
except serializers.ValidationError as e:
|
||||||
return Response(
|
return Response(
|
||||||
@ -312,7 +327,9 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
|
|||||||
serializer.save()
|
serializer.save()
|
||||||
if serializer.data["inbox_view"]:
|
if serializer.data["inbox_view"]:
|
||||||
Inbox.objects.get_or_create(
|
Inbox.objects.get_or_create(
|
||||||
name=f"{project.name} Inbox", project=project, is_default=True
|
name=f"{project.name} Inbox",
|
||||||
|
project=project,
|
||||||
|
is_default=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create the triage state in Backlog group
|
# Create the triage state in Backlog group
|
||||||
@ -324,10 +341,16 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
|
|||||||
color="#ff7700",
|
color="#ff7700",
|
||||||
)
|
)
|
||||||
|
|
||||||
project = self.get_queryset().filter(pk=serializer.data["id"]).first()
|
project = (
|
||||||
|
self.get_queryset()
|
||||||
|
.filter(pk=serializer.data["id"])
|
||||||
|
.first()
|
||||||
|
)
|
||||||
serializer = ProjectListSerializer(project)
|
serializer = ProjectListSerializer(project)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(
|
||||||
|
serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
except IntegrityError as e:
|
except IntegrityError as e:
|
||||||
if "already exists" in str(e):
|
if "already exists" in str(e):
|
||||||
@ -337,7 +360,8 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
|
|||||||
)
|
)
|
||||||
except (Project.DoesNotExist, Workspace.DoesNotExist):
|
except (Project.DoesNotExist, Workspace.DoesNotExist):
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Project does not exist"}, status=status.HTTP_404_NOT_FOUND
|
{"error": "Project does not exist"},
|
||||||
|
status=status.HTTP_404_NOT_FOUND,
|
||||||
)
|
)
|
||||||
except serializers.ValidationError as e:
|
except serializers.ValidationError as e:
|
||||||
return Response(
|
return Response(
|
||||||
@ -372,11 +396,14 @@ class ProjectInvitationsViewset(BaseViewSet):
|
|||||||
# Check if email is provided
|
# Check if email is provided
|
||||||
if not emails:
|
if not emails:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Emails are required"}, status=status.HTTP_400_BAD_REQUEST
|
{"error": "Emails are required"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
requesting_user = ProjectMember.objects.get(
|
requesting_user = ProjectMember.objects.get(
|
||||||
workspace__slug=slug, project_id=project_id, member_id=request.user.id
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
member_id=request.user.id,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check if any invited user has an higher role
|
# Check if any invited user has an higher role
|
||||||
@ -550,7 +577,9 @@ class ProjectJoinEndpoint(BaseAPIView):
|
|||||||
_ = WorkspaceMember.objects.create(
|
_ = WorkspaceMember.objects.create(
|
||||||
workspace_id=project_invite.workspace_id,
|
workspace_id=project_invite.workspace_id,
|
||||||
member=user,
|
member=user,
|
||||||
role=15 if project_invite.role >= 15 else project_invite.role,
|
role=15
|
||||||
|
if project_invite.role >= 15
|
||||||
|
else project_invite.role,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# Else make him active
|
# Else make him active
|
||||||
@ -656,11 +685,25 @@ class ProjectMemberViewSet(BaseViewSet):
|
|||||||
.order_by("sort_order")
|
.order_by("sort_order")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
bulk_project_members = []
|
||||||
|
member_roles = {member.get("member_id"): member.get("role") for member in members}
|
||||||
|
# Update roles in the members array based on the member_roles dictionary
|
||||||
|
for project_member in ProjectMember.objects.filter(project_id=project_id, member_id__in=[member.get("member_id") for member in members]):
|
||||||
|
project_member.role = member_roles[str(project_member.member_id)]
|
||||||
|
project_member.is_active = True
|
||||||
|
bulk_project_members.append(project_member)
|
||||||
|
|
||||||
|
# Update the roles of the existing members
|
||||||
|
ProjectMember.objects.bulk_update(
|
||||||
|
bulk_project_members, ["is_active", "role"], batch_size=100
|
||||||
|
)
|
||||||
|
|
||||||
for member in members:
|
for member in members:
|
||||||
sort_order = [
|
sort_order = [
|
||||||
project_member.get("sort_order")
|
project_member.get("sort_order")
|
||||||
for project_member in project_members
|
for project_member in project_members
|
||||||
if str(project_member.get("member_id")) == str(member.get("member_id"))
|
if str(project_member.get("member_id"))
|
||||||
|
== str(member.get("member_id"))
|
||||||
]
|
]
|
||||||
bulk_project_members.append(
|
bulk_project_members.append(
|
||||||
ProjectMember(
|
ProjectMember(
|
||||||
@ -668,7 +711,9 @@ class ProjectMemberViewSet(BaseViewSet):
|
|||||||
role=member.get("role", 10),
|
role=member.get("role", 10),
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
workspace_id=project.workspace_id,
|
workspace_id=project.workspace_id,
|
||||||
sort_order=sort_order[0] - 10000 if len(sort_order) else 65535,
|
sort_order=sort_order[0] - 10000
|
||||||
|
if len(sort_order)
|
||||||
|
else 65535,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
bulk_issue_props.append(
|
bulk_issue_props.append(
|
||||||
@ -679,25 +724,6 @@ class ProjectMemberViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check if the user is already a member of the project and is inactive
|
|
||||||
if ProjectMember.objects.filter(
|
|
||||||
workspace__slug=slug,
|
|
||||||
project_id=project_id,
|
|
||||||
member_id=member.get("member_id"),
|
|
||||||
is_active=False,
|
|
||||||
).exists():
|
|
||||||
member_detail = ProjectMember.objects.get(
|
|
||||||
workspace__slug=slug,
|
|
||||||
project_id=project_id,
|
|
||||||
member_id=member.get("member_id"),
|
|
||||||
is_active=False,
|
|
||||||
)
|
|
||||||
# Check if the user has not deactivated the account
|
|
||||||
user = User.objects.filter(pk=member.get("member_id")).first()
|
|
||||||
if user.is_active:
|
|
||||||
member_detail.is_active = True
|
|
||||||
member_detail.save(update_fields=["is_active"])
|
|
||||||
|
|
||||||
project_members = ProjectMember.objects.bulk_create(
|
project_members = ProjectMember.objects.bulk_create(
|
||||||
bulk_project_members,
|
bulk_project_members,
|
||||||
batch_size=10,
|
batch_size=10,
|
||||||
@ -708,18 +734,12 @@ class ProjectMemberViewSet(BaseViewSet):
|
|||||||
bulk_issue_props, batch_size=10, ignore_conflicts=True
|
bulk_issue_props, batch_size=10, ignore_conflicts=True
|
||||||
)
|
)
|
||||||
|
|
||||||
serializer = ProjectMemberSerializer(project_members, many=True)
|
project_members = ProjectMember.objects.filter(project_id=project_id, member_id__in=[member.get("member_id") for member in members])
|
||||||
|
serializer = ProjectMemberRoleSerializer(project_members, many=True)
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
def list(self, request, slug, project_id):
|
def list(self, request, slug, project_id):
|
||||||
project_member = ProjectMember.objects.get(
|
# Get the list of project members for the project
|
||||||
member=request.user,
|
|
||||||
workspace__slug=slug,
|
|
||||||
project_id=project_id,
|
|
||||||
is_active=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
project_members = ProjectMember.objects.filter(
|
project_members = ProjectMember.objects.filter(
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
@ -727,10 +747,9 @@ class ProjectMemberViewSet(BaseViewSet):
|
|||||||
is_active=True,
|
is_active=True,
|
||||||
).select_related("project", "member", "workspace")
|
).select_related("project", "member", "workspace")
|
||||||
|
|
||||||
if project_member.role > 10:
|
serializer = ProjectMemberRoleSerializer(
|
||||||
serializer = ProjectMemberAdminSerializer(project_members, many=True)
|
project_members, fields=("id", "member", "role"), many=True
|
||||||
else:
|
)
|
||||||
serializer = ProjectMemberSerializer(project_members, many=True)
|
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def partial_update(self, request, slug, project_id, pk):
|
def partial_update(self, request, slug, project_id, pk):
|
||||||
@ -758,7 +777,9 @@ class ProjectMemberViewSet(BaseViewSet):
|
|||||||
> requested_project_member.role
|
> requested_project_member.role
|
||||||
):
|
):
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "You cannot update a role that is higher than your own role"},
|
{
|
||||||
|
"error": "You cannot update a role that is higher than your own role"
|
||||||
|
},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -797,7 +818,9 @@ class ProjectMemberViewSet(BaseViewSet):
|
|||||||
# User cannot deactivate higher role
|
# User cannot deactivate higher role
|
||||||
if requesting_project_member.role < project_member.role:
|
if requesting_project_member.role < project_member.role:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "You cannot remove a user having role higher than you"},
|
{
|
||||||
|
"error": "You cannot remove a user having role higher than you"
|
||||||
|
},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -848,7 +871,8 @@ class AddTeamToProjectEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
if len(team_members) == 0:
|
if len(team_members) == 0:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "No such team exists"}, status=status.HTTP_400_BAD_REQUEST
|
{"error": "No such team exists"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
workspace = Workspace.objects.get(slug=slug)
|
workspace = Workspace.objects.get(slug=slug)
|
||||||
@ -895,7 +919,8 @@ class ProjectIdentifierEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
if name == "":
|
if name == "":
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST
|
{"error": "Name is required"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
exists = ProjectIdentifier.objects.filter(
|
exists = ProjectIdentifier.objects.filter(
|
||||||
@ -912,16 +937,23 @@ class ProjectIdentifierEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
if name == "":
|
if name == "":
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST
|
{"error": "Name is required"},
|
||||||
)
|
|
||||||
|
|
||||||
if Project.objects.filter(identifier=name, workspace__slug=slug).exists():
|
|
||||||
return Response(
|
|
||||||
{"error": "Cannot delete an identifier of an existing project"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
ProjectIdentifier.objects.filter(name=name, workspace__slug=slug).delete()
|
if Project.objects.filter(
|
||||||
|
identifier=name, workspace__slug=slug
|
||||||
|
).exists():
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"error": "Cannot delete an identifier of an existing project"
|
||||||
|
},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
ProjectIdentifier.objects.filter(
|
||||||
|
name=name, workspace__slug=slug
|
||||||
|
).delete()
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
status=status.HTTP_204_NO_CONTENT,
|
status=status.HTTP_204_NO_CONTENT,
|
||||||
@ -939,7 +971,9 @@ class ProjectUserViewsEndpoint(BaseAPIView):
|
|||||||
).first()
|
).first()
|
||||||
|
|
||||||
if project_member is None:
|
if project_member is None:
|
||||||
return Response({"error": "Forbidden"}, status=status.HTTP_403_FORBIDDEN)
|
return Response(
|
||||||
|
{"error": "Forbidden"}, status=status.HTTP_403_FORBIDDEN
|
||||||
|
)
|
||||||
|
|
||||||
view_props = project_member.view_props
|
view_props = project_member.view_props
|
||||||
default_props = project_member.default_props
|
default_props = project_member.default_props
|
||||||
@ -947,8 +981,12 @@ class ProjectUserViewsEndpoint(BaseAPIView):
|
|||||||
sort_order = project_member.sort_order
|
sort_order = project_member.sort_order
|
||||||
|
|
||||||
project_member.view_props = request.data.get("view_props", view_props)
|
project_member.view_props = request.data.get("view_props", view_props)
|
||||||
project_member.default_props = request.data.get("default_props", default_props)
|
project_member.default_props = request.data.get(
|
||||||
project_member.preferences = request.data.get("preferences", preferences)
|
"default_props", default_props
|
||||||
|
)
|
||||||
|
project_member.preferences = request.data.get(
|
||||||
|
"preferences", preferences
|
||||||
|
)
|
||||||
project_member.sort_order = request.data.get("sort_order", sort_order)
|
project_member.sort_order = request.data.get("sort_order", sort_order)
|
||||||
|
|
||||||
project_member.save()
|
project_member.save()
|
||||||
@ -1010,18 +1048,11 @@ class ProjectPublicCoverImagesEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
files = []
|
files = []
|
||||||
s3_client_params = {
|
s3 = boto3.client(
|
||||||
"service_name": "s3",
|
"s3",
|
||||||
"aws_access_key_id": settings.AWS_ACCESS_KEY_ID,
|
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
|
||||||
"aws_secret_access_key": settings.AWS_SECRET_ACCESS_KEY,
|
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
|
||||||
}
|
)
|
||||||
|
|
||||||
# Use AWS_S3_ENDPOINT_URL if it is present in the settings
|
|
||||||
if hasattr(settings, "AWS_S3_ENDPOINT_URL") and settings.AWS_S3_ENDPOINT_URL:
|
|
||||||
s3_client_params["endpoint_url"] = settings.AWS_S3_ENDPOINT_URL
|
|
||||||
|
|
||||||
s3 = boto3.client(**s3_client_params)
|
|
||||||
|
|
||||||
params = {
|
params = {
|
||||||
"Bucket": settings.AWS_STORAGE_BUCKET_NAME,
|
"Bucket": settings.AWS_STORAGE_BUCKET_NAME,
|
||||||
"Prefix": "static/project-cover/",
|
"Prefix": "static/project-cover/",
|
||||||
@ -1034,19 +1065,9 @@ class ProjectPublicCoverImagesEndpoint(BaseAPIView):
|
|||||||
if not content["Key"].endswith(
|
if not content["Key"].endswith(
|
||||||
"/"
|
"/"
|
||||||
): # This line ensures we're only getting files, not "sub-folders"
|
): # This line ensures we're only getting files, not "sub-folders"
|
||||||
if (
|
files.append(
|
||||||
hasattr(settings, "AWS_S3_CUSTOM_DOMAIN")
|
f"https://{settings.AWS_STORAGE_BUCKET_NAME}.s3.{settings.AWS_REGION}.amazonaws.com/{content['Key']}"
|
||||||
and settings.AWS_S3_CUSTOM_DOMAIN
|
)
|
||||||
and hasattr(settings, "AWS_S3_URL_PROTOCOL")
|
|
||||||
and settings.AWS_S3_URL_PROTOCOL
|
|
||||||
):
|
|
||||||
files.append(
|
|
||||||
f"{settings.AWS_S3_URL_PROTOCOL}//{settings.AWS_S3_CUSTOM_DOMAIN}/{content['Key']}"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
files.append(
|
|
||||||
f"https://{settings.AWS_STORAGE_BUCKET_NAME}.s3.{settings.AWS_REGION}.amazonaws.com/{content['Key']}"
|
|
||||||
)
|
|
||||||
|
|
||||||
return Response(files, status=status.HTTP_200_OK)
|
return Response(files, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
@ -1113,6 +1134,7 @@ class UserProjectRolesEndpoint(BaseAPIView):
|
|||||||
).values("project_id", "role")
|
).values("project_id", "role")
|
||||||
|
|
||||||
project_members = {
|
project_members = {
|
||||||
str(member["project_id"]): member["role"] for member in project_members
|
str(member["project_id"]): member["role"]
|
||||||
|
for member in project_members
|
||||||
}
|
}
|
||||||
return Response(project_members, status=status.HTTP_200_OK)
|
return Response(project_members, status=status.HTTP_200_OK)
|
||||||
|
@ -10,7 +10,15 @@ from rest_framework.response import Response
|
|||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from .base import BaseAPIView
|
from .base import BaseAPIView
|
||||||
from plane.db.models import Workspace, Project, Issue, Cycle, Module, Page, IssueView
|
from plane.db.models import (
|
||||||
|
Workspace,
|
||||||
|
Project,
|
||||||
|
Issue,
|
||||||
|
Cycle,
|
||||||
|
Module,
|
||||||
|
Page,
|
||||||
|
IssueView,
|
||||||
|
)
|
||||||
from plane.utils.issue_search import search_issues
|
from plane.utils.issue_search import search_issues
|
||||||
|
|
||||||
|
|
||||||
@ -25,7 +33,9 @@ class GlobalSearchEndpoint(BaseAPIView):
|
|||||||
for field in fields:
|
for field in fields:
|
||||||
q |= Q(**{f"{field}__icontains": query})
|
q |= Q(**{f"{field}__icontains": query})
|
||||||
return (
|
return (
|
||||||
Workspace.objects.filter(q, workspace_member__member=self.request.user)
|
Workspace.objects.filter(
|
||||||
|
q, workspace_member__member=self.request.user
|
||||||
|
)
|
||||||
.distinct()
|
.distinct()
|
||||||
.values("name", "id", "slug")
|
.values("name", "id", "slug")
|
||||||
)
|
)
|
||||||
@ -38,7 +48,8 @@ class GlobalSearchEndpoint(BaseAPIView):
|
|||||||
return (
|
return (
|
||||||
Project.objects.filter(
|
Project.objects.filter(
|
||||||
q,
|
q,
|
||||||
Q(project_projectmember__member=self.request.user) | Q(network=2),
|
Q(project_projectmember__member=self.request.user)
|
||||||
|
| Q(network=2),
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
)
|
)
|
||||||
.distinct()
|
.distinct()
|
||||||
@ -169,7 +180,9 @@ class GlobalSearchEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
def get(self, request, slug):
|
def get(self, request, slug):
|
||||||
query = request.query_params.get("search", False)
|
query = request.query_params.get("search", False)
|
||||||
workspace_search = request.query_params.get("workspace_search", "false")
|
workspace_search = request.query_params.get(
|
||||||
|
"workspace_search", "false"
|
||||||
|
)
|
||||||
project_id = request.query_params.get("project_id", False)
|
project_id = request.query_params.get("project_id", False)
|
||||||
|
|
||||||
if not query:
|
if not query:
|
||||||
@ -209,11 +222,13 @@ class GlobalSearchEndpoint(BaseAPIView):
|
|||||||
class IssueSearchEndpoint(BaseAPIView):
|
class IssueSearchEndpoint(BaseAPIView):
|
||||||
def get(self, request, slug, project_id):
|
def get(self, request, slug, project_id):
|
||||||
query = request.query_params.get("search", False)
|
query = request.query_params.get("search", False)
|
||||||
workspace_search = request.query_params.get("workspace_search", "false")
|
workspace_search = request.query_params.get(
|
||||||
|
"workspace_search", "false"
|
||||||
|
)
|
||||||
parent = request.query_params.get("parent", "false")
|
parent = request.query_params.get("parent", "false")
|
||||||
issue_relation = request.query_params.get("issue_relation", "false")
|
issue_relation = request.query_params.get("issue_relation", "false")
|
||||||
cycle = request.query_params.get("cycle", "false")
|
cycle = request.query_params.get("cycle", "false")
|
||||||
module = request.query_params.get("module", "false")
|
module = request.query_params.get("module", False)
|
||||||
sub_issue = request.query_params.get("sub_issue", "false")
|
sub_issue = request.query_params.get("sub_issue", "false")
|
||||||
|
|
||||||
issue_id = request.query_params.get("issue_id", False)
|
issue_id = request.query_params.get("issue_id", False)
|
||||||
@ -234,9 +249,9 @@ class IssueSearchEndpoint(BaseAPIView):
|
|||||||
issues = issues.filter(
|
issues = issues.filter(
|
||||||
~Q(pk=issue_id), ~Q(pk=issue.parent_id), parent__isnull=True
|
~Q(pk=issue_id), ~Q(pk=issue.parent_id), parent__isnull=True
|
||||||
).exclude(
|
).exclude(
|
||||||
pk__in=Issue.issue_objects.filter(parent__isnull=False).values_list(
|
pk__in=Issue.issue_objects.filter(
|
||||||
"parent_id", flat=True
|
parent__isnull=False
|
||||||
)
|
).values_list("parent_id", flat=True)
|
||||||
)
|
)
|
||||||
if issue_relation == "true" and issue_id:
|
if issue_relation == "true" and issue_id:
|
||||||
issue = Issue.issue_objects.get(pk=issue_id)
|
issue = Issue.issue_objects.get(pk=issue_id)
|
||||||
@ -254,8 +269,8 @@ class IssueSearchEndpoint(BaseAPIView):
|
|||||||
if cycle == "true":
|
if cycle == "true":
|
||||||
issues = issues.exclude(issue_cycle__isnull=False)
|
issues = issues.exclude(issue_cycle__isnull=False)
|
||||||
|
|
||||||
if module == "true":
|
if module:
|
||||||
issues = issues.exclude(issue_module__isnull=False)
|
issues = issues.exclude(issue_module__module=module)
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
issues.values(
|
issues.values(
|
||||||
|
@ -9,9 +9,12 @@ from rest_framework.response import Response
|
|||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from . import BaseViewSet
|
from . import BaseViewSet, BaseAPIView
|
||||||
from plane.app.serializers import StateSerializer
|
from plane.app.serializers import StateSerializer
|
||||||
from plane.app.permissions import ProjectEntityPermission
|
from plane.app.permissions import (
|
||||||
|
ProjectEntityPermission,
|
||||||
|
WorkspaceEntityPermission,
|
||||||
|
)
|
||||||
from plane.db.models import State, Issue
|
from plane.db.models import State, Issue
|
||||||
|
|
||||||
|
|
||||||
@ -22,9 +25,6 @@ class StateViewSet(BaseViewSet):
|
|||||||
ProjectEntityPermission,
|
ProjectEntityPermission,
|
||||||
]
|
]
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
|
||||||
serializer.save(project_id=self.kwargs.get("project_id"))
|
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.filter_queryset(
|
return self.filter_queryset(
|
||||||
super()
|
super()
|
||||||
@ -77,14 +77,19 @@ class StateViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if state.default:
|
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=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
# Check for any issues in the state
|
# Check for any issues in the state
|
||||||
issue_exist = Issue.issue_objects.filter(state=pk).exists()
|
issue_exist = Issue.issue_objects.filter(state=pk).exists()
|
||||||
|
|
||||||
if issue_exist:
|
if issue_exist:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "The state is not empty, only empty states can be deleted"},
|
{
|
||||||
|
"error": "The state is not empty, only empty states can be deleted"
|
||||||
|
},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -43,7 +43,9 @@ class UserEndpoint(BaseViewSet):
|
|||||||
is_admin = InstanceAdmin.objects.filter(
|
is_admin = InstanceAdmin.objects.filter(
|
||||||
instance=instance, user=request.user
|
instance=instance, user=request.user
|
||||||
).exists()
|
).exists()
|
||||||
return Response({"is_instance_admin": is_admin}, status=status.HTTP_200_OK)
|
return Response(
|
||||||
|
{"is_instance_admin": is_admin}, status=status.HTTP_200_OK
|
||||||
|
)
|
||||||
|
|
||||||
def deactivate(self, request):
|
def deactivate(self, request):
|
||||||
# Check all workspace user is active
|
# Check all workspace user is active
|
||||||
@ -51,7 +53,12 @@ class UserEndpoint(BaseViewSet):
|
|||||||
|
|
||||||
# Instance admin check
|
# Instance admin check
|
||||||
if InstanceAdmin.objects.filter(user=user).exists():
|
if InstanceAdmin.objects.filter(user=user).exists():
|
||||||
return Response({"error": "You cannot deactivate your account since you are an instance admin"}, status=status.HTTP_400_BAD_REQUEST)
|
return Response(
|
||||||
|
{
|
||||||
|
"error": "You cannot deactivate your account since you are an instance admin"
|
||||||
|
},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
projects_to_deactivate = []
|
projects_to_deactivate = []
|
||||||
workspaces_to_deactivate = []
|
workspaces_to_deactivate = []
|
||||||
@ -61,7 +68,10 @@ class UserEndpoint(BaseViewSet):
|
|||||||
).annotate(
|
).annotate(
|
||||||
other_admin_exists=Count(
|
other_admin_exists=Count(
|
||||||
Case(
|
Case(
|
||||||
When(Q(role=20, is_active=True) & ~Q(member=request.user), then=1),
|
When(
|
||||||
|
Q(role=20, is_active=True) & ~Q(member=request.user),
|
||||||
|
then=1,
|
||||||
|
),
|
||||||
default=0,
|
default=0,
|
||||||
output_field=IntegerField(),
|
output_field=IntegerField(),
|
||||||
)
|
)
|
||||||
@ -86,7 +96,10 @@ class UserEndpoint(BaseViewSet):
|
|||||||
).annotate(
|
).annotate(
|
||||||
other_admin_exists=Count(
|
other_admin_exists=Count(
|
||||||
Case(
|
Case(
|
||||||
When(Q(role=20, is_active=True) & ~Q(member=request.user), then=1),
|
When(
|
||||||
|
Q(role=20, is_active=True) & ~Q(member=request.user),
|
||||||
|
then=1,
|
||||||
|
),
|
||||||
default=0,
|
default=0,
|
||||||
output_field=IntegerField(),
|
output_field=IntegerField(),
|
||||||
)
|
)
|
||||||
@ -95,7 +108,9 @@ class UserEndpoint(BaseViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
for workspace in workspaces:
|
for workspace in workspaces:
|
||||||
if workspace.other_admin_exists > 0 or (workspace.total_members == 1):
|
if workspace.other_admin_exists > 0 or (
|
||||||
|
workspace.total_members == 1
|
||||||
|
):
|
||||||
workspace.is_active = False
|
workspace.is_active = False
|
||||||
workspaces_to_deactivate.append(workspace)
|
workspaces_to_deactivate.append(workspace)
|
||||||
else:
|
else:
|
||||||
@ -134,7 +149,9 @@ class UpdateUserOnBoardedEndpoint(BaseAPIView):
|
|||||||
user = User.objects.get(pk=request.user.id, is_active=True)
|
user = User.objects.get(pk=request.user.id, is_active=True)
|
||||||
user.is_onboarded = request.data.get("is_onboarded", False)
|
user.is_onboarded = request.data.get("is_onboarded", False)
|
||||||
user.save()
|
user.save()
|
||||||
return Response({"message": "Updated successfully"}, status=status.HTTP_200_OK)
|
return Response(
|
||||||
|
{"message": "Updated successfully"}, status=status.HTTP_200_OK
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class UpdateUserTourCompletedEndpoint(BaseAPIView):
|
class UpdateUserTourCompletedEndpoint(BaseAPIView):
|
||||||
@ -142,14 +159,16 @@ class UpdateUserTourCompletedEndpoint(BaseAPIView):
|
|||||||
user = User.objects.get(pk=request.user.id, is_active=True)
|
user = User.objects.get(pk=request.user.id, is_active=True)
|
||||||
user.is_tour_completed = request.data.get("is_tour_completed", False)
|
user.is_tour_completed = request.data.get("is_tour_completed", False)
|
||||||
user.save()
|
user.save()
|
||||||
return Response({"message": "Updated successfully"}, status=status.HTTP_200_OK)
|
return Response(
|
||||||
|
{"message": "Updated successfully"}, status=status.HTTP_200_OK
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class UserActivityEndpoint(BaseAPIView, BasePaginator):
|
class UserActivityEndpoint(BaseAPIView, BasePaginator):
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
queryset = IssueActivity.objects.filter(actor=request.user).select_related(
|
queryset = IssueActivity.objects.filter(
|
||||||
"actor", "workspace", "issue", "project"
|
actor=request.user
|
||||||
)
|
).select_related("actor", "workspace", "issue", "project")
|
||||||
|
|
||||||
return self.paginate(
|
return self.paginate(
|
||||||
request=request,
|
request=request,
|
||||||
@ -158,4 +177,3 @@ class UserActivityEndpoint(BaseAPIView, BasePaginator):
|
|||||||
issue_activities, many=True
|
issue_activities, many=True
|
||||||
).data,
|
).data,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -24,10 +24,15 @@ from . import BaseViewSet, BaseAPIView
|
|||||||
from plane.app.serializers import (
|
from plane.app.serializers import (
|
||||||
GlobalViewSerializer,
|
GlobalViewSerializer,
|
||||||
IssueViewSerializer,
|
IssueViewSerializer,
|
||||||
IssueLiteSerializer,
|
IssueSerializer,
|
||||||
IssueViewFavoriteSerializer,
|
IssueViewFavoriteSerializer,
|
||||||
)
|
)
|
||||||
from plane.app.permissions import WorkspaceEntityPermission, ProjectEntityPermission
|
from plane.app.permissions import (
|
||||||
|
WorkspaceEntityPermission,
|
||||||
|
ProjectEntityPermission,
|
||||||
|
WorkspaceViewerPermission,
|
||||||
|
ProjectLitePermission,
|
||||||
|
)
|
||||||
from plane.db.models import (
|
from plane.db.models import (
|
||||||
Workspace,
|
Workspace,
|
||||||
GlobalView,
|
GlobalView,
|
||||||
@ -37,14 +42,15 @@ from plane.db.models import (
|
|||||||
IssueReaction,
|
IssueReaction,
|
||||||
IssueLink,
|
IssueLink,
|
||||||
IssueAttachment,
|
IssueAttachment,
|
||||||
|
IssueSubscriber,
|
||||||
)
|
)
|
||||||
from plane.utils.issue_filters import issue_filters
|
from plane.utils.issue_filters import issue_filters
|
||||||
from plane.utils.grouper import group_results
|
from plane.utils.grouper import group_results
|
||||||
|
|
||||||
|
|
||||||
class GlobalViewViewSet(BaseViewSet):
|
class GlobalViewViewSet(BaseViewSet):
|
||||||
serializer_class = GlobalViewSerializer
|
serializer_class = IssueViewSerializer
|
||||||
model = GlobalView
|
model = IssueView
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
WorkspaceEntityPermission,
|
WorkspaceEntityPermission,
|
||||||
]
|
]
|
||||||
@ -58,6 +64,7 @@ class GlobalViewViewSet(BaseViewSet):
|
|||||||
super()
|
super()
|
||||||
.get_queryset()
|
.get_queryset()
|
||||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||||
|
.filter(project__isnull=True)
|
||||||
.select_related("workspace")
|
.select_related("workspace")
|
||||||
.order_by(self.request.GET.get("order_by", "-created_at"))
|
.order_by(self.request.GET.get("order_by", "-created_at"))
|
||||||
.distinct()
|
.distinct()
|
||||||
@ -72,18 +79,16 @@ class GlobalViewIssuesViewSet(BaseViewSet):
|
|||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return (
|
return (
|
||||||
Issue.issue_objects.annotate(
|
Issue.issue_objects.annotate(
|
||||||
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
|
sub_issues_count=Issue.issue_objects.filter(
|
||||||
|
parent=OuterRef("id")
|
||||||
|
)
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
)
|
)
|
||||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||||
.select_related("project")
|
.select_related("workspace", "project", "state", "parent")
|
||||||
.select_related("workspace")
|
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||||
.select_related("state")
|
|
||||||
.select_related("parent")
|
|
||||||
.prefetch_related("assignees")
|
|
||||||
.prefetch_related("labels")
|
|
||||||
.prefetch_related(
|
.prefetch_related(
|
||||||
Prefetch(
|
Prefetch(
|
||||||
"issue_reactions",
|
"issue_reactions",
|
||||||
@ -95,11 +100,21 @@ class GlobalViewIssuesViewSet(BaseViewSet):
|
|||||||
@method_decorator(gzip_page)
|
@method_decorator(gzip_page)
|
||||||
def list(self, request, slug):
|
def list(self, request, slug):
|
||||||
filters = issue_filters(request.query_params, "GET")
|
filters = issue_filters(request.query_params, "GET")
|
||||||
fields = [field for field in request.GET.get("fields", "").split(",") if field]
|
fields = [
|
||||||
|
field
|
||||||
|
for field in request.GET.get("fields", "").split(",")
|
||||||
|
if field
|
||||||
|
]
|
||||||
|
|
||||||
# Custom ordering for priority and state
|
# Custom ordering for priority and state
|
||||||
priority_order = ["urgent", "high", "medium", "low", "none"]
|
priority_order = ["urgent", "high", "medium", "low", "none"]
|
||||||
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
|
state_order = [
|
||||||
|
"backlog",
|
||||||
|
"unstarted",
|
||||||
|
"started",
|
||||||
|
"completed",
|
||||||
|
"cancelled",
|
||||||
|
]
|
||||||
|
|
||||||
order_by_param = request.GET.get("order_by", "-created_at")
|
order_by_param = request.GET.get("order_by", "-created_at")
|
||||||
|
|
||||||
@ -108,7 +123,6 @@ class GlobalViewIssuesViewSet(BaseViewSet):
|
|||||||
.filter(**filters)
|
.filter(**filters)
|
||||||
.filter(project__project_projectmember__member=self.request.user)
|
.filter(project__project_projectmember__member=self.request.user)
|
||||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||||
.annotate(module_id=F("issue_module__module_id"))
|
|
||||||
.annotate(
|
.annotate(
|
||||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||||
.order_by()
|
.order_by()
|
||||||
@ -116,7 +130,17 @@ class GlobalViewIssuesViewSet(BaseViewSet):
|
|||||||
.values("count")
|
.values("count")
|
||||||
)
|
)
|
||||||
.annotate(
|
.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")
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
sub_issues_count=Issue.issue_objects.filter(
|
||||||
|
parent=OuterRef("id")
|
||||||
|
)
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
@ -126,7 +150,9 @@ class GlobalViewIssuesViewSet(BaseViewSet):
|
|||||||
# Priority Ordering
|
# Priority Ordering
|
||||||
if order_by_param == "priority" or order_by_param == "-priority":
|
if order_by_param == "priority" or order_by_param == "-priority":
|
||||||
priority_order = (
|
priority_order = (
|
||||||
priority_order 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(
|
issue_queryset = issue_queryset.annotate(
|
||||||
priority_order=Case(
|
priority_order=Case(
|
||||||
@ -174,17 +200,17 @@ class GlobalViewIssuesViewSet(BaseViewSet):
|
|||||||
else order_by_param
|
else order_by_param
|
||||||
)
|
)
|
||||||
).order_by(
|
).order_by(
|
||||||
"-max_values" if order_by_param.startswith("-") else "max_values"
|
"-max_values"
|
||||||
|
if order_by_param.startswith("-")
|
||||||
|
else "max_values"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
issue_queryset = issue_queryset.order_by(order_by_param)
|
issue_queryset = issue_queryset.order_by(order_by_param)
|
||||||
|
|
||||||
issues = IssueLiteSerializer(issue_queryset, many=True, fields=fields if fields else None).data
|
serializer = IssueSerializer(
|
||||||
issue_dict = {str(issue["id"]): issue for issue in issues}
|
issue_queryset, many=True, fields=fields if fields else None
|
||||||
return Response(
|
|
||||||
issue_dict,
|
|
||||||
status=status.HTTP_200_OK,
|
|
||||||
)
|
)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
class IssueViewViewSet(BaseViewSet):
|
class IssueViewViewSet(BaseViewSet):
|
||||||
@ -217,6 +243,18 @@ class IssueViewViewSet(BaseViewSet):
|
|||||||
.distinct()
|
.distinct()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def list(self, request, slug, project_id):
|
||||||
|
queryset = self.get_queryset()
|
||||||
|
fields = [
|
||||||
|
field
|
||||||
|
for field in request.GET.get("fields", "").split(",")
|
||||||
|
if field
|
||||||
|
]
|
||||||
|
views = IssueViewSerializer(
|
||||||
|
queryset, many=True, fields=fields if fields else None
|
||||||
|
).data
|
||||||
|
return Response(views, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
class IssueViewFavoriteViewSet(BaseViewSet):
|
class IssueViewFavoriteViewSet(BaseViewSet):
|
||||||
serializer_class = IssueViewFavoriteSerializer
|
serializer_class = IssueViewFavoriteSerializer
|
||||||
|
@ -26,8 +26,12 @@ class WebhookEndpoint(BaseAPIView):
|
|||||||
)
|
)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
serializer.save(workspace_id=workspace.id)
|
serializer.save(workspace_id=workspace.id)
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
serializer.data, status=status.HTTP_201_CREATED
|
||||||
|
)
|
||||||
|
return Response(
|
||||||
|
serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
except IntegrityError as e:
|
except IntegrityError as e:
|
||||||
if "already exists" in str(e):
|
if "already exists" in str(e):
|
||||||
return Response(
|
return Response(
|
||||||
|
@ -41,13 +41,19 @@ from plane.app.serializers import (
|
|||||||
ProjectMemberSerializer,
|
ProjectMemberSerializer,
|
||||||
WorkspaceThemeSerializer,
|
WorkspaceThemeSerializer,
|
||||||
IssueActivitySerializer,
|
IssueActivitySerializer,
|
||||||
IssueLiteSerializer,
|
IssueSerializer,
|
||||||
WorkspaceMemberAdminSerializer,
|
WorkspaceMemberAdminSerializer,
|
||||||
WorkspaceMemberMeSerializer,
|
WorkspaceMemberMeSerializer,
|
||||||
|
ProjectMemberRoleSerializer,
|
||||||
|
WorkspaceUserPropertiesSerializer,
|
||||||
|
WorkspaceEstimateSerializer,
|
||||||
|
StateSerializer,
|
||||||
|
LabelSerializer,
|
||||||
)
|
)
|
||||||
from plane.app.views.base import BaseAPIView
|
from plane.app.views.base import BaseAPIView
|
||||||
from . import BaseViewSet
|
from . import BaseViewSet
|
||||||
from plane.db.models import (
|
from plane.db.models import (
|
||||||
|
State,
|
||||||
User,
|
User,
|
||||||
Workspace,
|
Workspace,
|
||||||
WorkspaceMemberInvite,
|
WorkspaceMemberInvite,
|
||||||
@ -64,6 +70,9 @@ from plane.db.models import (
|
|||||||
WorkspaceMember,
|
WorkspaceMember,
|
||||||
CycleIssue,
|
CycleIssue,
|
||||||
IssueReaction,
|
IssueReaction,
|
||||||
|
WorkspaceUserProperties,
|
||||||
|
Estimate,
|
||||||
|
EstimatePoint,
|
||||||
)
|
)
|
||||||
from plane.app.permissions import (
|
from plane.app.permissions import (
|
||||||
WorkSpaceBasePermission,
|
WorkSpaceBasePermission,
|
||||||
@ -71,11 +80,13 @@ from plane.app.permissions import (
|
|||||||
WorkspaceEntityPermission,
|
WorkspaceEntityPermission,
|
||||||
WorkspaceViewerPermission,
|
WorkspaceViewerPermission,
|
||||||
WorkspaceUserPermission,
|
WorkspaceUserPermission,
|
||||||
|
ProjectLitePermission,
|
||||||
)
|
)
|
||||||
from plane.bgtasks.workspace_invitation_task import workspace_invitation
|
from plane.bgtasks.workspace_invitation_task import workspace_invitation
|
||||||
from plane.utils.issue_filters import issue_filters
|
from plane.utils.issue_filters import issue_filters
|
||||||
from plane.bgtasks.event_tracking_task import workspace_invite_event
|
from plane.bgtasks.event_tracking_task import workspace_invite_event
|
||||||
|
|
||||||
|
|
||||||
class WorkSpaceViewSet(BaseViewSet):
|
class WorkSpaceViewSet(BaseViewSet):
|
||||||
model = Workspace
|
model = Workspace
|
||||||
serializer_class = WorkSpaceSerializer
|
serializer_class = WorkSpaceSerializer
|
||||||
@ -111,7 +122,9 @@ class WorkSpaceViewSet(BaseViewSet):
|
|||||||
.values("count")
|
.values("count")
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
self.filter_queryset(super().get_queryset().select_related("owner"))
|
self.filter_queryset(
|
||||||
|
super().get_queryset().select_related("owner")
|
||||||
|
)
|
||||||
.order_by("name")
|
.order_by("name")
|
||||||
.filter(
|
.filter(
|
||||||
workspace_member__member=self.request.user,
|
workspace_member__member=self.request.user,
|
||||||
@ -137,7 +150,9 @@ class WorkSpaceViewSet(BaseViewSet):
|
|||||||
|
|
||||||
if len(name) > 80 or len(slug) > 48:
|
if len(name) > 80 or len(slug) > 48:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "The maximum length for name is 80 and for slug is 48"},
|
{
|
||||||
|
"error": "The maximum length for name is 80 and for slug is 48"
|
||||||
|
},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -150,7 +165,9 @@ class WorkSpaceViewSet(BaseViewSet):
|
|||||||
role=20,
|
role=20,
|
||||||
company_role=request.data.get("company_role", ""),
|
company_role=request.data.get("company_role", ""),
|
||||||
)
|
)
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(
|
||||||
|
serializer.data, status=status.HTTP_201_CREATED
|
||||||
|
)
|
||||||
return Response(
|
return Response(
|
||||||
[serializer.errors[error][0] for error in serializer.errors],
|
[serializer.errors[error][0] for error in serializer.errors],
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
@ -173,6 +190,11 @@ class UserWorkSpacesEndpoint(BaseAPIView):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
|
fields = [
|
||||||
|
field
|
||||||
|
for field in request.GET.get("fields", "").split(",")
|
||||||
|
if field
|
||||||
|
]
|
||||||
member_count = (
|
member_count = (
|
||||||
WorkspaceMember.objects.filter(
|
WorkspaceMember.objects.filter(
|
||||||
workspace=OuterRef("id"),
|
workspace=OuterRef("id"),
|
||||||
@ -204,13 +226,17 @@ class UserWorkSpacesEndpoint(BaseAPIView):
|
|||||||
.annotate(total_members=member_count)
|
.annotate(total_members=member_count)
|
||||||
.annotate(total_issues=issue_count)
|
.annotate(total_issues=issue_count)
|
||||||
.filter(
|
.filter(
|
||||||
workspace_member__member=request.user, workspace_member__is_active=True
|
workspace_member__member=request.user,
|
||||||
|
workspace_member__is_active=True,
|
||||||
)
|
)
|
||||||
.distinct()
|
.distinct()
|
||||||
)
|
)
|
||||||
|
workspaces = WorkSpaceSerializer(
|
||||||
serializer = WorkSpaceSerializer(self.filter_queryset(workspace), many=True)
|
self.filter_queryset(workspace),
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
fields=fields if fields else None,
|
||||||
|
many=True,
|
||||||
|
).data
|
||||||
|
return Response(workspaces, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
class WorkSpaceAvailabilityCheckEndpoint(BaseAPIView):
|
class WorkSpaceAvailabilityCheckEndpoint(BaseAPIView):
|
||||||
@ -250,7 +276,8 @@ class WorkspaceInvitationsViewset(BaseViewSet):
|
|||||||
# Check if email is provided
|
# Check if email is provided
|
||||||
if not emails:
|
if not emails:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Emails are required"}, status=status.HTTP_400_BAD_REQUEST
|
{"error": "Emails are required"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
# check for role level of the requesting user
|
# check for role level of the requesting user
|
||||||
@ -537,10 +564,15 @@ class WorkSpaceMemberViewSet(BaseViewSet):
|
|||||||
workspace_members = self.get_queryset()
|
workspace_members = self.get_queryset()
|
||||||
|
|
||||||
if workspace_member.role > 10:
|
if workspace_member.role > 10:
|
||||||
serializer = WorkspaceMemberAdminSerializer(workspace_members, many=True)
|
serializer = WorkspaceMemberAdminSerializer(
|
||||||
|
workspace_members,
|
||||||
|
fields=("id", "member", "role"),
|
||||||
|
many=True,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
serializer = WorkSpaceMemberSerializer(
|
serializer = WorkSpaceMemberSerializer(
|
||||||
workspace_members,
|
workspace_members,
|
||||||
|
fields=("id", "member", "role"),
|
||||||
many=True,
|
many=True,
|
||||||
)
|
)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
@ -572,7 +604,9 @@ class WorkSpaceMemberViewSet(BaseViewSet):
|
|||||||
> requested_workspace_member.role
|
> requested_workspace_member.role
|
||||||
):
|
):
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "You cannot update a role that is higher than your own role"},
|
{
|
||||||
|
"error": "You cannot update a role that is higher than your own role"
|
||||||
|
},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -611,7 +645,9 @@ class WorkSpaceMemberViewSet(BaseViewSet):
|
|||||||
|
|
||||||
if requesting_workspace_member.role < workspace_member.role:
|
if requesting_workspace_member.role < workspace_member.role:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "You cannot remove a user having role higher than you"},
|
{
|
||||||
|
"error": "You cannot remove a user having role higher than you"
|
||||||
|
},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -705,6 +741,49 @@ class WorkSpaceMemberViewSet(BaseViewSet):
|
|||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
|
class WorkspaceProjectMemberEndpoint(BaseAPIView):
|
||||||
|
serializer_class = ProjectMemberRoleSerializer
|
||||||
|
model = ProjectMember
|
||||||
|
|
||||||
|
permission_classes = [
|
||||||
|
WorkspaceEntityPermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
def get(self, request, slug):
|
||||||
|
# Fetch all project IDs where the user is involved
|
||||||
|
project_ids = (
|
||||||
|
ProjectMember.objects.filter(
|
||||||
|
member=request.user,
|
||||||
|
member__is_bot=False,
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
.values_list("project_id", flat=True)
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get all the project members in which the user is involved
|
||||||
|
project_members = ProjectMember.objects.filter(
|
||||||
|
workspace__slug=slug,
|
||||||
|
member__is_bot=False,
|
||||||
|
project_id__in=project_ids,
|
||||||
|
is_active=True,
|
||||||
|
).select_related("project", "member", "workspace")
|
||||||
|
project_members = ProjectMemberRoleSerializer(
|
||||||
|
project_members, many=True
|
||||||
|
).data
|
||||||
|
|
||||||
|
project_members_dict = dict()
|
||||||
|
|
||||||
|
# Construct a dictionary with project_id as key and project_members as value
|
||||||
|
for project_member in project_members:
|
||||||
|
project_id = project_member.pop("project")
|
||||||
|
if str(project_id) not in project_members_dict:
|
||||||
|
project_members_dict[str(project_id)] = []
|
||||||
|
project_members_dict[str(project_id)].append(project_member)
|
||||||
|
|
||||||
|
return Response(project_members_dict, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
class TeamMemberViewSet(BaseViewSet):
|
class TeamMemberViewSet(BaseViewSet):
|
||||||
serializer_class = TeamSerializer
|
serializer_class = TeamSerializer
|
||||||
model = Team
|
model = Team
|
||||||
@ -739,7 +818,9 @@ class TeamMemberViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if len(members) != len(request.data.get("members", [])):
|
if len(members) != len(request.data.get("members", [])):
|
||||||
users = list(set(request.data.get("members", [])).difference(members))
|
users = list(
|
||||||
|
set(request.data.get("members", [])).difference(members)
|
||||||
|
)
|
||||||
users = User.objects.filter(pk__in=users)
|
users = User.objects.filter(pk__in=users)
|
||||||
|
|
||||||
serializer = UserLiteSerializer(users, many=True)
|
serializer = UserLiteSerializer(users, many=True)
|
||||||
@ -753,7 +834,9 @@ class TeamMemberViewSet(BaseViewSet):
|
|||||||
|
|
||||||
workspace = Workspace.objects.get(slug=slug)
|
workspace = Workspace.objects.get(slug=slug)
|
||||||
|
|
||||||
serializer = TeamSerializer(data=request.data, context={"workspace": workspace})
|
serializer = TeamSerializer(
|
||||||
|
data=request.data, context={"workspace": workspace}
|
||||||
|
)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
serializer.save()
|
serializer.save()
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
@ -782,7 +865,9 @@ class UserLastProjectWithWorkspaceEndpoint(BaseAPIView):
|
|||||||
workspace_id=last_workspace_id, member=request.user
|
workspace_id=last_workspace_id, member=request.user
|
||||||
).select_related("workspace", "project", "member", "workspace__owner")
|
).select_related("workspace", "project", "member", "workspace__owner")
|
||||||
|
|
||||||
project_member_serializer = ProjectMemberSerializer(project_member, many=True)
|
project_member_serializer = ProjectMemberSerializer(
|
||||||
|
project_member, many=True
|
||||||
|
)
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
@ -966,7 +1051,11 @@ class WorkspaceThemeViewSet(BaseViewSet):
|
|||||||
serializer_class = WorkspaceThemeSerializer
|
serializer_class = WorkspaceThemeSerializer
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return super().get_queryset().filter(workspace__slug=self.kwargs.get("slug"))
|
return (
|
||||||
|
super()
|
||||||
|
.get_queryset()
|
||||||
|
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||||
|
)
|
||||||
|
|
||||||
def create(self, request, slug):
|
def create(self, request, slug):
|
||||||
workspace = Workspace.objects.get(slug=slug)
|
workspace = Workspace.objects.get(slug=slug)
|
||||||
@ -1229,12 +1318,22 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def get(self, request, slug, user_id):
|
def get(self, request, slug, user_id):
|
||||||
fields = [field for field in request.GET.get("fields", "").split(",") if field]
|
fields = [
|
||||||
|
field
|
||||||
|
for field in request.GET.get("fields", "").split(",")
|
||||||
|
if field
|
||||||
|
]
|
||||||
filters = issue_filters(request.query_params, "GET")
|
filters = issue_filters(request.query_params, "GET")
|
||||||
|
|
||||||
# Custom ordering for priority and state
|
# Custom ordering for priority and state
|
||||||
priority_order = ["urgent", "high", "medium", "low", "none"]
|
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")
|
order_by_param = request.GET.get("order_by", "-created_at")
|
||||||
issue_queryset = (
|
issue_queryset = (
|
||||||
@ -1246,21 +1345,9 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
|
|||||||
project__project_projectmember__member=request.user,
|
project__project_projectmember__member=request.user,
|
||||||
)
|
)
|
||||||
.filter(**filters)
|
.filter(**filters)
|
||||||
.annotate(
|
.select_related("workspace", "project", "state", "parent")
|
||||||
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
|
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||||
.order_by()
|
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
|
||||||
.values("count")
|
|
||||||
)
|
|
||||||
.select_related("project", "workspace", "state", "parent")
|
|
||||||
.prefetch_related("assignees", "labels")
|
|
||||||
.prefetch_related(
|
|
||||||
Prefetch(
|
|
||||||
"issue_reactions",
|
|
||||||
queryset=IssueReaction.objects.select_related("actor"),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.order_by("-created_at")
|
|
||||||
.annotate(
|
.annotate(
|
||||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||||
.order_by()
|
.order_by()
|
||||||
@ -1268,17 +1355,30 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
|
|||||||
.values("count")
|
.values("count")
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id"))
|
attachment_count=IssueAttachment.objects.filter(
|
||||||
|
issue=OuterRef("id")
|
||||||
|
)
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
)
|
)
|
||||||
|
.annotate(
|
||||||
|
sub_issues_count=Issue.issue_objects.filter(
|
||||||
|
parent=OuterRef("id")
|
||||||
|
)
|
||||||
|
.order_by()
|
||||||
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
|
.values("count")
|
||||||
|
)
|
||||||
|
.order_by("created_at")
|
||||||
).distinct()
|
).distinct()
|
||||||
|
|
||||||
# Priority Ordering
|
# Priority Ordering
|
||||||
if order_by_param == "priority" or order_by_param == "-priority":
|
if order_by_param == "priority" or order_by_param == "-priority":
|
||||||
priority_order = (
|
priority_order = (
|
||||||
priority_order 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(
|
issue_queryset = issue_queryset.annotate(
|
||||||
priority_order=Case(
|
priority_order=Case(
|
||||||
@ -1326,16 +1426,17 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
|
|||||||
else order_by_param
|
else order_by_param
|
||||||
)
|
)
|
||||||
).order_by(
|
).order_by(
|
||||||
"-max_values" if order_by_param.startswith("-") else "max_values"
|
"-max_values"
|
||||||
|
if order_by_param.startswith("-")
|
||||||
|
else "max_values"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
issue_queryset = issue_queryset.order_by(order_by_param)
|
issue_queryset = issue_queryset.order_by(order_by_param)
|
||||||
|
|
||||||
issues = IssueLiteSerializer(
|
issues = IssueSerializer(
|
||||||
issue_queryset, many=True, fields=fields if fields else None
|
issue_queryset, many=True, fields=fields if fields else None
|
||||||
).data
|
).data
|
||||||
issue_dict = {str(issue["id"]): issue for issue in issues}
|
return Response(issues, status=status.HTTP_200_OK)
|
||||||
return Response(issue_dict, status=status.HTTP_200_OK)
|
|
||||||
|
|
||||||
|
|
||||||
class WorkspaceLabelsEndpoint(BaseAPIView):
|
class WorkspaceLabelsEndpoint(BaseAPIView):
|
||||||
@ -1347,5 +1448,79 @@ class WorkspaceLabelsEndpoint(BaseAPIView):
|
|||||||
labels = Label.objects.filter(
|
labels = Label.objects.filter(
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
project__project_projectmember__member=request.user,
|
project__project_projectmember__member=request.user,
|
||||||
).values("parent", "name", "color", "id", "project_id", "workspace__slug")
|
)
|
||||||
return Response(labels, status=status.HTTP_200_OK)
|
serializer = LabelSerializer(labels, many=True).data
|
||||||
|
return Response(serializer, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
class WorkspaceStatesEndpoint(BaseAPIView):
|
||||||
|
permission_classes = [
|
||||||
|
WorkspaceEntityPermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
def get(self, request, slug):
|
||||||
|
states = State.objects.filter(
|
||||||
|
workspace__slug=slug,
|
||||||
|
project__project_projectmember__member=request.user,
|
||||||
|
)
|
||||||
|
serializer = StateSerializer(states, many=True).data
|
||||||
|
return Response(serializer, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
class WorkspaceEstimatesEndpoint(BaseAPIView):
|
||||||
|
permission_classes = [
|
||||||
|
WorkspaceEntityPermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
def get(self, request, slug):
|
||||||
|
estimate_ids = Project.objects.filter(
|
||||||
|
workspace__slug=slug, estimate__isnull=False
|
||||||
|
).values_list("estimate_id", flat=True)
|
||||||
|
estimates = Estimate.objects.filter(
|
||||||
|
pk__in=estimate_ids
|
||||||
|
).prefetch_related(
|
||||||
|
Prefetch(
|
||||||
|
"points",
|
||||||
|
queryset=EstimatePoint.objects.select_related(
|
||||||
|
"estimate", "workspace", "project"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
serializer = WorkspaceEstimateSerializer(estimates, many=True)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
class WorkspaceUserPropertiesEndpoint(BaseAPIView):
|
||||||
|
permission_classes = [
|
||||||
|
WorkspaceViewerPermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
def patch(self, request, slug):
|
||||||
|
workspace_properties = WorkspaceUserProperties.objects.get(
|
||||||
|
user=request.user,
|
||||||
|
workspace__slug=slug,
|
||||||
|
)
|
||||||
|
|
||||||
|
workspace_properties.filters = request.data.get(
|
||||||
|
"filters", workspace_properties.filters
|
||||||
|
)
|
||||||
|
workspace_properties.display_filters = request.data.get(
|
||||||
|
"display_filters", workspace_properties.display_filters
|
||||||
|
)
|
||||||
|
workspace_properties.display_properties = request.data.get(
|
||||||
|
"display_properties", workspace_properties.display_properties
|
||||||
|
)
|
||||||
|
workspace_properties.save()
|
||||||
|
|
||||||
|
serializer = WorkspaceUserPropertiesSerializer(workspace_properties)
|
||||||
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
def get(self, request, slug):
|
||||||
|
(
|
||||||
|
workspace_properties,
|
||||||
|
_,
|
||||||
|
) = WorkspaceUserProperties.objects.get_or_create(
|
||||||
|
user=request.user, workspace__slug=slug
|
||||||
|
)
|
||||||
|
serializer = WorkspaceUserPropertiesSerializer(workspace_properties)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user