Compare commits

..
Author SHA1 Message Date
NarayanBavisetti 2475097b4e Merge branch 'preview' of github.com:makeplane/plane into fix/external-api-context 2023-12-26 16:03:25 +05:30
M. PalanikannanandGitHub 1a2186cca4 [fix] Image insertion position at cursor position and Updated placeholder text. (#3224)
* Fix position bug in the UploadImagesPlugin widget decoration and adjust transaction to correctly insert image node and set meta data for image removal.

* Update CSS styles in editor.css to remove margin top and bottom on images and img placeholders and adjust the margin on the table in the editor container.

* Better typescript support for images extension.

Update the `Command` extension in `slash-commands.tsx` to include a `SlashCommandOptions` type for better TS support and allow spaces in the suggestion options and modify the `image` suggestion's search terms to include "img" in addition to "photo", "picture", and "media".
2023-12-22 14:10:42 +05:30
Lakhan BahetiandGitHub ac39bb9b1d chore: allow non-latin characters to project identifier (#3226) 2023-12-22 14:04:47 +05:30
Lakhan BahetiandGitHub b8b58d3acd style: email placeholder changed across the platform (#3206)
* style: email placeholder changed across the platform

* fix: placeholder text
2023-12-21 16:02:56 +05:30
M. PalanikannanandGitHub 75ca932682 [refactor] Editor code refactoring (#3194)
* removed relative imports from editor core

* Update issue widget file paths and imports to use kebab case instead of camel case, to align with coding conventions and improve consistency.

* Update Tiptap core and extensions versions to 2.1.13 and Tiptap React version to 2.1.13. Update Tiptap table imports to use the new location in package @tiptap/pm/tables. Update AlertLabel component to use the new type definition for LucideIcon.

* updated lock file

* removed default exports from editor/core

* fixed injecting css into the core package itself

* seperated css code to have single source of origin wrt to the package

* removed default imports from document editor

* all instances using index as key while mapping fixed

* Update Lite Text Editor package.json to remove @plane/editor-types as a dependency.

Update Lite Text Editor index.ts to update the import of IMentionSuggestion and IMentionHighlight from @plane/editor-types to @plane/editor-core.

Update Lite Text Editor ui/index.tsx to update the import of UploadImage, DeleteImage, IMentionSuggestion, and RestoreImage from @plane/editor-types to @plane/editor-core.

Update Lite Text Editor ui/menus/fixed-menu/index.tsx to update the import of UploadImage from @plane/editor-types to @plane/editor-core.

Update turbo.json to remove @plane/editor-types#build as a dependency for @plane/lite-text-editor#build, @plane/rich-text-editor#build, and @plane/document-editor#build.

* Remove deprecated import and adjust tippy.js usage in the slash-commands.tsx file of the editor extensions package.

* Update dependencies in `rich-text-editor/package.json`, remove `@plane/editor-types` and add `@plane/editor-core` in `rich-text-editor/src/index.ts`, and update imports in `rich-text-editor/src/ui/extensions/index.tsx` and `rich-text-editor/src/ui/index.tsx` to use `@plane/editor-core` instead of `@plane/editor-types`.

* Update package.json dependencies and add new types for image deletion, upload, restore, mention highlight, mention suggestion, and slash command item.

* Update import statements in various files to use the new package "@plane/editor-core" instead of "@plane/editor-types".

* fixed document editor to follow conventions

* Refactor imports in the Rich Text Editor package to use relative paths instead of absolute paths.

- Updated imports in `index.ts`, `ui/index.tsx`, and `ui/menus/bubble-menu/index.tsx` to use relative paths.
- Updated `tsconfig.json` to include the `baseUrl` compiler option and adjust the `include` and `exclude` paths.

* Refactor Lite Text Editor code to use relative import paths instead of absolute import paths.

* Added LucideIconType to the exports in index.ts for use in other files.
Created a new file lucide-icon.ts which contains the type LucideIconType.
Updated the icon type in HeadingOneItem in menu-items/index.tsx to use LucideIconType.
Updated the Icon type in AlertLabel in alert-label.tsx to use LucideIconType.
Updated the Icon type in VerticalDropdownItemProps in vertical-dropdown-menu.tsx to use LucideIconType.
Updated the Icon type in BubbleMenuItem in fixed-menu/index.tsx to use LucideIconType.
Deleted the file tooltip.tsx since it is no longer used.
Updated the Icon type in BubbleMenuItem in bubble-menu/index.tsx to use LucideIconType.

* ♻️ refactor: simplify rendering logic in slash-commands.tsx

The rendering logic in the file "slash-commands.tsx" has been simplified. Previously, the code used inline positioning for the popup, but it has now been removed. Instead of appending the popup to the document body, it is now appended to the element with the ID "tiptap-container". The "flip" option has also been removed. These changes have improved the readability and maintainability of the code.

* fixed build errors caused due to core's internal imports

* regression: fixed pages not saving issue and not duplicating with proper content issue

* build: Update @tiptap dependencies

Updated the @tiptap dependencies in the package.json files of `document-editor`, `extensions`, and `rich-text-editor` packages to version 2.1.13.

* 🚑 fix: Correct appendTo selector in slash-commands.tsx

Update the `appendTo` function call in `slash-commands.tsx` to use the correct selector `#editor-container` instead of `#tiptap-container`. This ensures that the component is appended to the appropriate container in the editor extension.

Note: The commit message assumes that the change is a fix for an issue or error. If it's not a fix, please provide more context so that an appropriate commit type can be determined.
2023-12-21 16:01:38 +05:30
Lakhan BahetiandGitHub be9c0be886 fix: bugs & improvements (#3189)
* fix: workspace invitation modal form values reset

* fix: profile sidebar avatar letter
2023-12-21 15:59:21 +05:30
Anmol Singh BhatiaandGitHub ee08feb59b chore: bug fixes & improvement (#3218)
* chore: draft issue validation added to prevent saving empty or whitespace title

* chore: resolve scrolling issue in page empty state

* chore: kanban layout quick add issue improvement
2023-12-21 15:56:02 +05:30
Prateek ShouryaandGitHub 22b0bb4615 style: instance admin email settings ui & ux update. (#3186) 2023-12-21 14:37:26 +05:30
1e464d8613 fix - file size limit not work on plane.settings.production (#3160)
* fix - file size limit not work on plane.settings.production

* fix - file size limit not work on plane.settings.production

* fix - file size limit not work on plane.settings.production, move to common.py

---------

Co-authored-by: luanduongtel4vn <hoangluan@tel4vn.com>
Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2023-12-21 14:26:44 +05:30
Manish GuptaandGitHub 3224dc4fe6 fix: branch build fix (#3214)
* branch build fix for release-* in case of space,backend,proxy

* fixes
2023-12-21 13:52:38 +05:30
NikhilandGitHub 7010448c34 dev: github importer (#3205)
* dev: initiate github import

* dev: github importer all issues import

* dev: github comments and links for the imported issues

* dev: update controller to use logger and spread the resultData in getAllEntities

* dev: removed console log

* dev: update code structure and sync functions

* dev: updated retry logic when exception

* dev: add imported data as well

* dev: update logger and repo fetch

* dev: update jira integration to new structure

* dev: update migrations

* dev: update the reason field
2023-12-20 20:33:51 +05:30
1cc18a0915 dev: segway intgegration (#3132)
* feat: implemented rabbitmq

* dev: initialize segway with queue setup

* dev: import refactors

* dev: create communication with the segway server

* dev: create new workers

* dev: create celery node queue for consuming messages from django

* dev: node to celery connection

* dev: setup segway and django connection

* dev: refactor the structure and add database integration to the app

* dev: add external id and source added

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
2023-12-20 19:24:22 +05:30
sriram veeraghanta a04ad4c4e2 Merge branch 'preview' of github.com:makeplane/plane into release-0.15 2023-12-20 19:23:24 +05:30
1623 changed files with 61259 additions and 67961 deletions
+6 -4
View File
@@ -1,12 +1,14 @@
# Database Settings # Database Settings
POSTGRES_USER="plane" PGUSER="plane"
POSTGRES_PASSWORD="plane" PGPASSWORD="plane"
POSTGRES_DB="plane" PGHOST="plane-db"
PGDATA="/var/lib/postgresql/data" PGDATABASE="plane"
DATABASE_URL=postgresql://${PGUSER}:${PGPASSWORD}@${PGHOST}/${PGDATABASE}
# Redis Settings # Redis Settings
REDIS_HOST="plane-redis" REDIS_HOST="plane-redis"
REDIS_PORT="6379" REDIS_PORT="6379"
REDIS_URL="redis://${REDIS_HOST}:6379/"
# AWS Settings # AWS Settings
AWS_REGION="" AWS_REGION=""
+1 -2
View File
@@ -1,8 +1,7 @@
name: Bug report name: Bug report
description: Create a bug report to help us improve Plane description: Create a bug report to help us improve Plane
title: "[bug]: " title: "[bug]: "
labels: [🐛bug] labels: [bug, need testing]
assignees: [srinivaspendem, pushya-plane]
body: body:
- type: markdown - type: markdown
attributes: attributes:
@@ -1,8 +1,7 @@
name: Feature request name: Feature request
description: Suggest a feature to improve Plane description: Suggest a feature to improve Plane
title: "[feature]: " title: "[feature]: "
labels: [feature] labels: [feature]
assignees: [srinivaspendem, pushya-plane]
body: body:
- type: markdown - type: markdown
attributes: attributes:
+83 -74
View File
@@ -1,30 +1,61 @@
name: Branch Build name: Branch Build
on: on:
workflow_dispatch: pull_request:
inputs: types:
branch_name: - closed
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: ${{ inputs.branch_name || github.ref_name || github.event.release.target_commitish }} TARGET_BRANCH: ${{ github.event.pull_request.base.ref || 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 }}
@@ -32,38 +63,33 @@ 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:${{ needs.branch_build_setup.outputs.gh_branch_name }} FRONTEND_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend${{ secrets.DOCKER_REPO_SUFFIX || '' }}:${{ 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:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend:${{ github.event.release.tag_name }} 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 }}
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:stable TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend${{ secrets.DOCKER_REPO_SUFFIX || '' }}: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@v3.0.0 uses: docker/setup-buildx-action@v2.5.0
with:
platforms: linux/amd64,linux/arm64
buildkitd-flags: "--allow-insecure-entitlement security.insecure"
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v3.0.0 uses: docker/login-action@v2.1.0
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Downloading Web Source Code
- name: Check out the repo uses: actions/download-artifact@v3
uses: actions/checkout@v4.1.1 with:
name: web-src-code
- name: Build and Push Frontend to Docker Container Registry - name: Build and Push Frontend to Docker Container Registry
uses: docker/build-push-action@v5.1.0 uses: docker/build-push-action@v4.0.0
with: with:
context: . context: .
file: ./web/Dockerfile.web file: ./web/Dockerfile.web
@@ -79,39 +105,33 @@ 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:${{ needs.branch_build_setup.outputs.gh_branch_name }} SPACE_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-space${{ secrets.DOCKER_REPO_SUFFIX || '' }}:${{ 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:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-space:${{ github.event.release.tag_name }} 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 }}
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:stable TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-space${{ secrets.DOCKER_REPO_SUFFIX || '' }}: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@v3.0.0 uses: docker/setup-buildx-action@v2.5.0
with:
platforms: linux/amd64,linux/arm64
buildkitd-flags: "--allow-insecure-entitlement security.insecure"
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v3.0.0 uses: docker/login-action@v2.1.0
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Downloading Space Source Code
- name: Check out the repo uses: actions/download-artifact@v3
uses: actions/checkout@v4.1.1 with:
name: space-src-code
- name: Build and Push Space to Docker Hub - name: Build and Push Space to Docker Hub
uses: docker/build-push-action@v5.1.0 uses: docker/build-push-action@v4.0.0
with: with:
context: . context: .
file: ./space/Dockerfile.space file: ./space/Dockerfile.space
@@ -127,42 +147,36 @@ 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:${{ needs.branch_build_setup.outputs.gh_branch_name }} BACKEND_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-backend${{ secrets.DOCKER_REPO_SUFFIX || '' }}:${{ 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:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-backend:${{ github.event.release.tag_name }} 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 }}
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:stable TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-backend${{ secrets.DOCKER_REPO_SUFFIX || '' }}: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@v3.0.0 uses: docker/setup-buildx-action@v2.5.0
with:
platforms: linux/amd64,linux/arm64
buildkitd-flags: "--allow-insecure-entitlement security.insecure"
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v3.0.0 uses: docker/login-action@v2.1.0
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Downloading Backend Source Code
- name: Check out the repo uses: actions/download-artifact@v3
uses: actions/checkout@v4.1.1 with:
name: backend-src-code
- name: Build and Push Backend to Docker Hub - name: Build and Push Backend to Docker Hub
uses: docker/build-push-action@v5.1.0 uses: docker/build-push-action@v4.0.0
with: with:
context: ./apiserver context: .
file: ./apiserver/Dockerfile.api file: ./Dockerfile.api
platforms: linux/amd64 platforms: linux/amd64
push: true push: true
tags: ${{ env.BACKEND_TAG }} tags: ${{ env.BACKEND_TAG }}
@@ -175,42 +189,37 @@ 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:${{ needs.branch_build_setup.outputs.gh_branch_name }} PROXY_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy${{ secrets.DOCKER_REPO_SUFFIX || '' }}:${{ 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:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy:${{ github.event.release.tag_name }} 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 }}
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:stable TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy${{ secrets.DOCKER_REPO_SUFFIX || '' }}: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@v3.0.0 uses: docker/setup-buildx-action@v2.5.0
with:
platforms: linux/amd64,linux/arm64
buildkitd-flags: "--allow-insecure-entitlement security.insecure"
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v3.0.0 uses: docker/login-action@v2.1.0
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Check out the repo - name: Downloading Proxy Source Code
uses: actions/checkout@v4.1.1 uses: actions/download-artifact@v3
with:
name: proxy-src-code
- name: Build and Push Plane-Proxy to Docker Hub - name: Build and Push Plane-Proxy to Docker Hub
uses: docker/build-push-action@v5.1.0 uses: docker/build-push-action@v4.0.0
with: with:
context: ./nginx context: .
file: ./nginx/Dockerfile file: ./Dockerfile
platforms: linux/amd64 platforms: linux/amd64
tags: ${{ env.PROXY_TAG }} tags: ${{ env.PROXY_TAG }}
push: true push: true
@@ -14,18 +14,16 @@ jobs:
steps: steps:
- name: Checkout Repository to Actions - name: Checkout Repository to Actions
uses: actions/checkout@v3.3.0 uses: actions/checkout@v3.3.0
with:
token: ${{ secrets.ACCESS_TOKEN }}
- name: Setup Node.js 18.x - name: Setup Node.js 18.x
uses: actions/setup-node@v2 uses: actions/setup-node@v2
with: with:
node-version: 18.x node-version: 18.x
cache: "yarn" cache: 'yarn'
- name: Get changed files - name: Get changed files
id: changed-files id: changed-files
uses: tj-actions/changed-files@v41 uses: tj-actions/changed-files@v38
with: with:
files_yaml: | files_yaml: |
apiserver: apiserver:
@@ -46,3 +44,5 @@ jobs:
run: | run: |
yarn yarn
yarn build --filter=space yarn build --filter=space
+2 -2
View File
@@ -2,10 +2,10 @@ name: "CodeQL"
on: on:
push: push:
branches: [ 'develop', 'preview', 'master' ] branches: [ 'develop', 'hot-fix', 'stage-release' ]
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', 'preview', 'master' ] branches: [ 'develop' ]
schedule: schedule:
- cron: '53 19 * * 5' - cron: '53 19 * * 5'
+8 -6
View File
@@ -1,23 +1,25 @@
name: Create Sync Action name: Create Sync Action
on: on:
workflow_dispatch: pull_request:
push:
branches: branches:
- preview - preview
types:
- closed
env: env:
SOURCE_BRANCH_NAME: ${{ github.ref_name }} SOURCE_BRANCH_NAME: ${{github.event.pull_request.base.ref}}
jobs: jobs:
sync_changes: create_pr:
# Only run the job when a PR is merged
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
pull-requests: write pull-requests: write
contents: read contents: read
steps: steps:
- name: Checkout Code - name: Checkout Code
uses: actions/checkout@v4.1.1 uses: actions/checkout@v2
with: with:
persist-credentials: false persist-credentials: false
fetch-depth: 0 fetch-depth: 0
+3
View File
@@ -80,3 +80,6 @@ tmp/
## packages ## packages
dist dist
.temp/ .temp/
# logs
combined.log
+2 -2
View File
@@ -37,7 +37,7 @@ Meet [Plane](https://plane.so). An open-source software development tool to mana
> Plane is still in its early days, not everything will be perfect yet, and hiccups may happen. Please let us know of any suggestions, ideas, or bugs that you encounter on our [Discord](https://discord.com/invite/A92xrEGCge) or GitHub issues, and we will use your feedback to improve on our upcoming releases. > Plane is still in its early days, not everything will be perfect yet, and hiccups may happen. Please let us know of any suggestions, ideas, or bugs that you encounter on our [Discord](https://discord.com/invite/A92xrEGCge) or GitHub issues, and we will use your feedback to improve on our upcoming releases.
The easiest way to get started with Plane is by creating a [Plane Cloud](https://app.plane.so) account. Plane Cloud offers a hosted solution for Plane. If you prefer to self-host Plane, please refer to our [deployment documentation](https://docs.plane.so/docker-compose). The easiest way to get started with Plane is by creating a [Plane Cloud](https://app.plane.so) account. Plane Cloud offers a hosted solution for Plane. If you prefer to self-host Plane, please refer to our [deployment documentation](https://docs.plane.so/self-hosting/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/docker-compose) documentation page For self hosting environment setup, visit the [Self Hosting](https://docs.plane.so/self-hosting/docker-compose) documentation page
## 🚀 Features ## 🚀 Features
+8 -5
View File
@@ -8,11 +8,11 @@ SENTRY_DSN=""
SENTRY_ENVIRONMENT="development" SENTRY_ENVIRONMENT="development"
# Database Settings # Database Settings
POSTGRES_USER="plane" PGUSER="plane"
POSTGRES_PASSWORD="plane" PGPASSWORD="plane"
POSTGRES_HOST="plane-db" PGHOST="plane-db"
POSTGRES_DB="plane" PGDATABASE="plane"
DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}/${POSTGRES_DB} DATABASE_URL=postgresql://${PGUSER}:${PGPASSWORD}@${PGHOST}/${PGDATABASE}
# Oauth variables # Oauth variables
GOOGLE_CLIENT_ID="" GOOGLE_CLIENT_ID=""
@@ -39,6 +39,9 @@ OPENAI_API_BASE="https://api.openai.com/v1" # deprecated
OPENAI_API_KEY="sk-" # deprecated OPENAI_API_KEY="sk-" # deprecated
GPT_ENGINE="gpt-3.5-turbo" # deprecated GPT_ENGINE="gpt-3.5-turbo" # deprecated
# Github
GITHUB_CLIENT_SECRET="" # For fetching release notes
# Settings related to Docker # Settings related to Docker
DOCKERIZED=1 # deprecated DOCKERIZED=1 # deprecated
-1
View File
@@ -41,7 +41,6 @@ USER captain
# Add in Django deps and generate Django's static files # Add in Django deps and generate Django's static files
COPY manage.py manage.py COPY manage.py manage.py
COPY server.py server.py
COPY plane plane/ COPY plane plane/
COPY templates templates/ COPY templates templates/
COPY package.json package.json COPY package.json package.json
+8 -3
View File
@@ -33,10 +33,15 @@ RUN pip install -r requirements/local.txt --compile --no-cache-dir
RUN addgroup -S plane && \ RUN addgroup -S plane && \
adduser -S captain -G plane adduser -S captain -G plane
COPY . . RUN chown captain.plane /code
RUN chown -R captain.plane /code USER captain
RUN chmod -R +x /code/bin
# Add in Django deps and generate Django's static files
USER root
# RUN chmod +x ./bin/takeoff ./bin/worker ./bin/beat
RUN chmod -R 777 /code RUN chmod -R 777 /code
USER captain USER captain
+1 -1
View File
@@ -1,3 +1,3 @@
web: gunicorn -w 4 -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:$PORT --max-requests 10000 --max-requests-jitter 1000 --access-logfile - web: gunicorn -w 4 -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:$PORT --max-requests 10000 --max-requests-jitter 1000 --access-logfile -
worker: celery -A plane worker -l info worker: celery -A plane worker -l info -Q internal_tasks,external_tasks
beat: celery -A plane beat -l INFO beat: celery -A plane beat -l INFO
+7 -21
View File
@@ -26,9 +26,7 @@ def update_description():
updated_issues.append(issue) updated_issues.append(issue)
Issue.objects.bulk_update( Issue.objects.bulk_update(
updated_issues, updated_issues, ["description_html", "description_stripped"], batch_size=100
["description_html", "description_stripped"],
batch_size=100,
) )
print("Success") print("Success")
except Exception as e: except Exception as e:
@@ -42,9 +40,7 @@ def update_comments():
updated_issue_comments = [] updated_issue_comments = []
for issue_comment in issue_comments: for issue_comment in issue_comments:
issue_comment.comment_html = ( issue_comment.comment_html = f"<p>{issue_comment.comment_stripped}</p>"
f"<p>{issue_comment.comment_stripped}</p>"
)
updated_issue_comments.append(issue_comment) updated_issue_comments.append(issue_comment)
IssueComment.objects.bulk_update( IssueComment.objects.bulk_update(
@@ -103,9 +99,7 @@ def updated_issue_sort_order():
issue.sort_order = issue.sequence_id * random.randint(100, 500) issue.sort_order = issue.sequence_id * random.randint(100, 500)
updated_issues.append(issue) updated_issues.append(issue)
Issue.objects.bulk_update( Issue.objects.bulk_update(updated_issues, ["sort_order"], batch_size=100)
updated_issues, ["sort_order"], batch_size=100
)
print("Success") print("Success")
except Exception as e: except Exception as e:
print(e) print(e)
@@ -143,9 +137,7 @@ def update_project_cover_images():
project.cover_image = project_cover_images[random.randint(0, 19)] project.cover_image = project_cover_images[random.randint(0, 19)]
updated_projects.append(project) updated_projects.append(project)
Project.objects.bulk_update( Project.objects.bulk_update(updated_projects, ["cover_image"], batch_size=100)
updated_projects, ["cover_image"], batch_size=100
)
print("Success") print("Success")
except Exception as e: except Exception as e:
print(e) print(e)
@@ -194,9 +186,7 @@ def update_label_color():
def create_slack_integration(): def create_slack_integration():
try: try:
_ = Integration.objects.create( _ = Integration.objects.create(provider="slack", network=2, title="Slack")
provider="slack", network=2, title="Slack"
)
print("Success") print("Success")
except Exception as e: except Exception as e:
print(e) print(e)
@@ -222,16 +212,12 @@ def update_integration_verified():
def update_start_date(): def update_start_date():
try: try:
issues = Issue.objects.filter( issues = Issue.objects.filter(state__group__in=["started", "completed"])
state__group__in=["started", "completed"]
)
updated_issues = [] updated_issues = []
for issue in issues: for issue in issues:
issue.start_date = issue.created_at.date() issue.start_date = issue.created_at.date()
updated_issues.append(issue) updated_issues.append(issue)
Issue.objects.bulk_update( Issue.objects.bulk_update(updated_issues, ["start_date"], batch_size=500)
updated_issues, ["start_date"], batch_size=500
)
print("Success") print("Success")
except Exception as e: except Exception as e:
print(e) print(e)
Executable → Regular
-3
View File
@@ -2,7 +2,4 @@
set -e set -e
python manage.py wait_for_db python manage.py wait_for_db
# Wait for migrations
python manage.py wait_for_migrations
# Run the processes
celery -A plane beat -l info celery -A plane beat -l info
+2 -3
View File
@@ -1,8 +1,7 @@
#!/bin/bash #!/bin/bash
set -e set -e
python manage.py wait_for_db python manage.py wait_for_db
# Wait for migrations python manage.py migrate
python manage.py wait_for_migrations
# Create the default bucket # Create the default bucket
#!/bin/bash #!/bin/bash
@@ -28,4 +27,4 @@ python manage.py configure_instance
# Create the default bucket # Create the default bucket
python manage.py create_bucket python manage.py create_bucket
python server.py exec gunicorn -w $GUNICORN_WORKERS -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:${PORT:-8000} --max-requests 1200 --max-requests-jitter 1000 --access-logfile -
+1 -2
View File
@@ -1,8 +1,7 @@
#!/bin/bash #!/bin/bash
set -e set -e
python manage.py wait_for_db python manage.py wait_for_db
# Wait for migrations python manage.py migrate
python manage.py wait_for_migrations
# Create the default bucket # Create the default bucket
#!/bin/bash #!/bin/bash
-3
View File
@@ -2,7 +2,4 @@
set -e set -e
python manage.py wait_for_db python manage.py wait_for_db
# Wait for migrations
python manage.py wait_for_migrations
# Run the processes
celery -A plane worker -l info celery -A plane worker -l info
+3 -3
View File
@@ -2,10 +2,10 @@
import os import os
import sys import sys
if __name__ == "__main__": if __name__ == '__main__':
os.environ.setdefault( os.environ.setdefault(
"DJANGO_SETTINGS_MODULE", "plane.settings.production" 'DJANGO_SETTINGS_MODULE',
) 'plane.settings.production')
try: try:
from django.core.management import execute_from_command_line from django.core.management import execute_from_command_line
except ImportError as exc: except ImportError as exc:
+1 -1
View File
@@ -1,4 +1,4 @@
{ {
"name": "plane-api", "name": "plane-api",
"version": "0.15.0" "version": "0.14.0"
} }
+1 -1
View File
@@ -1,3 +1,3 @@
from .celery import app as celery_app from .celery import app as celery_app
__all__ = ("celery_app",) __all__ = ('celery_app',)
+1 -1
View File
@@ -2,4 +2,4 @@ from django.apps import AppConfig
class AnalyticsConfig(AppConfig): class AnalyticsConfig(AppConfig):
name = "plane.analytics" name = 'plane.analytics'
@@ -25,10 +25,7 @@ class APIKeyAuthentication(authentication.BaseAuthentication):
def validate_api_token(self, token): def validate_api_token(self, token):
try: try:
api_token = APIToken.objects.get( api_token = APIToken.objects.get(
Q( Q(Q(expired_at__gt=timezone.now()) | Q(expired_at__isnull=True)),
Q(expired_at__gt=timezone.now())
| Q(expired_at__isnull=True)
),
token=token, token=token,
is_active=True, is_active=True,
) )
+6 -7
View File
@@ -1,18 +1,17 @@
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)
@@ -36,7 +35,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
+1 -5
View File
@@ -13,9 +13,5 @@ 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 ( from .module import ModuleSerializer, ModuleIssueSerializer, ModuleLiteSerializer
ModuleSerializer,
ModuleIssueSerializer,
ModuleLiteSerializer,
)
from .inbox import InboxIssueSerializer from .inbox import InboxIssueSerializer
+2 -4
View File
@@ -97,11 +97,9 @@ class BaseSerializer(serializers.ModelSerializer):
exp_serializer = expansion[expand]( exp_serializer = expansion[expand](
getattr(instance, expand) getattr(instance, expand)
) )
response[expand] = exp_serializer.data response[expand] = exp_serializer.data
else: else:
# You might need to handle this case differently # You might need to handle this case differently
response[expand] = getattr( response[expand] = getattr(instance, f"{expand}_id", None)
instance, f"{expand}_id", None
)
return response return response
+2 -3
View File
@@ -23,9 +23,7 @@ class CycleSerializer(BaseSerializer):
and data.get("end_date", None) is not None and data.get("end_date", None) is not None
and data.get("start_date", None) > data.get("end_date", None) and data.get("start_date", None) > data.get("end_date", None)
): ):
raise serializers.ValidationError( raise serializers.ValidationError("Start date cannot exceed end date")
"Start date cannot exceed end date"
)
return data return data
class Meta: class Meta:
@@ -57,6 +55,7 @@ class CycleIssueSerializer(BaseSerializer):
class CycleLiteSerializer(BaseSerializer): class CycleLiteSerializer(BaseSerializer):
class Meta: class Meta:
model = Cycle model = Cycle
fields = "__all__" fields = "__all__"
+1 -1
View File
@@ -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__"
+11 -20
View File
@@ -27,7 +27,6 @@ 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(
@@ -67,14 +66,12 @@ class IssueSerializer(BaseSerializer):
and data.get("target_date", None) is not None and data.get("target_date", None) is not None
and data.get("start_date", None) > data.get("target_date", None) and data.get("start_date", None) > data.get("target_date", None)
): ):
raise serializers.ValidationError( raise serializers.ValidationError("Start date cannot exceed target date")
"Start date cannot exceed target date"
)
try: 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:
@@ -99,8 +96,7 @@ class IssueSerializer(BaseSerializer):
if ( if (
data.get("state") data.get("state")
and not State.objects.filter( and not State.objects.filter(
project_id=self.context.get("project_id"), project_id=self.context.get("project_id"), pk=data.get("state")
pk=data.get("state").id,
).exists() ).exists()
): ):
raise serializers.ValidationError( raise serializers.ValidationError(
@@ -111,8 +107,7 @@ class IssueSerializer(BaseSerializer):
if ( if (
data.get("parent") data.get("parent")
and not Issue.objects.filter( and not Issue.objects.filter(
workspace_id=self.context.get("workspace_id"), workspace_id=self.context.get("workspace_id"), pk=data.get("parent")
pk=data.get("parent").id,
).exists() ).exists()
): ):
raise serializers.ValidationError( raise serializers.ValidationError(
@@ -243,13 +238,9 @@ class IssueSerializer(BaseSerializer):
] ]
if "labels" in self.fields: if "labels" in self.fields:
if "labels" in self.expand: if "labels" in self.expand:
data["labels"] = LabelSerializer( data["labels"] = LabelSerializer(instance.labels.all(), many=True).data
instance.labels.all(), many=True
).data
else: else:
data["labels"] = [ data["labels"] = [str(label.id) for label in instance.labels.all()]
str(label.id) for label in instance.labels.all()
]
return data return data
@@ -287,8 +278,7 @@ class IssueLinkSerializer(BaseSerializer):
# Validation if url already exists # Validation if url already exists
def create(self, validated_data): def create(self, validated_data):
if IssueLink.objects.filter( if IssueLink.objects.filter(
url=validated_data.get("url"), url=validated_data.get("url"), issue_id=validated_data.get("issue_id")
issue_id=validated_data.get("issue_id"),
).exists(): ).exists():
raise serializers.ValidationError( raise serializers.ValidationError(
{"error": "URL already exists for this Issue"} {"error": "URL already exists for this Issue"}
@@ -334,9 +324,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:
@@ -372,6 +362,7 @@ class ModuleIssueSerializer(BaseSerializer):
class LabelLiteSerializer(BaseSerializer): class LabelLiteSerializer(BaseSerializer):
class Meta: class Meta:
model = Label model = Label
fields = [ fields = [
+10 -12
View File
@@ -52,9 +52,7 @@ class ModuleSerializer(BaseSerializer):
and data.get("target_date", None) is not None and data.get("target_date", None) is not None
and data.get("start_date", None) > data.get("target_date", None) and data.get("start_date", None) > data.get("target_date", None)
): ):
raise serializers.ValidationError( raise serializers.ValidationError("Start date cannot exceed target date")
"Start date cannot exceed target date"
)
if data.get("members", []): if data.get("members", []):
data["members"] = ProjectMember.objects.filter( data["members"] = ProjectMember.objects.filter(
@@ -67,18 +65,18 @@ class ModuleSerializer(BaseSerializer):
def create(self, validated_data): def create(self, validated_data):
members = validated_data.pop("members", None) members = validated_data.pop("members", None)
project_id = self.context["project_id"] project = self.context["project"]
workspace_id = self.context["workspace_id"]
module = Module.objects.create(**validated_data, project=project)
module = Module.objects.create(**validated_data, project_id=project_id)
if members is not None: if members is not None:
ModuleMember.objects.bulk_create( ModuleMember.objects.bulk_create(
[ [
ModuleMember( ModuleMember(
module=module, module=module,
member_id=str(member), member=member,
project_id=project_id, project=project,
workspace_id=workspace_id, workspace=project.workspace,
created_by=module.created_by, created_by=module.created_by,
updated_by=module.updated_by, updated_by=module.updated_by,
) )
@@ -99,7 +97,7 @@ class ModuleSerializer(BaseSerializer):
[ [
ModuleMember( ModuleMember(
module=instance, module=instance,
member_id=str(member), member=member,
project=instance.project, project=instance.project,
workspace=instance.project.workspace, workspace=instance.project.workspace,
created_by=instance.created_by, created_by=instance.created_by,
@@ -148,8 +146,7 @@ class ModuleLinkSerializer(BaseSerializer):
# Validation if url already exists # Validation if url already exists
def create(self, validated_data): def create(self, validated_data):
if ModuleLink.objects.filter( if ModuleLink.objects.filter(
url=validated_data.get("url"), url=validated_data.get("url"), module_id=validated_data.get("module_id")
module_id=validated_data.get("module_id"),
).exists(): ).exists():
raise serializers.ValidationError( raise serializers.ValidationError(
{"error": "URL already exists for this Issue"} {"error": "URL already exists for this Issue"}
@@ -158,6 +155,7 @@ class ModuleLinkSerializer(BaseSerializer):
class ModuleLiteSerializer(BaseSerializer): class ModuleLiteSerializer(BaseSerializer):
class Meta: class Meta:
model = Module model = Module
fields = "__all__" fields = "__all__"
+5 -14
View File
@@ -2,17 +2,12 @@
from rest_framework import serializers from rest_framework import serializers
# Module imports # Module imports
from plane.db.models import ( from plane.db.models import Project, ProjectIdentifier, WorkspaceMember, State, Estimate
Project,
ProjectIdentifier,
WorkspaceMember,
State,
Estimate,
)
from .base import BaseSerializer from .base import BaseSerializer
class ProjectSerializer(BaseSerializer): class ProjectSerializer(BaseSerializer):
total_members = serializers.IntegerField(read_only=True) total_members = serializers.IntegerField(read_only=True)
total_cycles = serializers.IntegerField(read_only=True) total_cycles = serializers.IntegerField(read_only=True)
total_modules = serializers.IntegerField(read_only=True) total_modules = serializers.IntegerField(read_only=True)
@@ -26,7 +21,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",
@@ -64,16 +59,12 @@ class ProjectSerializer(BaseSerializer):
def create(self, validated_data): def create(self, validated_data):
identifier = validated_data.get("identifier", "").strip().upper() identifier = validated_data.get("identifier", "").strip().upper()
if identifier == "": if identifier == "":
raise serializers.ValidationError( raise serializers.ValidationError(detail="Project Identifier is required")
detail="Project Identifier is required"
)
if ProjectIdentifier.objects.filter( if ProjectIdentifier.objects.filter(
name=identifier, workspace_id=self.context["workspace_id"] name=identifier, workspace_id=self.context["workspace_id"]
).exists(): ).exists():
raise serializers.ValidationError( raise serializers.ValidationError(detail="Project Identifier is taken")
detail="Project Identifier is taken"
)
project = Project.objects.create( project = Project.objects.create(
**validated_data, workspace_id=self.context["workspace_id"] **validated_data, workspace_id=self.context["workspace_id"]
+3 -3
View File
@@ -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( State.objects.filter(project_id=self.context.get("project_id")).update(
project_id=self.context.get("project_id") default=False
).update(default=False) )
return data return data
class Meta: class Meta:
@@ -5,7 +5,6 @@ from .base import BaseSerializer
class WorkspaceLiteSerializer(BaseSerializer): class WorkspaceLiteSerializer(BaseSerializer):
"""Lite serializer with only required fields""" """Lite serializer with only required fields"""
class Meta: class Meta:
model = Workspace model = Workspace
fields = [ fields = [
+1 -1
View File
@@ -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",
+7 -14
View File
@@ -41,9 +41,7 @@ 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( response = super().finalize_response(request, response, *args, **kwargs)
request, response, *args, **kwargs
)
# Check for the case should webhook be sent # Check for the case should webhook be sent
if ( if (
@@ -106,14 +104,15 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
) )
if isinstance(e, ObjectDoesNotExist): if isinstance(e, ObjectDoesNotExist):
model_name = str(exc).split(" matching query does not exist.")[0]
return Response( return Response(
{"error": f"The required object does not exist."}, {"error": f"{model_name} does not exist."},
status=status.HTTP_404_NOT_FOUND, status=status.HTTP_404_NOT_FOUND,
) )
if isinstance(e, KeyError): if isinstance(e, KeyError):
return Response( return Response(
{"error": f" The required key does not exist."}, {"error": f"key {e} does not exist"},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
@@ -141,9 +140,7 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
def finalize_response(self, request, response, *args, **kwargs): def finalize_response(self, request, response, *args, **kwargs):
# Call super to get the default response # Call super to get the default response
response = super().finalize_response( response = super().finalize_response(request, response, *args, **kwargs)
request, response, *args, **kwargs
)
# Add custom headers if they exist in the request META # Add custom headers if they exist in the request META
ratelimit_remaining = request.META.get("X-RateLimit-Remaining") ratelimit_remaining = request.META.get("X-RateLimit-Remaining")
@@ -167,17 +164,13 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
@property @property
def fields(self): def fields(self):
fields = [ fields = [
field field for field in self.request.GET.get("fields", "").split(",") if field
for field in self.request.GET.get("fields", "").split(",")
if field
] ]
return fields if fields else None return fields if fields else None
@property @property
def expand(self): def expand(self):
expand = [ expand = [
expand expand for expand in self.request.GET.get("expand", "").split(",") if expand
for expand in self.request.GET.get("expand", "").split(",")
if expand
] ]
return expand if expand else None return expand if expand else None
+58 -129
View File
@@ -12,13 +12,7 @@ from rest_framework import status
# Module imports # Module imports
from .base import BaseAPIView, WebhookMixin from .base import BaseAPIView, WebhookMixin
from plane.db.models import ( from plane.db.models import Cycle, Issue, CycleIssue, IssueLink, IssueAttachment
Cycle,
Issue,
CycleIssue,
IssueLink,
IssueAttachment,
)
from plane.app.permissions import ProjectEntityPermission from plane.app.permissions import ProjectEntityPermission
from plane.api.serializers import ( from plane.api.serializers import (
CycleSerializer, CycleSerializer,
@@ -108,9 +102,7 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView):
), ),
) )
) )
.annotate( .annotate(total_estimates=Sum("issue_cycle__issue__estimate_point"))
total_estimates=Sum("issue_cycle__issue__estimate_point")
)
.annotate( .annotate(
completed_estimates=Sum( completed_estimates=Sum(
"issue_cycle__issue__estimate_point", "issue_cycle__issue__estimate_point",
@@ -209,8 +201,7 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView):
# Incomplete Cycles # Incomplete Cycles
if cycle_view == "incomplete": if cycle_view == "incomplete":
queryset = queryset.filter( queryset = queryset.filter(
Q(end_date__gte=timezone.now().date()) Q(end_date__gte=timezone.now().date()) | Q(end_date__isnull=True),
| Q(end_date__isnull=True),
) )
return self.paginate( return self.paginate(
request=request, request=request,
@@ -243,39 +234,12 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView):
): ):
serializer = CycleSerializer(data=request.data) serializer = CycleSerializer(data=request.data)
if serializer.is_valid(): if serializer.is_valid():
if (
request.data.get("external_id")
and request.data.get("external_source")
and Cycle.objects.filter(
project_id=project_id,
workspace__slug=slug,
external_source=request.data.get("external_source"),
external_id=request.data.get("external_id"),
).exists()
):
cycle = Cycle.objects.filter(
workspace__slug=slug,
project_id=project_id,
external_source=request.data.get("external_source"),
external_id=request.data.get("external_id"),
).first()
return Response(
{
"error": "Cycle with the same external id and external source already exists",
"id": str(cycle.id),
},
status=status.HTTP_409_CONFLICT,
)
serializer.save( serializer.save(
project_id=project_id, project_id=project_id,
owned_by=request.user, owned_by=request.user,
) )
return Response( return Response(serializer.data, status=status.HTTP_201_CREATED)
serializer.data, status=status.HTTP_201_CREATED return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
)
return Response(
serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
else: else:
return Response( return Response(
{ {
@@ -285,22 +249,15 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView):
) )
def patch(self, request, slug, project_id, pk): def patch(self, request, slug, project_id, pk):
cycle = Cycle.objects.get( cycle = Cycle.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
workspace__slug=slug, project_id=project_id, pk=pk
)
request_data = request.data request_data = request.data
if ( if cycle.end_date is not None and cycle.end_date < timezone.now().date():
cycle.end_date is not None
and cycle.end_date < timezone.now().date()
):
if "sort_order" in request_data: if "sort_order" in request_data:
# Can only change sort order # Can only change sort order
request_data = { request_data = {
"sort_order": request_data.get( "sort_order": request_data.get("sort_order", cycle.sort_order)
"sort_order", cycle.sort_order
)
} }
else: else:
return Response( return Response(
@@ -312,51 +269,34 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView):
serializer = CycleSerializer(cycle, data=request.data, partial=True) serializer = CycleSerializer(cycle, data=request.data, partial=True)
if serializer.is_valid(): if serializer.is_valid():
if (
request.data.get("external_id")
and (cycle.external_id != request.data.get("external_id"))
and Cycle.objects.filter(
project_id=project_id,
workspace__slug=slug,
external_source=request.data.get("external_source", cycle.external_source),
external_id=request.data.get("external_id"),
).exists()
):
return Response(
{
"error": "Cycle with the same external id and external source already exists",
"id": str(cycle.id),
},
status=status.HTTP_409_CONFLICT,
)
serializer.save() serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, slug, project_id, pk): def delete(self, request, slug, project_id, pk):
cycle_issues = list( cycle_issues = list(
CycleIssue.objects.filter( CycleIssue.objects.filter(cycle_id=self.kwargs.get("pk")).values_list(
cycle_id=self.kwargs.get("pk") "issue", flat=True
).values_list("issue", flat=True) )
)
cycle = Cycle.objects.get(
workspace__slug=slug, project_id=project_id, pk=pk
) )
cycle = Cycle.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
issue_activity.delay( issue_activity.apply_async(
type="cycle.activity.deleted", args=[],
requested_data=json.dumps( kwargs={
{ 'type': "cycle.activity.deleted",
'requested_data': json.dumps({
"cycle_id": str(pk), "cycle_id": str(pk),
"cycle_name": str(cycle.name), "cycle_name": str(cycle.name),
"issues": [str(issue_id) for issue_id in cycle_issues], "issues": [str(issue_id) for issue_id in cycle_issues],
} }),
), 'actor_id': str(request.user.id),
actor_id=str(request.user.id), 'issue_id': None,
issue_id=None, 'project_id': str(project_id),
project_id=str(project_id), 'current_instance': None,
current_instance=None, 'epoch': int(timezone.now().timestamp()),
epoch=int(timezone.now().timestamp()), },
routing_key='external',
) )
# Delete the cycle # Delete the cycle
cycle.delete() cycle.delete()
@@ -381,9 +321,7 @@ class CycleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
def get_queryset(self): def get_queryset(self):
return ( return (
CycleIssue.objects.annotate( CycleIssue.objects.annotate(
sub_issues_count=Issue.issue_objects.filter( sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("issue_id"))
parent=OuterRef("issue_id")
)
.order_by() .order_by()
.annotate(count=Func(F("id"), function="Count")) .annotate(count=Func(F("id"), function="Count"))
.values("count") .values("count")
@@ -406,9 +344,7 @@ class CycleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
issues = ( issues = (
Issue.issue_objects.filter(issue_cycle__cycle_id=cycle_id) Issue.issue_objects.filter(issue_cycle__cycle_id=cycle_id)
.annotate( .annotate(
sub_issues_count=Issue.issue_objects.filter( sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
parent=OuterRef("id")
)
.order_by() .order_by()
.annotate(count=Func(F("id"), function="Count")) .annotate(count=Func(F("id"), function="Count"))
.values("count") .values("count")
@@ -430,9 +366,7 @@ class CycleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
.values("count") .values("count")
) )
.annotate( .annotate(
attachment_count=IssueAttachment.objects.filter( attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id"))
issue=OuterRef("id")
)
.order_by() .order_by()
.annotate(count=Func(F("id"), function="Count")) .annotate(count=Func(F("id"), function="Count"))
.values("count") .values("count")
@@ -455,18 +389,14 @@ class CycleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
if not issues: if not issues:
return Response( return Response(
{"error": "Issues are required"}, {"error": "Issues are required"}, status=status.HTTP_400_BAD_REQUEST
status=status.HTTP_400_BAD_REQUEST,
) )
cycle = Cycle.objects.get( cycle = Cycle.objects.get(
workspace__slug=slug, project_id=project_id, pk=cycle_id workspace__slug=slug, project_id=project_id, pk=cycle_id
) )
if ( if cycle.end_date is not None and cycle.end_date < timezone.now().date():
cycle.end_date is not None
and cycle.end_date < timezone.now().date()
):
return Response( return Response(
{ {
"error": "The Cycle has already been completed so no new issues can be added" "error": "The Cycle has already been completed so no new issues can be added"
@@ -526,21 +456,21 @@ class CycleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
) )
# Capture Issue Activity # Capture Issue Activity
issue_activity.delay( issue_activity.apply_async(
type="cycle.activity.created", args=[],
requested_data=json.dumps({"cycles_list": str(issues)}), kwargs={
actor_id=str(self.request.user.id), 'type': "cycle.activity.created",
issue_id=None, 'requested_data': json.dumps({"cycles_list": str(issues)}),
project_id=str(self.kwargs.get("project_id", None)), 'actor_id': str(self.request.user.id),
current_instance=json.dumps( 'issue_id': None,
{ 'project_id': str(self.kwargs.get("project_id", None)),
'current_instance': json.dumps({
"updated_cycle_issues": update_cycle_issue_activity, "updated_cycle_issues": update_cycle_issue_activity,
"created_cycle_issues": serializers.serialize( "created_cycle_issues": serializers.serialize("json", record_to_create),
"json", record_to_create }),
), 'epoch': int(timezone.now().timestamp()),
} },
), routing_key='external',
epoch=int(timezone.now().timestamp()),
) )
# Return all Cycle Issues # Return all Cycle Issues
@@ -551,26 +481,25 @@ 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, issue_id=issue_id, workspace__slug=slug, project_id=project_id, cycle_id=cycle_id
workspace__slug=slug,
project_id=project_id,
cycle_id=cycle_id,
) )
issue_id = cycle_issue.issue_id issue_id = cycle_issue.issue_id
cycle_issue.delete() cycle_issue.delete()
issue_activity.delay( issue_activity.apply_async(
type="cycle.activity.deleted", args=[],
requested_data=json.dumps( kwargs={
{ 'type': "cycle.activity.deleted",
'requested_data': json.dumps({
"cycle_id": str(self.kwargs.get("cycle_id")), "cycle_id": str(self.kwargs.get("cycle_id")),
"issues": [str(issue_id)], "issues": [str(issue_id)],
} }),
), 'actor_id': str(self.request.user.id),
actor_id=str(self.request.user.id), 'issue_id': str(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()), },
routing_key='external',
) )
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
+38 -52
View File
@@ -14,14 +14,7 @@ from rest_framework.response import Response
from .base import BaseAPIView from .base import BaseAPIView
from plane.app.permissions import ProjectLitePermission from plane.app.permissions import ProjectLitePermission
from plane.api.serializers import InboxIssueSerializer, IssueSerializer from plane.api.serializers import InboxIssueSerializer, IssueSerializer
from plane.db.models import ( from plane.db.models import InboxIssue, Issue, State, ProjectMember, Project, Inbox
InboxIssue,
Issue,
State,
ProjectMember,
Project,
Inbox,
)
from plane.bgtasks.issue_activites_task import issue_activity from plane.bgtasks.issue_activites_task import issue_activity
@@ -50,8 +43,7 @@ class InboxIssueAPIEndpoint(BaseAPIView):
).first() ).first()
project = Project.objects.get( project = Project.objects.get(
workspace__slug=self.kwargs.get("slug"), workspace__slug=self.kwargs.get("slug"), pk=self.kwargs.get("project_id")
pk=self.kwargs.get("project_id"),
) )
if inbox is None and not project.inbox_view: if inbox is None and not project.inbox_view:
@@ -59,8 +51,7 @@ class InboxIssueAPIEndpoint(BaseAPIView):
return ( return (
InboxIssue.objects.filter( InboxIssue.objects.filter(
Q(snoozed_till__gte=timezone.now()) Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True),
| 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,
@@ -96,8 +87,7 @@ 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"}, {"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST
status=status.HTTP_400_BAD_REQUEST,
) )
inbox = Inbox.objects.filter( inbox = Inbox.objects.filter(
@@ -127,8 +117,7 @@ class InboxIssueAPIEndpoint(BaseAPIView):
"none", "none",
]: ]:
return Response( return Response(
{"error": "Invalid priority"}, {"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST
status=status.HTTP_400_BAD_REQUEST,
) )
# Create or get state # Create or get state
@@ -153,14 +142,18 @@ class InboxIssueAPIEndpoint(BaseAPIView):
) )
# Create an Issue Activity # Create an Issue Activity
issue_activity.delay( issue_activity.apply_async(
type="issue.activity.created", args=[], # If no positional arguments are required
requested_data=json.dumps(request.data, cls=DjangoJSONEncoder), kwargs={
actor_id=str(request.user.id), "type": "issue.activity.created",
issue_id=str(issue.id), "requested_data": json.dumps(request.data, cls=DjangoJSONEncoder),
project_id=str(project_id), "actor_id": str(request.user.id),
current_instance=None, "issue_id": str(issue.id),
epoch=int(timezone.now().timestamp()), "project_id": str(project_id),
"current_instance": None,
"epoch": int(timezone.now().timestamp()),
},
routing_key="external",
) )
# create an inbox issue # create an inbox issue
@@ -233,31 +226,31 @@ class InboxIssueAPIEndpoint(BaseAPIView):
"description_html": issue_data.get( "description_html": issue_data.get(
"description_html", issue.description_html "description_html", issue.description_html
), ),
"description": issue_data.get( "description": issue_data.get("description", issue.description),
"description", issue.description
),
} }
issue_serializer = IssueSerializer( issue_serializer = IssueSerializer(issue, data=issue_data, partial=True)
issue, data=issue_data, partial=True
)
if issue_serializer.is_valid(): if issue_serializer.is_valid():
current_instance = issue current_instance = issue
# Log all the updates # Log all the updates
requested_data = json.dumps(issue_data, cls=DjangoJSONEncoder) requested_data = json.dumps(issue_data, cls=DjangoJSONEncoder)
if issue is not None: if issue is not None:
issue_activity.delay( issue_activity.apply_async(
type="issue.activity.updated", args=[],
requested_data=requested_data, kwargs={
actor_id=str(request.user.id), "type": "issue.activity.updated",
issue_id=str(issue_id), "requested_data": requested_data,
project_id=str(project_id), "actor_id": str(request.user.id),
current_instance=json.dumps( "issue_id": str(issue_id),
IssueSerializer(current_instance).data, "project_id": str(project_id),
cls=DjangoJSONEncoder, "current_instance": json.dumps(
), IssueSerializer(current_instance).data,
epoch=int(timezone.now().timestamp()), cls=DjangoJSONEncoder,
),
"epoch": int(timezone.now().timestamp()),
},
routing_key="external",
) )
issue_serializer.save() issue_serializer.save()
else: else:
@@ -281,9 +274,7 @@ class InboxIssueAPIEndpoint(BaseAPIView):
project_id=project_id, project_id=project_id,
) )
state = State.objects.filter( state = State.objects.filter(
group="cancelled", group="cancelled", workspace__slug=slug, project_id=project_id
workspace__slug=slug,
project_id=project_id,
).first() ).first()
if state is not None: if state is not None:
issue.state = state issue.state = state
@@ -301,22 +292,17 @@ class InboxIssueAPIEndpoint(BaseAPIView):
if issue.state.name == "Triage": if issue.state.name == "Triage":
# Move to default state # Move to default state
state = State.objects.filter( state = State.objects.filter(
workspace__slug=slug, workspace__slug=slug, project_id=project_id, default=True
project_id=project_id,
default=True,
).first() ).first()
if state is not None: if state is not None:
issue.state = state issue.state = state
issue.save() issue.save()
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
return Response( return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
else: else:
return Response( return Response(
InboxIssueSerializer(inbox_issue).data, InboxIssueSerializer(inbox_issue).data, status=status.HTTP_200_OK
status=status.HTTP_200_OK,
) )
def delete(self, request, slug, project_id, issue_id): def delete(self, request, slug, project_id, issue_id):
+130 -243
View File
@@ -67,9 +67,7 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
def get_queryset(self): def get_queryset(self):
return ( return (
Issue.issue_objects.annotate( Issue.issue_objects.annotate(
sub_issues_count=Issue.issue_objects.filter( sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
parent=OuterRef("id")
)
.order_by() .order_by()
.annotate(count=Func(F("id"), function="Count")) .annotate(count=Func(F("id"), function="Count"))
.values("count") .values("count")
@@ -88,9 +86,7 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
def get(self, request, slug, project_id, pk=None): def get(self, request, slug, project_id, pk=None):
if pk: if pk:
issue = Issue.issue_objects.annotate( issue = Issue.issue_objects.annotate(
sub_issues_count=Issue.issue_objects.filter( sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
parent=OuterRef("id")
)
.order_by() .order_by()
.annotate(count=Func(F("id"), function="Count")) .annotate(count=Func(F("id"), function="Count"))
.values("count") .values("count")
@@ -106,13 +102,7 @@ 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 = [ state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
"backlog",
"unstarted",
"started",
"completed",
"cancelled",
]
order_by_param = request.GET.get("order_by", "-created_at") order_by_param = request.GET.get("order_by", "-created_at")
@@ -127,9 +117,7 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
.values("count") .values("count")
) )
.annotate( .annotate(
attachment_count=IssueAttachment.objects.filter( attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id"))
issue=OuterRef("id")
)
.order_by() .order_by()
.annotate(count=Func(F("id"), function="Count")) .annotate(count=Func(F("id"), function="Count"))
.values("count") .values("count")
@@ -139,9 +127,7 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
# Priority Ordering # Priority Ordering
if order_by_param == "priority" or order_by_param == "-priority": if order_by_param == "priority" or order_by_param == "-priority":
priority_order = ( priority_order = (
priority_order priority_order if order_by_param == "priority" else priority_order[::-1]
if order_by_param == "priority"
else priority_order[::-1]
) )
issue_queryset = issue_queryset.annotate( issue_queryset = issue_queryset.annotate(
priority_order=Case( priority_order=Case(
@@ -189,9 +175,7 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
else order_by_param else order_by_param
) )
).order_by( ).order_by(
"-max_values" "-max_values" if order_by_param.startswith("-") else "max_values"
if order_by_param.startswith("-")
else "max_values"
) )
else: else:
issue_queryset = issue_queryset.order_by(order_by_param) issue_queryset = issue_queryset.order_by(order_by_param)
@@ -220,117 +204,69 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
) )
if serializer.is_valid(): if serializer.is_valid():
if (
request.data.get("external_id")
and request.data.get("external_source")
and Issue.objects.filter(
project_id=project_id,
workspace__slug=slug,
external_source=request.data.get("external_source"),
external_id=request.data.get("external_id"),
).exists()
):
issue = Issue.objects.filter(
workspace__slug=slug,
project_id=project_id,
external_id=request.data.get("external_id"),
external_source=request.data.get("external_source"),
).first()
return Response(
{
"error": "Issue with the same external id and external source already exists",
"id": str(issue.id),
},
status=status.HTTP_409_CONFLICT,
)
serializer.save() serializer.save()
# Track the issue # Track the issue
issue_activity.delay( issue_activity.apply_async(
type="issue.activity.created", args=[], # If no positional arguments are required
requested_data=json.dumps( kwargs={
self.request.data, cls=DjangoJSONEncoder 'type': "issue.activity.created",
), '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),
current_instance=None, 'current_instance': None,
epoch=int(timezone.now().timestamp()), 'epoch': int(timezone.now().timestamp()),
},
routing_key='external',
) )
return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def patch(self, request, slug, project_id, pk=None): def patch(self, request, slug, project_id, pk=None):
issue = Issue.objects.get( issue = Issue.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
workspace__slug=slug, project_id=project_id, pk=pk
)
project = Project.objects.get(pk=project_id)
current_instance = json.dumps( current_instance = json.dumps(
IssueSerializer(issue).data, cls=DjangoJSONEncoder IssueSerializer(issue).data, cls=DjangoJSONEncoder
) )
requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder) requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder)
serializer = IssueSerializer( serializer = IssueSerializer(issue, data=request.data, partial=True)
issue,
data=request.data,
context={
"project_id": project_id,
"workspace_id": project.workspace_id,
},
partial=True,
)
if serializer.is_valid(): if serializer.is_valid():
if (
str(request.data.get("external_id"))
and (issue.external_id != str(request.data.get("external_id")))
and Issue.objects.filter(
project_id=project_id,
workspace__slug=slug,
external_source=request.data.get(
"external_source", issue.external_source
),
external_id=request.data.get("external_id"),
).exists()
):
return Response(
{
"error": "Issue with the same external id and external source already exists",
"id": str(issue.id),
},
status=status.HTTP_409_CONFLICT,
)
serializer.save() serializer.save()
issue_activity.delay( issue_activity.apply_async(
type="issue.activity.updated", args=[],
requested_data=requested_data, kwargs={
actor_id=str(request.user.id), 'type': "issue.activity.updated",
issue_id=str(pk), 'requested_data': requested_data,
project_id=str(project_id), 'actor_id': str(request.user.id),
external_id__isnull=False, 'issue_id': str(pk),
external_source__isnull=False, 'project_id': str(project_id),
current_instance=current_instance, 'current_instance': current_instance,
epoch=int(timezone.now().timestamp()), 'epoch': int(timezone.now().timestamp()),
},
routing_key='external',
) )
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):
issue = Issue.objects.get( issue = Issue.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
workspace__slug=slug, project_id=project_id, pk=pk
)
current_instance = json.dumps( current_instance = json.dumps(
IssueSerializer(issue).data, cls=DjangoJSONEncoder IssueSerializer(issue).data, cls=DjangoJSONEncoder
) )
issue.delete() issue.delete()
issue_activity.delay( issue_activity.apply_async(
type="issue.activity.deleted", args=[],
requested_data=json.dumps({"issue_id": str(pk)}), kwargs={
actor_id=str(request.user.id), 'type': "issue.activity.deleted",
issue_id=str(pk), 'requested_data': json.dumps({"issue_id": str(pk)}),
project_id=str(project_id), 'actor_id': str(request.user.id),
current_instance=current_instance, 'issue_id': str(pk),
epoch=int(timezone.now().timestamp()), 'project_id': str(project_id),
'current_instance': current_instance,
'epoch': int(timezone.now().timestamp()),
},
routing_key='your_routing_key',
queue='your_queue_name'
) )
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
@@ -364,49 +300,13 @@ class LabelAPIEndpoint(BaseAPIView):
try: try:
serializer = LabelSerializer(data=request.data) serializer = LabelSerializer(data=request.data)
if serializer.is_valid(): if serializer.is_valid():
if (
request.data.get("external_id")
and request.data.get("external_source")
and Label.objects.filter(
project_id=project_id,
workspace__slug=slug,
external_source=request.data.get("external_source"),
external_id=request.data.get("external_id"),
).exists()
):
label = Label.objects.filter(
workspace__slug=slug,
project_id=project_id,
external_id=request.data.get("external_id"),
external_source=request.data.get("external_source"),
).first()
return Response(
{
"error": "Label with the same external id and external source already exists",
"id": str(label.id),
},
status=status.HTTP_409_CONFLICT,
)
serializer.save(project_id=project_id) serializer.save(project_id=project_id)
return Response( return Response(serializer.data, status=status.HTTP_201_CREATED)
serializer.data, status=status.HTTP_201_CREATED return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
)
return Response(
serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
except IntegrityError: except IntegrityError:
label = Label.objects.filter(
workspace__slug=slug,
project_id=project_id,
name=request.data.get("name"),
).first()
return Response( return Response(
{ {"error": "Label with the same name already exists in the project"},
"error": "Label with the same name already exists in the project", status=status.HTTP_400_BAD_REQUEST,
"id": str(label.id),
},
status=status.HTTP_409_CONFLICT,
) )
def get(self, request, slug, project_id, pk=None): def get(self, request, slug, project_id, pk=None):
@@ -433,25 +333,6 @@ class LabelAPIEndpoint(BaseAPIView):
label = self.get_queryset().get(pk=pk) label = self.get_queryset().get(pk=pk)
serializer = LabelSerializer(label, data=request.data, partial=True) serializer = LabelSerializer(label, data=request.data, partial=True)
if serializer.is_valid(): if serializer.is_valid():
if (
str(request.data.get("external_id"))
and (label.external_id != str(request.data.get("external_id")))
and Issue.objects.filter(
project_id=project_id,
workspace__slug=slug,
external_source=request.data.get(
"external_source", label.external_source
),
external_id=request.data.get("external_id"),
).exists()
):
return Response(
{
"error": "Label with the same external id and external source already exists",
"id": str(label.id),
},
status=status.HTTP_409_CONFLICT,
)
serializer.save() 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)
@@ -519,68 +400,70 @@ class IssueLinkAPIEndpoint(BaseAPIView):
project_id=project_id, project_id=project_id,
issue_id=issue_id, issue_id=issue_id,
) )
issue_activity.delay( issue_activity.apply_async(
type="link.activity.created", args=[], # If no positional arguments are required
requested_data=json.dumps( kwargs={
serializer.data, cls=DjangoJSONEncoder 'type': "link.activity.created",
), '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")),
current_instance=None, 'current_instance': None,
epoch=int(timezone.now().timestamp()), 'epoch': int(timezone.now().timestamp()),
},
routing_key='external',
) )
return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def patch(self, request, slug, project_id, issue_id, pk): def patch(self, request, slug, project_id, issue_id, pk):
issue_link = IssueLink.objects.get( issue_link = IssueLink.objects.get(
workspace__slug=slug, workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
project_id=project_id,
issue_id=issue_id,
pk=pk,
) )
requested_data = json.dumps(request.data, cls=DjangoJSONEncoder) requested_data = json.dumps(request.data, cls=DjangoJSONEncoder)
current_instance = json.dumps( current_instance = json.dumps(
IssueLinkSerializer(issue_link).data, IssueLinkSerializer(issue_link).data,
cls=DjangoJSONEncoder, cls=DjangoJSONEncoder,
) )
serializer = IssueLinkSerializer( serializer = IssueLinkSerializer(issue_link, data=request.data, partial=True)
issue_link, data=request.data, partial=True
)
if serializer.is_valid(): if serializer.is_valid():
serializer.save() serializer.save()
issue_activity.delay( issue_activity.apply_async(
type="link.activity.updated", args=[], # If no positional arguments are required
requested_data=requested_data, kwargs={
actor_id=str(request.user.id), 'type': "link.activity.updated",
issue_id=str(issue_id), 'requested_data': requested_data,
project_id=str(project_id), 'actor_id': str(request.user.id),
current_instance=current_instance, 'issue_id': str(issue_id),
epoch=int(timezone.now().timestamp()), 'project_id': str(project_id),
'current_instance': current_instance,
'epoch': int(timezone.now().timestamp()),
},
routing_key='external',
) )
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, issue_id, pk): def delete(self, request, slug, project_id, issue_id, pk):
issue_link = IssueLink.objects.get( issue_link = IssueLink.objects.get(
workspace__slug=slug, workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
project_id=project_id,
issue_id=issue_id,
pk=pk,
) )
current_instance = json.dumps( current_instance = json.dumps(
IssueLinkSerializer(issue_link).data, IssueLinkSerializer(issue_link).data,
cls=DjangoJSONEncoder, cls=DjangoJSONEncoder,
) )
issue_activity.delay( issue_activity.apply_async(
type="link.activity.deleted", args=[], # If no positional arguments are required
requested_data=json.dumps({"link_id": str(pk)}), kwargs={
actor_id=str(request.user.id), 'type': "link.activity.deleted",
issue_id=str(issue_id), 'requested_data': json.dumps({"link_id": str(pk)}),
project_id=str(project_id), 'actor_id': str(request.user.id),
current_instance=current_instance, 'issue_id': str(issue_id),
epoch=int(timezone.now().timestamp()), 'project_id': str(project_id),
'current_instance': current_instance,
'epoch': int(timezone.now().timestamp()),
},
routing_key='external',
) )
issue_link.delete() issue_link.delete()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
@@ -602,9 +485,7 @@ class IssueCommentAPIEndpoint(WebhookMixin, BaseAPIView):
def get_queryset(self): def get_queryset(self):
return ( return (
IssueComment.objects.filter( IssueComment.objects.filter(workspace__slug=self.kwargs.get("slug"))
workspace__slug=self.kwargs.get("slug")
)
.filter(project_id=self.kwargs.get("project_id")) .filter(project_id=self.kwargs.get("project_id"))
.filter(issue_id=self.kwargs.get("issue_id")) .filter(issue_id=self.kwargs.get("issue_id"))
.filter(project__project_projectmember__member=self.request.user) .filter(project__project_projectmember__member=self.request.user)
@@ -654,26 +535,27 @@ class IssueCommentAPIEndpoint(WebhookMixin, BaseAPIView):
issue_id=issue_id, issue_id=issue_id,
actor=request.user, actor=request.user,
) )
issue_activity.delay( issue_activity.apply_async(
type="comment.activity.created", args=[],
requested_data=json.dumps( kwargs={
serializer.data, cls=DjangoJSONEncoder "type": "comment.activity.created",
), "requested_data": json.dumps(
actor_id=str(self.request.user.id), serializer.data, cls=DjangoJSONEncoder
issue_id=str(self.kwargs.get("issue_id")), ),
project_id=str(self.kwargs.get("project_id")), "actor_id": str(self.request.user.id),
current_instance=None, "issue_id": str(self.kwargs.get("issue_id")),
epoch=int(timezone.now().timestamp()), "project_id": str(self.kwargs.get("project_id")),
"current_instance": None,
"epoch": int(timezone.now().timestamp()),
},
routing_key="external",
) )
return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def patch(self, request, slug, project_id, issue_id, pk): def patch(self, request, slug, project_id, issue_id, pk):
issue_comment = IssueComment.objects.get( issue_comment = IssueComment.objects.get(
workspace__slug=slug, workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
project_id=project_id,
issue_id=issue_id,
pk=pk,
) )
requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder) requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder)
current_instance = json.dumps( current_instance = json.dumps(
@@ -685,38 +567,43 @@ class IssueCommentAPIEndpoint(WebhookMixin, BaseAPIView):
) )
if serializer.is_valid(): if serializer.is_valid():
serializer.save() serializer.save()
issue_activity.delay( issue_activity.apply_async(
type="comment.activity.updated", args=[],
requested_data=requested_data, kwargs={
actor_id=str(request.user.id), "type": "comment.activity.updated",
issue_id=str(issue_id), "requested_data": requested_data,
project_id=str(project_id), "actor_id": str(request.user.id),
current_instance=current_instance, "issue_id": str(issue_id),
epoch=int(timezone.now().timestamp()), "project_id": str(project_id),
"current_instance": current_instance,
"epoch": int(timezone.now().timestamp()),
},
routing_key="external",
) )
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, issue_id, pk): def delete(self, request, slug, project_id, issue_id, pk):
issue_comment = IssueComment.objects.get( issue_comment = IssueComment.objects.get(
workspace__slug=slug, workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
project_id=project_id,
issue_id=issue_id,
pk=pk,
) )
current_instance = json.dumps( current_instance = json.dumps(
IssueCommentSerializer(issue_comment).data, IssueCommentSerializer(issue_comment).data,
cls=DjangoJSONEncoder, cls=DjangoJSONEncoder,
) )
issue_comment.delete() issue_comment.delete()
issue_activity.delay( issue_activity.apply_async(
type="comment.activity.deleted", args=[], # If no positional arguments are required
requested_data=json.dumps({"comment_id": str(pk)}), kwargs={
actor_id=str(request.user.id), "type": "comment.activity.deleted",
issue_id=str(issue_id), "requested_data": json.dumps({"comment_id": str(pk)}),
project_id=str(project_id), "actor_id": str(request.user.id),
current_instance=current_instance, "issue_id": str(issue_id),
epoch=int(timezone.now().timestamp()), "project_id": str(project_id),
"current_instance": current_instance,
"epoch": int(timezone.now().timestamp()),
},
routing_key="external",
) )
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
+53 -118
View File
@@ -55,9 +55,7 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
.prefetch_related( .prefetch_related(
Prefetch( Prefetch(
"link_module", "link_module",
queryset=ModuleLink.objects.select_related( queryset=ModuleLink.objects.select_related("module", "created_by"),
"module", "created_by"
),
) )
) )
.annotate( .annotate(
@@ -123,38 +121,9 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
) )
def post(self, request, slug, project_id): def post(self, request, slug, project_id):
project = Project.objects.get(pk=project_id, workspace__slug=slug) project = Project.objects.get(workspace__slug=slug, pk=project_id)
serializer = ModuleSerializer( serializer = ModuleSerializer(data=request.data, context={"project": project})
data=request.data,
context={
"project_id": project_id,
"workspace_id": project.workspace_id,
},
)
if serializer.is_valid(): if serializer.is_valid():
if (
request.data.get("external_id")
and request.data.get("external_source")
and Module.objects.filter(
project_id=project_id,
workspace__slug=slug,
external_source=request.data.get("external_source"),
external_id=request.data.get("external_id"),
).exists()
):
module = Module.objects.filter(
project_id=project_id,
workspace__slug=slug,
external_source=request.data.get("external_source"),
external_id=request.data.get("external_id"),
).first()
return Response(
{
"error": "Module with the same external id and external source already exists",
"id": str(module.id),
},
status=status.HTTP_409_CONFLICT,
)
serializer.save() serializer.save()
module = Module.objects.get(pk=serializer.data["id"]) module = Module.objects.get(pk=serializer.data["id"])
serializer = ModuleSerializer(module) serializer = ModuleSerializer(module)
@@ -162,35 +131,11 @@ 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( module = Module.objects.get(pk=pk, project_id=project_id, workspace__slug=slug)
pk=pk, project_id=project_id, workspace__slug=slug serializer = ModuleSerializer(module, data=request.data)
)
serializer = ModuleSerializer(
module,
data=request.data,
context={"project_id": project_id},
partial=True,
)
if serializer.is_valid(): if serializer.is_valid():
if (
request.data.get("external_id")
and (module.external_id != request.data.get("external_id"))
and Module.objects.filter(
project_id=project_id,
workspace__slug=slug,
external_source=request.data.get("external_source", module.external_source),
external_id=request.data.get("external_id"),
).exists()
):
return Response(
{
"error": "Module with the same external id and external source already exists",
"id": str(module.id),
},
status=status.HTTP_409_CONFLICT,
)
serializer.save() serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK) 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 get(self, request, slug, project_id, pk=None): def get(self, request, slug, project_id, pk=None):
@@ -217,28 +162,26 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
) )
def delete(self, request, slug, project_id, pk): def delete(self, request, slug, project_id, pk):
module = Module.objects.get( module = Module.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
workspace__slug=slug, project_id=project_id, pk=pk
)
module_issues = list( module_issues = list(
ModuleIssue.objects.filter(module_id=pk).values_list( ModuleIssue.objects.filter(module_id=pk).values_list("issue", flat=True)
"issue", flat=True
)
) )
issue_activity.delay( issue_activity.apply_async(
type="module.activity.deleted", args=[],
requested_data=json.dumps( kwargs={
{ 'type': "module.activity.deleted",
'requested_data': json.dumps({
"module_id": str(pk), "module_id": str(pk),
"module_name": str(module.name), "module_name": str(module.name),
"issues": [str(issue_id) for issue_id in module_issues], "issues": [str(issue_id) for issue_id in module_issues],
} }),
), 'actor_id': str(request.user.id),
actor_id=str(request.user.id), 'issue_id': None,
issue_id=None, 'project_id': str(project_id),
project_id=str(project_id), 'current_instance': None,
current_instance=None, 'epoch': int(timezone.now().timestamp()),
epoch=int(timezone.now().timestamp()), },
routing_key='external',
) )
module.delete() module.delete()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
@@ -263,9 +206,7 @@ class ModuleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
def get_queryset(self): def get_queryset(self):
return ( return (
ModuleIssue.objects.annotate( ModuleIssue.objects.annotate(
sub_issues_count=Issue.issue_objects.filter( sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("issue"))
parent=OuterRef("issue")
)
.order_by() .order_by()
.annotate(count=Func(F("id"), function="Count")) .annotate(count=Func(F("id"), function="Count"))
.values("count") .values("count")
@@ -289,9 +230,7 @@ class ModuleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
issues = ( issues = (
Issue.issue_objects.filter(issue_module__module_id=module_id) Issue.issue_objects.filter(issue_module__module_id=module_id)
.annotate( .annotate(
sub_issues_count=Issue.issue_objects.filter( sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
parent=OuterRef("id")
)
.order_by() .order_by()
.annotate(count=Func(F("id"), function="Count")) .annotate(count=Func(F("id"), function="Count"))
.values("count") .values("count")
@@ -313,9 +252,7 @@ class ModuleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
.values("count") .values("count")
) )
.annotate( .annotate(
attachment_count=IssueAttachment.objects.filter( attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id"))
issue=OuterRef("id")
)
.order_by() .order_by()
.annotate(count=Func(F("id"), function="Count")) .annotate(count=Func(F("id"), function="Count"))
.values("count") .values("count")
@@ -336,8 +273,7 @@ class ModuleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
issues = request.data.get("issues", []) issues = request.data.get("issues", [])
if not len(issues): if not len(issues):
return Response( return Response(
{"error": "Issues are required"}, {"error": "Issues are required"}, status=status.HTTP_400_BAD_REQUEST
status=status.HTTP_400_BAD_REQUEST,
) )
module = Module.objects.get( module = Module.objects.get(
workspace__slug=slug, project_id=project_id, pk=module_id workspace__slug=slug, project_id=project_id, pk=module_id
@@ -396,21 +332,21 @@ class ModuleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
) )
# Capture Issue Activity # Capture Issue Activity
issue_activity.delay( issue_activity.apply_async(
type="module.activity.created", args=[],
requested_data=json.dumps({"modules_list": str(issues)}), kwargs={
actor_id=str(self.request.user.id), 'type': "module.activity.created",
issue_id=None, 'requested_data': json.dumps({"modules_list": str(issues)}),
project_id=str(self.kwargs.get("project_id", None)), 'actor_id': str(self.request.user.id),
current_instance=json.dumps( 'issue_id': None,
{ 'project_id': str(self.kwargs.get("project_id", None)),
'current_instance': json.dumps({
"updated_module_issues": update_module_issue_activity, "updated_module_issues": update_module_issue_activity,
"created_module_issues": serializers.serialize( "created_module_issues": serializers.serialize("json", record_to_create),
"json", record_to_create }),
), 'epoch': int(timezone.now().timestamp()),
} },
), routing_key='external',
epoch=int(timezone.now().timestamp()),
) )
return Response( return Response(
@@ -420,24 +356,23 @@ 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, workspace__slug=slug, project_id=project_id, module_id=module_id, issue_id=issue_id
project_id=project_id,
module_id=module_id,
issue_id=issue_id,
) )
module_issue.delete() module_issue.delete()
issue_activity.delay( issue_activity.apply_async(
type="module.activity.deleted", args=[], # If no positional arguments are required
requested_data=json.dumps( kwargs={
{ 'type': "module.activity.deleted",
'requested_data': json.dumps({
"module_id": str(module_id), "module_id": str(module_id),
"issues": [str(module_issue.issue_id)], "issues": [str(module_issue.issue_id)],
} }),
), 'actor_id': str(request.user.id),
actor_id=str(request.user.id), 'issue_id': str(issue_id),
issue_id=str(issue_id), 'project_id': str(project_id),
project_id=str(project_id), 'current_instance': None,
current_instance=None, 'epoch': int(timezone.now().timestamp()),
epoch=int(timezone.now().timestamp()), },
routing_key='external',
) )
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
+12 -43
View File
@@ -39,15 +39,9 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
def get_queryset(self): def get_queryset(self):
return ( return (
Project.objects.filter(workspace__slug=self.kwargs.get("slug")) Project.objects.filter(workspace__slug=self.kwargs.get("slug"))
.filter( .filter(Q(project_projectmember__member=self.request.user) | Q(network=2))
Q(project_projectmember__member=self.request.user)
| Q(network=2)
)
.select_related( .select_related(
"workspace", "workspace", "workspace__owner", "default_assignee", "project_lead"
"workspace__owner",
"default_assignee",
"project_lead",
) )
.annotate( .annotate(
is_member=Exists( is_member=Exists(
@@ -126,18 +120,11 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
request=request, request=request,
queryset=(projects), queryset=(projects),
on_results=lambda projects: ProjectSerializer( on_results=lambda projects: ProjectSerializer(
projects, projects, many=True, fields=self.fields, expand=self.expand,
many=True,
fields=self.fields,
expand=self.expand,
).data, ).data,
) )
project = self.get_queryset().get(workspace__slug=slug, pk=project_id) project = self.get_queryset().get(workspace__slug=slug, pk=project_id)
serializer = ProjectSerializer( serializer = ProjectSerializer(project, fields=self.fields, expand=self.expand,)
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):
@@ -151,9 +138,7 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
# Add the user as Administrator to the project # Add the user as Administrator to the project
project_member = ProjectMember.objects.create( project_member = ProjectMember.objects.create(
project_id=serializer.data["id"], project_id=serializer.data["id"], member=request.user, role=20
member=request.user,
role=20,
) )
# Also create the issue property for the user # Also create the issue property for the user
_ = IssueProperty.objects.create( _ = IssueProperty.objects.create(
@@ -226,15 +211,9 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
] ]
) )
project = ( project = self.get_queryset().filter(pk=serializer.data["id"]).first()
self.get_queryset()
.filter(pk=serializer.data["id"])
.first()
)
serializer = ProjectSerializer(project) serializer = ProjectSerializer(project)
return Response( return Response(serializer.data, status=status.HTTP_201_CREATED)
serializer.data, status=status.HTTP_201_CREATED
)
return Response( return Response(
serializer.errors, serializer.errors,
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
@@ -247,8 +226,7 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
) )
except Workspace.DoesNotExist as e: except Workspace.DoesNotExist as e:
return Response( return Response(
{"error": "Workspace does not exist"}, {"error": "Workspace does not exist"}, status=status.HTTP_404_NOT_FOUND
status=status.HTTP_404_NOT_FOUND,
) )
except ValidationError as e: except ValidationError as e:
return Response( return Response(
@@ -272,9 +250,7 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
serializer.save() serializer.save()
if serializer.data["inbox_view"]: if serializer.data["inbox_view"]:
Inbox.objects.get_or_create( Inbox.objects.get_or_create(
name=f"{project.name} Inbox", name=f"{project.name} Inbox", project=project, is_default=True
project=project,
is_default=True,
) )
# Create the triage state in Backlog group # Create the triage state in Backlog group
@@ -286,16 +262,10 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
color="#ff7700", color="#ff7700",
) )
project = ( project = self.get_queryset().filter(pk=serializer.data["id"]).first()
self.get_queryset()
.filter(pk=serializer.data["id"])
.first()
)
serializer = ProjectSerializer(project) serializer = ProjectSerializer(project)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
return Response( return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
except IntegrityError as e: except IntegrityError as e:
if "already exists" in str(e): if "already exists" in str(e):
return Response( return Response(
@@ -304,8 +274,7 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
) )
except (Project.DoesNotExist, Workspace.DoesNotExist): except (Project.DoesNotExist, Workspace.DoesNotExist):
return Response( return Response(
{"error": "Project does not exist"}, {"error": "Project does not exist"}, status=status.HTTP_404_NOT_FOUND
status=status.HTTP_404_NOT_FOUND,
) )
except ValidationError as e: except ValidationError as e:
return Response( return Response(
+4 -54
View File
@@ -34,34 +34,8 @@ class StateAPIEndpoint(BaseAPIView):
) )
def post(self, request, slug, project_id): def post(self, request, slug, project_id):
serializer = StateSerializer( serializer = StateSerializer(data=request.data, context={"project_id": project_id})
data=request.data, context={"project_id": project_id}
)
if serializer.is_valid(): if serializer.is_valid():
if (
request.data.get("external_id")
and request.data.get("external_source")
and State.objects.filter(
project_id=project_id,
workspace__slug=slug,
external_source=request.data.get("external_source"),
external_id=request.data.get("external_id"),
).exists()
):
state = State.objects.filter(
workspace__slug=slug,
project_id=project_id,
external_id=request.data.get("external_id"),
external_source=request.data.get("external_source"),
).first()
return Response(
{
"error": "State with the same external id and external source already exists",
"id": str(state.id),
},
status=status.HTTP_409_CONFLICT,
)
serializer.save(project_id=project_id) serializer.save(project_id=project_id)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@@ -90,19 +64,14 @@ class StateAPIEndpoint(BaseAPIView):
) )
if state.default: if state.default:
return Response( return Response({"error": "Default state cannot be deleted"}, status=status.HTTP_400_BAD_REQUEST)
{"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,
) )
@@ -110,28 +79,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( state = State.objects.get(workspace__slug=slug, project_id=project_id, pk=state_id)
workspace__slug=slug, project_id=project_id, pk=state_id
)
serializer = StateSerializer(state, data=request.data, partial=True) serializer = StateSerializer(state, data=request.data, partial=True)
if serializer.is_valid(): if serializer.is_valid():
if (
str(request.data.get("external_id"))
and (state.external_id != str(request.data.get("external_id")))
and State.objects.filter(
project_id=project_id,
workspace__slug=slug,
external_source=request.data.get("external_source", state.external_source),
external_id=request.data.get("external_id"),
).exists()
):
return Response(
{
"error": "State with the same external id and external source already exists",
"id": str(state.id),
},
status=status.HTTP_409_CONFLICT,
)
serializer.save() serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@@ -25,10 +25,7 @@ class APIKeyAuthentication(authentication.BaseAuthentication):
def validate_api_token(self, token): def validate_api_token(self, token):
try: try:
api_token = APIToken.objects.get( api_token = APIToken.objects.get(
Q( Q(Q(expired_at__gt=timezone.now()) | Q(expired_at__isnull=True)),
Q(expired_at__gt=timezone.now())
| Q(expired_at__isnull=True)
),
token=token, token=token,
is_active=True, is_active=True,
) )
@@ -1,3 +1,4 @@
from .workspace import ( from .workspace import (
WorkSpaceBasePermission, WorkSpaceBasePermission,
WorkspaceOwnerPermission, WorkspaceOwnerPermission,
@@ -12,3 +13,5 @@ from .project import (
ProjectMemberPermission, ProjectMemberPermission,
ProjectLitePermission, ProjectLitePermission,
) )
+4 -25
View File
@@ -17,7 +17,6 @@ from .workspace import (
WorkspaceThemeSerializer, WorkspaceThemeSerializer,
WorkspaceMemberAdminSerializer, WorkspaceMemberAdminSerializer,
WorkspaceMemberMeSerializer, WorkspaceMemberMeSerializer,
WorkspaceUserPropertiesSerializer,
) )
from .project import ( from .project import (
ProjectSerializer, ProjectSerializer,
@@ -32,20 +31,14 @@ from .project import (
ProjectDeployBoardSerializer, ProjectDeployBoardSerializer,
ProjectMemberAdminSerializer, ProjectMemberAdminSerializer,
ProjectPublicMemberSerializer, ProjectPublicMemberSerializer,
ProjectMemberRoleSerializer,
) )
from .state import StateSerializer, StateLiteSerializer from .state import StateSerializer, StateLiteSerializer
from .view import ( from .view import GlobalViewSerializer, IssueViewSerializer, IssueViewFavoriteSerializer
GlobalViewSerializer,
IssueViewSerializer,
IssueViewFavoriteSerializer,
)
from .cycle import ( from .cycle import (
CycleSerializer, CycleSerializer,
CycleIssueSerializer, CycleIssueSerializer,
CycleFavoriteSerializer, CycleFavoriteSerializer,
CycleWriteSerializer, CycleWriteSerializer,
CycleUserPropertiesSerializer,
) )
from .asset import FileAssetSerializer from .asset import FileAssetSerializer
from .issue import ( from .issue import (
@@ -76,7 +69,6 @@ from .module import (
ModuleIssueSerializer, ModuleIssueSerializer,
ModuleLinkSerializer, ModuleLinkSerializer,
ModuleFavoriteSerializer, ModuleFavoriteSerializer,
ModuleUserPropertiesSerializer,
) )
from .api import APITokenSerializer, APITokenReadSerializer from .api import APITokenSerializer, APITokenReadSerializer
@@ -93,33 +85,20 @@ from .integration import (
from .importer import ImporterSerializer from .importer import ImporterSerializer
from .page import ( from .page import PageSerializer, PageLogSerializer, SubPageSerializer, PageFavoriteSerializer
PageSerializer,
PageLogSerializer,
SubPageSerializer,
PageFavoriteSerializer,
)
from .estimate import ( from .estimate import (
EstimateSerializer, EstimateSerializer,
EstimatePointSerializer, EstimatePointSerializer,
EstimateReadSerializer, EstimateReadSerializer,
WorkspaceEstimateSerializer,
) )
from .inbox import ( from .inbox import InboxSerializer, InboxIssueSerializer, IssueStateInboxSerializer
InboxSerializer,
InboxIssueSerializer,
IssueStateInboxSerializer,
InboxIssueLiteSerializer,
)
from .analytic import AnalyticViewSerializer from .analytic import AnalyticViewSerializer
from .notification import NotificationSerializer, UserNotificationPreferenceSerializer from .notification import NotificationSerializer
from .exporter import ExporterHistorySerializer from .exporter import ExporterHistorySerializer
from .webhook import WebhookSerializer, WebhookLogSerializer from .webhook import WebhookSerializer, WebhookLogSerializer
from .dashboard import DashboardSerializer, WidgetSerializer
+4 -1
View File
@@ -3,6 +3,7 @@ from plane.db.models import APIToken, APIActivityLog
class APITokenSerializer(BaseSerializer): class APITokenSerializer(BaseSerializer):
class Meta: class Meta:
model = APIToken model = APIToken
fields = "__all__" fields = "__all__"
@@ -17,12 +18,14 @@ class APITokenSerializer(BaseSerializer):
class APITokenReadSerializer(BaseSerializer): class APITokenReadSerializer(BaseSerializer):
class Meta: class Meta:
model = APIToken model = APIToken
exclude = ("token",) exclude = ('token',)
class APIActivityLogSerializer(BaseSerializer): class APIActivityLogSerializer(BaseSerializer):
class Meta: class Meta:
model = APIActivityLog model = APIActivityLog
fields = "__all__" fields = "__all__"
+9 -99
View File
@@ -4,17 +4,16 @@ from rest_framework import serializers
class BaseSerializer(serializers.ModelSerializer): class BaseSerializer(serializers.ModelSerializer):
id = serializers.PrimaryKeyRelatedField(read_only=True) id = serializers.PrimaryKeyRelatedField(read_only=True)
class DynamicBaseSerializer(BaseSerializer): class DynamicBaseSerializer(BaseSerializer):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
# If 'fields' is provided in the arguments, remove it and store it separately. # If 'fields' is provided in the arguments, remove it and store it separately.
# This is done so as not to pass this custom argument up to the superclass. # This is done so as not to pass this custom argument up to the superclass.
fields = kwargs.pop("fields", []) fields = kwargs.pop("fields", None)
self.expand = kwargs.pop("expand", []) or []
fields = self.expand
# Call the initialization of the superclass. # Call the initialization of the superclass.
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
# If 'fields' was provided, filter the fields of the serializer accordingly. # If 'fields' was provided, filter the fields of the serializer accordingly.
if fields is not None: if fields is not None:
self.fields = self._filter_fields(fields) self.fields = self._filter_fields(fields)
@@ -48,101 +47,12 @@ class DynamicBaseSerializer(BaseSerializer):
elif isinstance(item, dict): elif isinstance(item, dict):
allowed.append(list(item.keys())[0]) allowed.append(list(item.keys())[0])
for field in allowed: # Convert the current serializer's fields and the allowed fields to sets.
if field not in self.fields: existing = set(self.fields)
from . import ( allowed = set(allowed)
WorkspaceLiteSerializer,
ProjectLiteSerializer,
UserLiteSerializer,
StateLiteSerializer,
IssueSerializer,
LabelSerializer,
CycleIssueSerializer,
IssueFlatSerializer,
IssueRelationSerializer,
InboxIssueLiteSerializer
)
# Expansion mapper # Remove fields from the serializer that aren't in the 'allowed' list.
expansion = { for field_name in (existing - allowed):
"user": UserLiteSerializer, self.fields.pop(field_name)
"workspace": WorkspaceLiteSerializer,
"project": ProjectLiteSerializer,
"default_assignee": UserLiteSerializer,
"project_lead": UserLiteSerializer,
"state": StateLiteSerializer,
"created_by": UserLiteSerializer,
"issue": IssueSerializer,
"actor": UserLiteSerializer,
"owned_by": UserLiteSerializer,
"members": UserLiteSerializer,
"assignees": UserLiteSerializer,
"labels": LabelSerializer,
"issue_cycle": CycleIssueSerializer,
"parent": IssueSerializer,
"issue_relation": IssueRelationSerializer,
"issue_inbox" : InboxIssueLiteSerializer,
}
self.fields[field] = expansion[field](many=True if field in ["members", "assignees", "labels", "issue_cycle", "issue_relation", "issue_inbox"] else False)
return self.fields return self.fields
def to_representation(self, instance):
response = super().to_representation(instance)
# Ensure 'expand' is iterable before processing
if self.expand:
for expand in self.expand:
if expand in self.fields:
# Import all the expandable serializers
from . import (
WorkspaceLiteSerializer,
ProjectLiteSerializer,
UserLiteSerializer,
StateLiteSerializer,
IssueSerializer,
LabelSerializer,
CycleIssueSerializer,
IssueRelationSerializer,
InboxIssueLiteSerializer
)
# Expansion mapper
expansion = {
"user": UserLiteSerializer,
"workspace": WorkspaceLiteSerializer,
"project": ProjectLiteSerializer,
"default_assignee": UserLiteSerializer,
"project_lead": UserLiteSerializer,
"state": StateLiteSerializer,
"created_by": UserLiteSerializer,
"issue": IssueSerializer,
"actor": UserLiteSerializer,
"owned_by": UserLiteSerializer,
"members": UserLiteSerializer,
"assignees": UserLiteSerializer,
"labels": LabelSerializer,
"issue_cycle": CycleIssueSerializer,
"parent": IssueSerializer,
"issue_relation": IssueRelationSerializer,
"issue_inbox" : InboxIssueLiteSerializer,
}
# Check if field in expansion then expand the field
if expand in expansion:
if isinstance(response.get(expand), list):
exp_serializer = expansion[expand](
getattr(instance, expand), many=True
)
else:
exp_serializer = expansion[expand](
getattr(instance, expand)
)
response[expand] = exp_serializer.data
else:
# You might need to handle this case differently
response[expand] = getattr(
instance, f"{expand}_id", None
)
return response
+5 -27
View File
@@ -7,12 +7,7 @@ from .user import UserLiteSerializer
from .issue import IssueStateSerializer from .issue import IssueStateSerializer
from .workspace import WorkspaceLiteSerializer from .workspace import WorkspaceLiteSerializer
from .project import ProjectLiteSerializer from .project import ProjectLiteSerializer
from plane.db.models import ( from plane.db.models import Cycle, CycleIssue, CycleFavorite
Cycle,
CycleIssue,
CycleFavorite,
CycleUserProperties,
)
class CycleWriteSerializer(BaseSerializer): class CycleWriteSerializer(BaseSerializer):
@@ -22,9 +17,7 @@ class CycleWriteSerializer(BaseSerializer):
and data.get("end_date", None) is not None and data.get("end_date", None) is not None
and data.get("start_date", None) > data.get("end_date", None) and data.get("start_date", None) > data.get("end_date", None)
): ):
raise serializers.ValidationError( raise serializers.ValidationError("Start date cannot exceed end date")
"Start date cannot exceed end date"
)
return data return data
class Meta: class Meta:
@@ -33,6 +26,7 @@ class CycleWriteSerializer(BaseSerializer):
class CycleSerializer(BaseSerializer): class CycleSerializer(BaseSerializer):
owned_by = UserLiteSerializer(read_only=True)
is_favorite = serializers.BooleanField(read_only=True) is_favorite = serializers.BooleanField(read_only=True)
total_issues = serializers.IntegerField(read_only=True) total_issues = serializers.IntegerField(read_only=True)
cancelled_issues = serializers.IntegerField(read_only=True) cancelled_issues = serializers.IntegerField(read_only=True)
@@ -44,11 +38,8 @@ class CycleSerializer(BaseSerializer):
total_estimates = serializers.IntegerField(read_only=True) total_estimates = serializers.IntegerField(read_only=True)
completed_estimates = serializers.IntegerField(read_only=True) completed_estimates = serializers.IntegerField(read_only=True)
started_estimates = serializers.IntegerField(read_only=True) started_estimates = serializers.IntegerField(read_only=True)
workspace_detail = WorkspaceLiteSerializer( workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
read_only=True, source="workspace"
)
project_detail = ProjectLiteSerializer(read_only=True, source="project") project_detail = ProjectLiteSerializer(read_only=True, source="project")
status = serializers.CharField(read_only=True)
def validate(self, data): def validate(self, data):
if ( if (
@@ -56,9 +47,7 @@ class CycleSerializer(BaseSerializer):
and data.get("end_date", None) is not None and data.get("end_date", None) is not None
and data.get("start_date", None) > data.get("end_date", None) and data.get("start_date", None) > data.get("end_date", None)
): ):
raise serializers.ValidationError( raise serializers.ValidationError("Start date cannot exceed end date")
"Start date cannot exceed end date"
)
return data return data
def get_assignees(self, obj): def get_assignees(self, obj):
@@ -116,14 +105,3 @@ class CycleFavoriteSerializer(BaseSerializer):
"project", "project",
"user", "user",
] ]
class CycleUserPropertiesSerializer(BaseSerializer):
class Meta:
model = CycleUserProperties
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"cycle" "user",
]
@@ -1,26 +0,0 @@
# Module imports
from .base import BaseSerializer
from plane.db.models import Dashboard, Widget
# Third party frameworks
from rest_framework import serializers
class DashboardSerializer(BaseSerializer):
class Meta:
model = Dashboard
fields = "__all__"
class WidgetSerializer(BaseSerializer):
is_visible = serializers.BooleanField(read_only=True)
widget_filters = serializers.JSONField(read_only=True)
class Meta:
model = Widget
fields = [
"id",
"key",
"is_visible",
"widget_filters"
]
+3 -37
View File
@@ -2,18 +2,11 @@
from .base import BaseSerializer from .base import BaseSerializer
from plane.db.models import Estimate, EstimatePoint from plane.db.models import Estimate, EstimatePoint
from plane.app.serializers import ( from plane.app.serializers import WorkspaceLiteSerializer, ProjectLiteSerializer
WorkspaceLiteSerializer,
ProjectLiteSerializer,
)
from rest_framework import serializers
class EstimateSerializer(BaseSerializer): class EstimateSerializer(BaseSerializer):
workspace_detail = WorkspaceLiteSerializer( workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
read_only=True, source="workspace"
)
project_detail = ProjectLiteSerializer(read_only=True, source="project") project_detail = ProjectLiteSerializer(read_only=True, source="project")
class Meta: class Meta:
@@ -26,16 +19,6 @@ class EstimateSerializer(BaseSerializer):
class EstimatePointSerializer(BaseSerializer): class EstimatePointSerializer(BaseSerializer):
def validate(self, data):
if not data:
raise serializers.ValidationError("Estimate points are required")
value = data.get("value")
if value and len(value) > 20:
raise serializers.ValidationError(
"Value can't be more than 20 characters"
)
return data
class Meta: class Meta:
model = EstimatePoint model = EstimatePoint
fields = "__all__" fields = "__all__"
@@ -48,9 +31,7 @@ class EstimatePointSerializer(BaseSerializer):
class EstimateReadSerializer(BaseSerializer): class EstimateReadSerializer(BaseSerializer):
points = EstimatePointSerializer(read_only=True, many=True) points = EstimatePointSerializer(read_only=True, many=True)
workspace_detail = WorkspaceLiteSerializer( workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
read_only=True, source="workspace"
)
project_detail = ProjectLiteSerializer(read_only=True, source="project") project_detail = ProjectLiteSerializer(read_only=True, source="project")
class Meta: class Meta:
@@ -61,18 +42,3 @@ class EstimateReadSerializer(BaseSerializer):
"name", "name",
"description", "description",
] ]
class WorkspaceEstimateSerializer(BaseSerializer):
points = EstimatePointSerializer(read_only=True, many=True)
class Meta:
model = Estimate
fields = "__all__"
read_only_fields = [
"points",
"name",
"description",
]
+1 -3
View File
@@ -5,9 +5,7 @@ from .user import UserLiteSerializer
class ExporterHistorySerializer(BaseSerializer): class ExporterHistorySerializer(BaseSerializer):
initiated_by_detail = UserLiteSerializer( initiated_by_detail = UserLiteSerializer(source="initiated_by", read_only=True)
source="initiated_by", read_only=True
)
class Meta: class Meta:
model = ExporterHistory model = ExporterHistory
+2 -6
View File
@@ -7,13 +7,9 @@ from plane.db.models import Importer
class ImporterSerializer(BaseSerializer): class ImporterSerializer(BaseSerializer):
initiated_by_detail = UserLiteSerializer( initiated_by_detail = UserLiteSerializer(source="initiated_by", read_only=True)
source="initiated_by", read_only=True
)
project_detail = ProjectLiteSerializer(source="project", read_only=True) project_detail = ProjectLiteSerializer(source="project", read_only=True)
workspace_detail = WorkspaceLiteSerializer( workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
source="workspace", read_only=True
)
class Meta: class Meta:
model = Importer model = Importer
+3 -6
View File
@@ -46,13 +46,10 @@ class InboxIssueLiteSerializer(BaseSerializer):
class IssueStateInboxSerializer(BaseSerializer): class IssueStateInboxSerializer(BaseSerializer):
state_detail = StateLiteSerializer(read_only=True, source="state") state_detail = StateLiteSerializer(read_only=True, source="state")
project_detail = ProjectLiteSerializer(read_only=True, source="project") project_detail = ProjectLiteSerializer(read_only=True, source="project")
label_details = LabelLiteSerializer( label_details = LabelLiteSerializer(read_only=True, source="labels", many=True)
read_only=True, source="labels", many=True assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
)
assignee_details = UserLiteSerializer(
read_only=True, source="assignees", many=True
)
sub_issues_count = serializers.IntegerField(read_only=True) sub_issues_count = serializers.IntegerField(read_only=True)
bridge_id = serializers.UUIDField(read_only=True)
issue_inbox = InboxIssueLiteSerializer(read_only=True, many=True) issue_inbox = InboxIssueLiteSerializer(read_only=True, many=True)
class Meta: class Meta:
@@ -13,9 +13,7 @@ class IntegrationSerializer(BaseSerializer):
class WorkspaceIntegrationSerializer(BaseSerializer): class WorkspaceIntegrationSerializer(BaseSerializer):
integration_detail = IntegrationSerializer( integration_detail = IntegrationSerializer(read_only=True, source="integration")
read_only=True, source="integration"
)
class Meta: class Meta:
model = WorkspaceIntegration model = WorkspaceIntegration
+80 -156
View File
@@ -30,8 +30,6 @@ from plane.db.models import (
CommentReaction, CommentReaction,
IssueVote, IssueVote,
IssueRelation, IssueRelation,
State,
Project,
) )
@@ -71,26 +69,19 @@ class IssueProjectLiteSerializer(BaseSerializer):
##TODO: Find a better way to write this serializer ##TODO: Find a better way to write this serializer
## Find a better approach to save manytomany? ## Find a better approach to save manytomany?
class IssueCreateSerializer(BaseSerializer): class IssueCreateSerializer(BaseSerializer):
# ids state_detail = StateSerializer(read_only=True, source="state")
state_id = serializers.PrimaryKeyRelatedField( created_by_detail = UserLiteSerializer(read_only=True, source="created_by")
source="state", project_detail = ProjectLiteSerializer(read_only=True, source="project")
queryset=State.objects.all(), workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
required=False,
allow_null=True, assignees = serializers.ListField(
) child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
parent_id = serializers.PrimaryKeyRelatedField(
source="parent",
queryset=Issue.objects.all(),
required=False,
allow_null=True,
)
label_ids = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()),
write_only=True, write_only=True,
required=False, required=False,
) )
assignee_ids = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()), labels = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()),
write_only=True, write_only=True,
required=False, required=False,
) )
@@ -109,10 +100,8 @@ class IssueCreateSerializer(BaseSerializer):
def to_representation(self, instance): def to_representation(self, instance):
data = super().to_representation(instance) data = super().to_representation(instance)
assignee_ids = self.initial_data.get("assignee_ids") data['assignees'] = [str(assignee.id) for assignee in instance.assignees.all()]
data["assignee_ids"] = assignee_ids if assignee_ids else [] data['labels'] = [str(label.id) for label in instance.labels.all()]
label_ids = self.initial_data.get("label_ids")
data["label_ids"] = label_ids if label_ids else []
return data return data
def validate(self, data): def validate(self, data):
@@ -121,14 +110,12 @@ class IssueCreateSerializer(BaseSerializer):
and data.get("target_date", None) is not None and data.get("target_date", None) is not None
and data.get("start_date", None) > data.get("target_date", None) and data.get("start_date", None) > data.get("target_date", None)
): ):
raise serializers.ValidationError( raise serializers.ValidationError("Start date cannot exceed target date")
"Start date cannot exceed target date"
)
return data return data
def create(self, validated_data): def create(self, validated_data):
assignees = validated_data.pop("assignee_ids", None) assignees = validated_data.pop("assignees", None)
labels = validated_data.pop("label_ids", None) labels = validated_data.pop("labels", None)
project_id = self.context["project_id"] project_id = self.context["project_id"]
workspace_id = self.context["workspace_id"] workspace_id = self.context["workspace_id"]
@@ -186,8 +173,8 @@ class IssueCreateSerializer(BaseSerializer):
return issue return issue
def update(self, instance, validated_data): def update(self, instance, validated_data):
assignees = validated_data.pop("assignee_ids", None) assignees = validated_data.pop("assignees", None)
labels = validated_data.pop("label_ids", None) labels = validated_data.pop("labels", None)
# Related models # Related models
project_id = instance.project_id project_id = instance.project_id
@@ -238,15 +225,14 @@ 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( workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
read_only=True, source="workspace"
)
class Meta: class Meta:
model = IssueActivity model = IssueActivity
fields = "__all__" fields = "__all__"
class IssuePropertySerializer(BaseSerializer): class IssuePropertySerializer(BaseSerializer):
class Meta: class Meta:
model = IssueProperty model = IssueProperty
@@ -259,17 +245,12 @@ class IssuePropertySerializer(BaseSerializer):
class LabelSerializer(BaseSerializer): class LabelSerializer(BaseSerializer):
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
project_detail = ProjectLiteSerializer(source="project", read_only=True)
class Meta: class Meta:
model = Label model = Label
fields = [ fields = "__all__"
"parent",
"name",
"color",
"id",
"project_id",
"workspace_id",
"sort_order",
]
read_only_fields = [ read_only_fields = [
"workspace", "workspace",
"project", "project",
@@ -287,6 +268,7 @@ class LabelLiteSerializer(BaseSerializer):
class IssueLabelSerializer(BaseSerializer): class IssueLabelSerializer(BaseSerializer):
class Meta: class Meta:
model = IssueLabel model = IssueLabel
fields = "__all__" fields = "__all__"
@@ -297,50 +279,33 @@ class IssueLabelSerializer(BaseSerializer):
class IssueRelationSerializer(BaseSerializer): class IssueRelationSerializer(BaseSerializer):
id = serializers.UUIDField(source="related_issue.id", read_only=True) issue_detail = IssueProjectLiteSerializer(read_only=True, source="related_issue")
project_id = serializers.PrimaryKeyRelatedField(
source="related_issue.project_id", read_only=True
)
sequence_id = serializers.IntegerField(
source="related_issue.sequence_id", read_only=True
)
name = serializers.CharField(source="related_issue.name", read_only=True)
relation_type = serializers.CharField(read_only=True)
class Meta: class Meta:
model = IssueRelation model = IssueRelation
fields = [ fields = [
"id", "issue_detail",
"project_id",
"sequence_id",
"relation_type", "relation_type",
"name", "related_issue",
"issue",
"id"
] ]
read_only_fields = [ read_only_fields = [
"workspace", "workspace",
"project", "project",
] ]
class RelatedIssueSerializer(BaseSerializer): class RelatedIssueSerializer(BaseSerializer):
id = serializers.UUIDField(source="issue.id", read_only=True) issue_detail = IssueProjectLiteSerializer(read_only=True, source="issue")
project_id = serializers.PrimaryKeyRelatedField(
source="issue.project_id", read_only=True
)
sequence_id = serializers.IntegerField(
source="issue.sequence_id", read_only=True
)
name = serializers.CharField(source="issue.name", read_only=True)
relation_type = serializers.CharField(read_only=True)
class Meta: class Meta:
model = IssueRelation model = IssueRelation
fields = [ fields = [
"id", "issue_detail",
"project_id",
"sequence_id",
"relation_type", "relation_type",
"name", "related_issue",
"issue",
"id"
] ]
read_only_fields = [ read_only_fields = [
"workspace", "workspace",
@@ -435,8 +400,7 @@ class IssueLinkSerializer(BaseSerializer):
# Validation if url already exists # Validation if url already exists
def create(self, validated_data): def create(self, validated_data):
if IssueLink.objects.filter( if IssueLink.objects.filter(
url=validated_data.get("url"), url=validated_data.get("url"), issue_id=validated_data.get("issue_id")
issue_id=validated_data.get("issue_id"),
).exists(): ).exists():
raise serializers.ValidationError( raise serializers.ValidationError(
{"error": "URL already exists for this Issue"} {"error": "URL already exists for this Issue"}
@@ -460,6 +424,7 @@ 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:
@@ -473,6 +438,19 @@ class IssueReactionSerializer(BaseSerializer):
] ]
class CommentReactionLiteSerializer(BaseSerializer):
actor_detail = UserLiteSerializer(read_only=True, source="actor")
class Meta:
model = CommentReaction
fields = [
"id",
"reaction",
"comment",
"actor_detail",
]
class CommentReactionSerializer(BaseSerializer): class CommentReactionSerializer(BaseSerializer):
class Meta: class Meta:
model = CommentReaction model = CommentReaction
@@ -481,18 +459,12 @@ class CommentReactionSerializer(BaseSerializer):
class IssueVoteSerializer(BaseSerializer): class IssueVoteSerializer(BaseSerializer):
actor_detail = UserLiteSerializer(read_only=True, source="actor") actor_detail = UserLiteSerializer(read_only=True, source="actor")
class Meta: class Meta:
model = IssueVote model = IssueVote
fields = [ fields = ["issue", "vote", "workspace", "project", "actor", "actor_detail"]
"issue",
"vote",
"workspace",
"project",
"actor",
"actor_detail",
]
read_only_fields = fields read_only_fields = fields
@@ -500,12 +472,8 @@ class IssueCommentSerializer(BaseSerializer):
actor_detail = UserLiteSerializer(read_only=True, source="actor") actor_detail = UserLiteSerializer(read_only=True, source="actor")
issue_detail = IssueFlatSerializer(read_only=True, source="issue") issue_detail = IssueFlatSerializer(read_only=True, source="issue")
project_detail = ProjectLiteSerializer(read_only=True, source="project") project_detail = ProjectLiteSerializer(read_only=True, source="project")
workspace_detail = WorkspaceLiteSerializer( workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
read_only=True, source="workspace" comment_reactions = CommentReactionLiteSerializer(read_only=True, many=True)
)
comment_reactions = CommentReactionSerializer(
read_only=True, many=True
)
is_member = serializers.BooleanField(read_only=True) is_member = serializers.BooleanField(read_only=True)
class Meta: class Meta:
@@ -539,15 +507,12 @@ class IssueStateFlatSerializer(BaseSerializer):
# Issue Serializer with state details # Issue Serializer with state details
class IssueStateSerializer(DynamicBaseSerializer): class IssueStateSerializer(DynamicBaseSerializer):
label_details = LabelLiteSerializer( label_details = LabelLiteSerializer(read_only=True, source="labels", many=True)
read_only=True, source="labels", many=True
)
state_detail = StateLiteSerializer(read_only=True, source="state") state_detail = StateLiteSerializer(read_only=True, source="state")
project_detail = ProjectLiteSerializer(read_only=True, source="project") project_detail = ProjectLiteSerializer(read_only=True, source="project")
assignee_details = UserLiteSerializer( assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
read_only=True, source="assignees", many=True
)
sub_issues_count = serializers.IntegerField(read_only=True) sub_issues_count = serializers.IntegerField(read_only=True)
bridge_id = serializers.UUIDField(read_only=True)
attachment_count = serializers.IntegerField(read_only=True) attachment_count = serializers.IntegerField(read_only=True)
link_count = serializers.IntegerField(read_only=True) link_count = serializers.IntegerField(read_only=True)
@@ -556,80 +521,40 @@ class IssueStateSerializer(DynamicBaseSerializer):
fields = "__all__" fields = "__all__"
class IssueSerializer(DynamicBaseSerializer): class IssueSerializer(BaseSerializer):
# ids project_detail = ProjectLiteSerializer(read_only=True, source="project")
project_id = serializers.PrimaryKeyRelatedField(read_only=True) state_detail = StateSerializer(read_only=True, source="state")
state_id = serializers.PrimaryKeyRelatedField(read_only=True) parent_detail = IssueStateFlatSerializer(read_only=True, source="parent")
parent_id = serializers.PrimaryKeyRelatedField(read_only=True) label_details = LabelSerializer(read_only=True, source="labels", many=True)
cycle_id = serializers.PrimaryKeyRelatedField(read_only=True) assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
module_ids = serializers.SerializerMethodField() related_issues = IssueRelationSerializer(read_only=True, source="issue_relation", many=True)
issue_relations = RelatedIssueSerializer(read_only=True, source="issue_related", many=True)
# Many to many issue_cycle = IssueCycleDetailSerializer(read_only=True)
label_ids = serializers.PrimaryKeyRelatedField( issue_module = IssueModuleDetailSerializer(read_only=True)
read_only=True, many=True, source="labels" issue_link = IssueLinkSerializer(read_only=True, many=True)
) issue_attachment = IssueAttachmentSerializer(read_only=True, many=True)
assignee_ids = serializers.PrimaryKeyRelatedField(
read_only=True, many=True, source="assignees"
)
# Count items
sub_issues_count = serializers.IntegerField(read_only=True) sub_issues_count = serializers.IntegerField(read_only=True)
attachment_count = serializers.IntegerField(read_only=True) issue_reactions = IssueReactionSerializer(read_only=True, many=True)
link_count = serializers.IntegerField(read_only=True)
# is_subscribed
is_subscribed = serializers.BooleanField(read_only=True)
class Meta: class Meta:
model = Issue model = Issue
fields = [ fields = "__all__"
"id", read_only_fields = [
"name", "workspace",
"state_id", "project",
"description_html",
"sort_order",
"completed_at",
"estimate_point",
"priority",
"start_date",
"target_date",
"sequence_id",
"project_id",
"parent_id",
"cycle_id",
"module_ids",
"label_ids",
"assignee_ids",
"sub_issues_count",
"created_at",
"updated_at",
"created_by", "created_by",
"updated_by", "updated_by",
"attachment_count", "created_at",
"link_count", "updated_at",
"is_subscribed",
"is_draft",
"archived_at",
] ]
read_only_fields = fields
def get_module_ids(self, obj):
# Access the prefetched modules and extract module IDs
return [module for module in obj.issue_module.values_list("module_id", flat=True)]
class IssueLiteSerializer(DynamicBaseSerializer): class IssueLiteSerializer(DynamicBaseSerializer):
workspace_detail = WorkspaceLiteSerializer( workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
read_only=True, source="workspace"
)
project_detail = ProjectLiteSerializer(read_only=True, source="project") project_detail = ProjectLiteSerializer(read_only=True, source="project")
state_detail = StateLiteSerializer(read_only=True, source="state") state_detail = StateLiteSerializer(read_only=True, source="state")
label_details = LabelLiteSerializer( label_details = LabelLiteSerializer(read_only=True, source="labels", many=True)
read_only=True, source="labels", many=True assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
)
assignee_details = UserLiteSerializer(
read_only=True, source="assignees", many=True
)
sub_issues_count = serializers.IntegerField(read_only=True) sub_issues_count = serializers.IntegerField(read_only=True)
cycle_id = serializers.UUIDField(read_only=True) cycle_id = serializers.UUIDField(read_only=True)
module_id = serializers.UUIDField(read_only=True) module_id = serializers.UUIDField(read_only=True)
@@ -656,9 +581,7 @@ class IssueLiteSerializer(DynamicBaseSerializer):
class IssuePublicSerializer(BaseSerializer): class IssuePublicSerializer(BaseSerializer):
project_detail = ProjectLiteSerializer(read_only=True, source="project") project_detail = ProjectLiteSerializer(read_only=True, source="project")
state_detail = StateLiteSerializer(read_only=True, source="state") state_detail = StateLiteSerializer(read_only=True, source="state")
reactions = IssueReactionSerializer( reactions = IssueReactionSerializer(read_only=True, many=True, source="issue_reactions")
read_only=True, many=True, source="issue_reactions"
)
votes = IssueVoteSerializer(read_only=True, many=True) votes = IssueVoteSerializer(read_only=True, many=True)
class Meta: class Meta:
@@ -681,6 +604,7 @@ class IssuePublicSerializer(BaseSerializer):
read_only_fields = fields read_only_fields = fields
class IssueSubscriberSerializer(BaseSerializer): class IssueSubscriberSerializer(BaseSerializer):
class Meta: class Meta:
model = IssueSubscriber model = IssueSubscriber
+8 -27
View File
@@ -2,7 +2,7 @@
from rest_framework import serializers from rest_framework import serializers
# Module imports # Module imports
from .base import BaseSerializer, DynamicBaseSerializer from .base import BaseSerializer
from .user import UserLiteSerializer from .user import UserLiteSerializer
from .project import ProjectLiteSerializer from .project import ProjectLiteSerializer
from .workspace import WorkspaceLiteSerializer from .workspace import WorkspaceLiteSerializer
@@ -14,7 +14,6 @@ from plane.db.models import (
ModuleIssue, ModuleIssue,
ModuleLink, ModuleLink,
ModuleFavorite, ModuleFavorite,
ModuleUserProperties,
) )
@@ -26,9 +25,7 @@ class ModuleWriteSerializer(BaseSerializer):
) )
project_detail = ProjectLiteSerializer(source="project", read_only=True) project_detail = ProjectLiteSerializer(source="project", read_only=True)
workspace_detail = WorkspaceLiteSerializer( workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
source="workspace", read_only=True
)
class Meta: class Meta:
model = Module model = Module
@@ -44,18 +41,12 @@ 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 ( if data.get("start_date", None) is not None and data.get("target_date", None) is not None and data.get("start_date", None) > data.get("target_date", None):
data.get("start_date", None) is not None raise serializers.ValidationError("Start date cannot exceed target date")
and data.get("target_date", None) is not None
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):
@@ -160,8 +151,7 @@ class ModuleLinkSerializer(BaseSerializer):
# Validation if url already exists # Validation if url already exists
def create(self, validated_data): def create(self, validated_data):
if ModuleLink.objects.filter( if ModuleLink.objects.filter(
url=validated_data.get("url"), url=validated_data.get("url"), module_id=validated_data.get("module_id")
module_id=validated_data.get("module_id"),
).exists(): ).exists():
raise serializers.ValidationError( raise serializers.ValidationError(
{"error": "URL already exists for this Issue"} {"error": "URL already exists for this Issue"}
@@ -169,12 +159,10 @@ class ModuleLinkSerializer(BaseSerializer):
return ModuleLink.objects.create(**validated_data) return ModuleLink.objects.create(**validated_data)
class ModuleSerializer(DynamicBaseSerializer): class ModuleSerializer(BaseSerializer):
project_detail = ProjectLiteSerializer(read_only=True, source="project") project_detail = ProjectLiteSerializer(read_only=True, source="project")
lead_detail = UserLiteSerializer(read_only=True, source="lead") lead_detail = UserLiteSerializer(read_only=True, source="lead")
members_detail = UserLiteSerializer( members_detail = UserLiteSerializer(read_only=True, many=True, source="members")
read_only=True, many=True, source="members"
)
link_module = ModuleLinkSerializer(read_only=True, many=True) link_module = ModuleLinkSerializer(read_only=True, many=True)
is_favorite = serializers.BooleanField(read_only=True) is_favorite = serializers.BooleanField(read_only=True)
total_issues = serializers.IntegerField(read_only=True) total_issues = serializers.IntegerField(read_only=True)
@@ -208,10 +196,3 @@ class ModuleFavoriteSerializer(BaseSerializer):
"project", "project",
"user", "user",
] ]
class ModuleUserPropertiesSerializer(BaseSerializer):
class Meta:
model = ModuleUserProperties
fields = "__all__"
read_only_fields = ["workspace", "project", "module", "user"]
@@ -1,21 +1,12 @@
# Module imports # Module imports
from .base import BaseSerializer from .base import BaseSerializer
from .user import UserLiteSerializer from .user import UserLiteSerializer
from plane.db.models import Notification, UserNotificationPreference from plane.db.models import Notification
class NotificationSerializer(BaseSerializer): class NotificationSerializer(BaseSerializer):
triggered_by_details = UserLiteSerializer( triggered_by_details = UserLiteSerializer(read_only=True, source="triggered_by")
read_only=True, source="triggered_by"
)
class Meta: class Meta:
model = Notification model = Notification
fields = "__all__" fields = "__all__"
class UserNotificationPreferenceSerializer(BaseSerializer):
class Meta:
model = UserNotificationPreference
fields = "__all__"
+6 -18
View File
@@ -6,31 +6,19 @@ from .base import BaseSerializer
from .issue import IssueFlatSerializer, LabelLiteSerializer from .issue import IssueFlatSerializer, LabelLiteSerializer
from .workspace import WorkspaceLiteSerializer from .workspace import WorkspaceLiteSerializer
from .project import ProjectLiteSerializer from .project import ProjectLiteSerializer
from plane.db.models import ( from plane.db.models import Page, PageLog, PageFavorite, PageLabel, Label, Issue, Module
Page,
PageLog,
PageFavorite,
PageLabel,
Label,
Issue,
Module,
)
class PageSerializer(BaseSerializer): class PageSerializer(BaseSerializer):
is_favorite = serializers.BooleanField(read_only=True) is_favorite = serializers.BooleanField(read_only=True)
label_details = LabelLiteSerializer( label_details = LabelLiteSerializer(read_only=True, source="labels", many=True)
read_only=True, source="labels", many=True
)
labels = serializers.ListField( labels = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()), child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()),
write_only=True, write_only=True,
required=False, required=False,
) )
project_detail = ProjectLiteSerializer(source="project", read_only=True) project_detail = ProjectLiteSerializer(source="project", read_only=True)
workspace_detail = WorkspaceLiteSerializer( workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
source="workspace", read_only=True
)
class Meta: class Meta:
model = Page model = Page
@@ -40,10 +28,9 @@ class PageSerializer(BaseSerializer):
"project", "project",
"owned_by", "owned_by",
] ]
def to_representation(self, instance): def to_representation(self, instance):
data = super().to_representation(instance) data = super().to_representation(instance)
data["labels"] = [str(label.id) for label in instance.labels.all()] data['labels'] = [str(label.id) for label in instance.labels.all()]
return data return data
def create(self, validated_data): def create(self, validated_data):
@@ -107,7 +94,7 @@ class SubPageSerializer(BaseSerializer):
def get_entity_details(self, obj): def get_entity_details(self, obj):
entity_name = obj.entity_name entity_name = obj.entity_name
if entity_name == "forward_link" or entity_name == "back_link": if entity_name == 'forward_link' or entity_name == 'back_link':
try: try:
page = Page.objects.get(pk=obj.entity_identifier) page = Page.objects.get(pk=obj.entity_identifier)
return PageSerializer(page).data return PageSerializer(page).data
@@ -117,6 +104,7 @@ class SubPageSerializer(BaseSerializer):
class PageLogSerializer(BaseSerializer): class PageLogSerializer(BaseSerializer):
class Meta: class Meta:
model = PageLog model = PageLog
fields = "__all__" fields = "__all__"
+6 -25
View File
@@ -4,10 +4,7 @@ from rest_framework import serializers
# Module imports # Module imports
from .base import BaseSerializer, DynamicBaseSerializer from .base import BaseSerializer, DynamicBaseSerializer
from plane.app.serializers.workspace import WorkspaceLiteSerializer from plane.app.serializers.workspace import WorkspaceLiteSerializer
from plane.app.serializers.user import ( from plane.app.serializers.user import UserLiteSerializer, UserAdminLiteSerializer
UserLiteSerializer,
UserAdminLiteSerializer,
)
from plane.db.models import ( from plane.db.models import (
Project, Project,
ProjectMember, ProjectMember,
@@ -20,9 +17,7 @@ from plane.db.models import (
class ProjectSerializer(BaseSerializer): class ProjectSerializer(BaseSerializer):
workspace_detail = WorkspaceLiteSerializer( workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
source="workspace", read_only=True
)
class Meta: class Meta:
model = Project model = Project
@@ -34,16 +29,12 @@ class ProjectSerializer(BaseSerializer):
def create(self, validated_data): def create(self, validated_data):
identifier = validated_data.get("identifier", "").strip().upper() identifier = validated_data.get("identifier", "").strip().upper()
if identifier == "": if identifier == "":
raise serializers.ValidationError( raise serializers.ValidationError(detail="Project Identifier is required")
detail="Project Identifier is required"
)
if ProjectIdentifier.objects.filter( if ProjectIdentifier.objects.filter(
name=identifier, workspace_id=self.context["workspace_id"] name=identifier, workspace_id=self.context["workspace_id"]
).exists(): ).exists():
raise serializers.ValidationError( raise serializers.ValidationError(detail="Project Identifier is taken")
detail="Project Identifier is taken"
)
project = Project.objects.create( project = Project.objects.create(
**validated_data, workspace_id=self.context["workspace_id"] **validated_data, workspace_id=self.context["workspace_id"]
) )
@@ -82,9 +73,7 @@ class ProjectSerializer(BaseSerializer):
return project return project
# If not same fail update # If not same fail update
raise serializers.ValidationError( raise serializers.ValidationError(detail="Project Identifier is already taken")
detail="Project Identifier is already taken"
)
class ProjectLiteSerializer(BaseSerializer): class ProjectLiteSerializer(BaseSerializer):
@@ -171,12 +160,6 @@ class ProjectMemberAdminSerializer(BaseSerializer):
fields = "__all__" fields = "__all__"
class ProjectMemberRoleSerializer(DynamicBaseSerializer):
class Meta:
model = ProjectMember
fields = ("id", "role", "member", "project")
class ProjectMemberInviteSerializer(BaseSerializer): class ProjectMemberInviteSerializer(BaseSerializer):
project = ProjectLiteSerializer(read_only=True) project = ProjectLiteSerializer(read_only=True)
workspace = WorkspaceLiteSerializer(read_only=True) workspace = WorkspaceLiteSerializer(read_only=True)
@@ -214,9 +197,7 @@ class ProjectMemberLiteSerializer(BaseSerializer):
class ProjectDeployBoardSerializer(BaseSerializer): class ProjectDeployBoardSerializer(BaseSerializer):
project_details = ProjectLiteSerializer(read_only=True, source="project") project_details = ProjectLiteSerializer(read_only=True, source="project")
workspace_detail = WorkspaceLiteSerializer( workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
read_only=True, source="workspace"
)
class Meta: class Meta:
model = ProjectDeployBoard model = ProjectDeployBoard
+2 -11
View File
@@ -6,19 +6,10 @@ from plane.db.models import State
class StateSerializer(BaseSerializer): class StateSerializer(BaseSerializer):
class Meta: class Meta:
model = State model = State
fields = [ fields = "__all__"
"id",
"project_id",
"workspace_id",
"name",
"color",
"group",
"default",
"description",
"sequence",
]
read_only_fields = [ read_only_fields = [
"workspace", "workspace",
"project", "project",
+3 -9
View File
@@ -99,9 +99,7 @@ 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 "last_workspace_slug": workspace.slug if workspace is not None else "",
if workspace is not None
else "",
"fallback_workspace_id": obj.last_workspace_id, "fallback_workspace_id": obj.last_workspace_id,
"fallback_workspace_slug": workspace.slug "fallback_workspace_slug": workspace.slug
if workspace is not None if workspace is not None
@@ -111,8 +109,7 @@ class UserMeSettingsSerializer(BaseSerializer):
else: else:
fallback_workspace = ( fallback_workspace = (
Workspace.objects.filter( Workspace.objects.filter(
workspace_member__member_id=obj.id, workspace_member__member_id=obj.id, workspace_member__is_active=True
workspace_member__is_active=True,
) )
.order_by("created_at") .order_by("created_at")
.first() .first()
@@ -183,9 +180,7 @@ 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
@@ -195,5 +190,4 @@ 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)
+4 -8
View File
@@ -2,7 +2,7 @@
from rest_framework import serializers from rest_framework import serializers
# Module imports # Module imports
from .base import BaseSerializer, DynamicBaseSerializer from .base import BaseSerializer
from .workspace import WorkspaceLiteSerializer from .workspace import WorkspaceLiteSerializer
from .project import ProjectLiteSerializer from .project import ProjectLiteSerializer
from plane.db.models import GlobalView, IssueView, IssueViewFavorite from plane.db.models import GlobalView, IssueView, IssueViewFavorite
@@ -10,9 +10,7 @@ from plane.utils.issue_filters import issue_filters
class GlobalViewSerializer(BaseSerializer): class GlobalViewSerializer(BaseSerializer):
workspace_detail = WorkspaceLiteSerializer( workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
source="workspace", read_only=True
)
class Meta: class Meta:
model = GlobalView model = GlobalView
@@ -40,12 +38,10 @@ class GlobalViewSerializer(BaseSerializer):
return super().update(instance, validated_data) return super().update(instance, validated_data)
class IssueViewSerializer(DynamicBaseSerializer): class IssueViewSerializer(BaseSerializer):
is_favorite = serializers.BooleanField(read_only=True) is_favorite = serializers.BooleanField(read_only=True)
project_detail = ProjectLiteSerializer(source="project", read_only=True) project_detail = ProjectLiteSerializer(source="project", read_only=True)
workspace_detail = WorkspaceLiteSerializer( workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
source="workspace", read_only=True
)
class Meta: class Meta:
model = IssueView model = IssueView
+24 -54
View File
@@ -12,7 +12,6 @@ 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])
@@ -22,49 +21,32 @@ 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( raise serializers.ValidationError({"url": "Invalid URL: No hostname found."})
{"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( raise serializers.ValidationError({"url": "Hostname could not be resolved."})
{"url": "Hostname could not be resolved."}
)
if not ip_addresses: if not ip_addresses:
raise serializers.ValidationError( raise serializers.ValidationError({"url": "No IP addresses found for the hostname."})
{"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( raise serializers.ValidationError({"url": "URL resolves to a blocked IP address."})
{"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 = [ disallowed_domains = ['plane.so',] # Add your disallowed domains here
"plane.so",
] # Add your disallowed domains here
if request: if request:
request_host = request.get_host().split(":")[ request_host = request.get_host().split(':')[0] # Remove port if present
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( if any(hostname == domain or hostname.endswith('.' + domain) for domain in disallowed_domains):
hostname == domain or hostname.endswith("." + domain) raise serializers.ValidationError({"url": "URL domain or its subdomain is not allowed."})
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)
@@ -74,49 +56,32 @@ 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( raise serializers.ValidationError({"url": "Invalid URL: No hostname found."})
{"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( raise serializers.ValidationError({"url": "Hostname could not be resolved."})
{"url": "Hostname could not be resolved."}
)
if not ip_addresses: if not ip_addresses:
raise serializers.ValidationError( raise serializers.ValidationError({"url": "No IP addresses found for the hostname."})
{"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( raise serializers.ValidationError({"url": "URL resolves to a blocked IP address."})
{"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 = [ disallowed_domains = ['plane.so',] # Add your disallowed domains here
"plane.so",
] # Add your disallowed domains here
if request: if request:
request_host = request.get_host().split(":")[ request_host = request.get_host().split(':')[0] # Remove port if present
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( if any(hostname == domain or hostname.endswith('.' + domain) for domain in disallowed_domains):
hostname == domain or hostname.endswith("." + domain) raise serializers.ValidationError({"url": "URL domain or its subdomain is not allowed."})
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)
@@ -130,7 +95,12 @@ class WebhookSerializer(DynamicBaseSerializer):
class WebhookLogSerializer(DynamicBaseSerializer): class WebhookLogSerializer(DynamicBaseSerializer):
class Meta: class Meta:
model = WebhookLog model = WebhookLog
fields = "__all__" fields = "__all__"
read_only_fields = ["workspace", "webhook"] read_only_fields = [
"workspace",
"webhook"
]
+8 -22
View File
@@ -2,7 +2,7 @@
from rest_framework import serializers from rest_framework import serializers
# Module imports # Module imports
from .base import BaseSerializer, DynamicBaseSerializer from .base import BaseSerializer
from .user import UserLiteSerializer, UserAdminLiteSerializer from .user import UserLiteSerializer, UserAdminLiteSerializer
from plane.db.models import ( from plane.db.models import (
@@ -13,11 +13,10 @@ from plane.db.models import (
TeamMember, TeamMember,
WorkspaceMemberInvite, WorkspaceMemberInvite,
WorkspaceTheme, WorkspaceTheme,
WorkspaceUserProperties,
) )
class WorkSpaceSerializer(DynamicBaseSerializer): class WorkSpaceSerializer(BaseSerializer):
owner = UserLiteSerializer(read_only=True) owner = UserLiteSerializer(read_only=True)
total_members = serializers.IntegerField(read_only=True) total_members = serializers.IntegerField(read_only=True)
total_issues = serializers.IntegerField(read_only=True) total_issues = serializers.IntegerField(read_only=True)
@@ -51,7 +50,6 @@ class WorkSpaceSerializer(DynamicBaseSerializer):
"owner", "owner",
] ]
class WorkspaceLiteSerializer(BaseSerializer): class WorkspaceLiteSerializer(BaseSerializer):
class Meta: class Meta:
model = Workspace model = Workspace
@@ -63,7 +61,8 @@ class WorkspaceLiteSerializer(BaseSerializer):
read_only_fields = fields read_only_fields = fields
class WorkSpaceMemberSerializer(DynamicBaseSerializer):
class WorkSpaceMemberSerializer(BaseSerializer):
member = UserLiteSerializer(read_only=True) member = UserLiteSerializer(read_only=True)
workspace = WorkspaceLiteSerializer(read_only=True) workspace = WorkspaceLiteSerializer(read_only=True)
@@ -73,12 +72,13 @@ class WorkSpaceMemberSerializer(DynamicBaseSerializer):
class WorkspaceMemberMeSerializer(BaseSerializer): class WorkspaceMemberMeSerializer(BaseSerializer):
class Meta: class Meta:
model = WorkspaceMember model = WorkspaceMember
fields = "__all__" fields = "__all__"
class WorkspaceMemberAdminSerializer(DynamicBaseSerializer): class WorkspaceMemberAdminSerializer(BaseSerializer):
member = UserAdminLiteSerializer(read_only=True) member = UserAdminLiteSerializer(read_only=True)
workspace = WorkspaceLiteSerializer(read_only=True) workspace = WorkspaceLiteSerializer(read_only=True)
@@ -108,9 +108,7 @@ class WorkSpaceMemberInviteSerializer(BaseSerializer):
class TeamSerializer(BaseSerializer): class TeamSerializer(BaseSerializer):
members_detail = UserLiteSerializer( members_detail = UserLiteSerializer(read_only=True, source="members", many=True)
read_only=True, source="members", many=True
)
members = serializers.ListField( members = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()), child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
write_only=True, write_only=True,
@@ -147,9 +145,7 @@ class TeamSerializer(BaseSerializer):
members = validated_data.pop("members") members = validated_data.pop("members")
TeamMember.objects.filter(team=instance).delete() TeamMember.objects.filter(team=instance).delete()
team_members = [ team_members = [
TeamMember( TeamMember(member=member, team=instance, workspace=instance.workspace)
member=member, team=instance, workspace=instance.workspace
)
for member in members for member in members
] ]
TeamMember.objects.bulk_create(team_members, batch_size=10) TeamMember.objects.bulk_create(team_members, batch_size=10)
@@ -165,13 +161,3 @@ class WorkspaceThemeSerializer(BaseSerializer):
"workspace", "workspace",
"actor", "actor",
] ]
class WorkspaceUserPropertiesSerializer(BaseSerializer):
class Meta:
model = WorkspaceUserProperties
fields = "__all__"
read_only_fields = [
"workspace",
"user",
]
-2
View File
@@ -3,7 +3,6 @@ from .asset import urlpatterns as asset_urls
from .authentication import urlpatterns as authentication_urls from .authentication import urlpatterns as authentication_urls
from .config import urlpatterns as configuration_urls from .config import urlpatterns as configuration_urls
from .cycle import urlpatterns as cycle_urls from .cycle import urlpatterns as cycle_urls
from .dashboard import urlpatterns as dashboard_urls
from .estimate import urlpatterns as estimate_urls from .estimate import urlpatterns as estimate_urls
from .external import urlpatterns as external_urls from .external import urlpatterns as external_urls
from .importer import urlpatterns as importer_urls from .importer import urlpatterns as importer_urls
@@ -29,7 +28,6 @@ urlpatterns = [
*authentication_urls, *authentication_urls,
*configuration_urls, *configuration_urls,
*cycle_urls, *cycle_urls,
*dashboard_urls,
*estimate_urls, *estimate_urls,
*external_urls, *external_urls,
*importer_urls, *importer_urls,
+3 -11
View File
@@ -31,14 +31,8 @@ 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( path("magic-generate/", MagicGenerateEndpoint.as_view(), name="magic-generate"),
"magic-generate/", path("magic-sign-in/", MagicSignInEndpoint.as_view(), name="magic-sign-in"),
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(
@@ -58,8 +52,6 @@ urlpatterns = [
), ),
# API Tokens # API Tokens
path("api-tokens/", ApiTokenEndpoint.as_view(), name="api-tokens"), path("api-tokens/", ApiTokenEndpoint.as_view(), name="api-tokens"),
path( path("api-tokens/<uuid:pk>/", ApiTokenEndpoint.as_view(), name="api-tokens"),
"api-tokens/<uuid:pk>/", ApiTokenEndpoint.as_view(), name="api-tokens"
),
## End API Tokens ## End API Tokens
] ]
+1 -6
View File
@@ -1,7 +1,7 @@
from django.urls import path from django.urls import path
from plane.app.views import ConfigurationEndpoint, MobileConfigurationEndpoint from plane.app.views import ConfigurationEndpoint
urlpatterns = [ urlpatterns = [
path( path(
@@ -9,9 +9,4 @@ urlpatterns = [
ConfigurationEndpoint.as_view(), ConfigurationEndpoint.as_view(),
name="configuration", name="configuration",
), ),
path(
"mobile-configs/",
MobileConfigurationEndpoint.as_view(),
name="configuration",
),
] ]
+1 -7
View File
@@ -7,7 +7,6 @@ from plane.app.views import (
CycleDateCheckEndpoint, CycleDateCheckEndpoint,
CycleFavoriteViewSet, CycleFavoriteViewSet,
TransferCycleIssueEndpoint, TransferCycleIssueEndpoint,
CycleUserPropertiesEndpoint,
) )
@@ -45,7 +44,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:issue_id>/", "workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/cycle-issues/<uuid:pk>/",
CycleIssueViewSet.as_view( CycleIssueViewSet.as_view(
{ {
"get": "retrieve", "get": "retrieve",
@@ -85,9 +84,4 @@ urlpatterns = [
TransferCycleIssueEndpoint.as_view(), TransferCycleIssueEndpoint.as_view(),
name="transfer-issues", name="transfer-issues",
), ),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/user-properties/",
CycleUserPropertiesEndpoint.as_view(),
name="cycle-user-filters",
),
] ]
-23
View File
@@ -1,23 +0,0 @@
from django.urls import path
from plane.app.views import DashboardEndpoint, WidgetsEndpoint
urlpatterns = [
path(
"workspaces/<str:slug>/dashboard/",
DashboardEndpoint.as_view(),
name="dashboard",
),
path(
"workspaces/<str:slug>/dashboard/<uuid:dashboard_id>/",
DashboardEndpoint.as_view(),
name="dashboard",
),
path(
"dashboard/<uuid:dashboard_id>/widgets/<uuid:widget_id>/",
WidgetsEndpoint.as_view(),
name="widgets",
),
]
+1 -1
View File
@@ -40,7 +40,7 @@ urlpatterns = [
name="inbox-issue", name="inbox-issue",
), ),
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/<uuid:inbox_id>/inbox-issues/<uuid:issue_id>/", "workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/<uuid:inbox_id>/inbox-issues/<uuid:pk>/",
InboxIssueViewSet.as_view( InboxIssueViewSet.as_view(
{ {
"get": "retrieve", "get": "retrieve",
+3 -4
View File
@@ -235,7 +235,7 @@ urlpatterns = [
## End Comment Reactions ## End Comment Reactions
## IssueProperty ## IssueProperty
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-properties/", "workspaces/<str:slug>/projects/<uuid:project_id>/issue-display-properties/",
IssueUserDisplayPropertyEndpoint.as_view(), IssueUserDisplayPropertyEndpoint.as_view(),
name="project-issue-display-properties", name="project-issue-display-properties",
), ),
@@ -275,17 +275,16 @@ urlpatterns = [
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-relation/", "workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-relation/",
IssueRelationViewSet.as_view( IssueRelationViewSet.as_view(
{ {
"get": "list",
"post": "create", "post": "create",
} }
), ),
name="issue-relation", name="issue-relation",
), ),
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/remove-relation/", "workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-relation/<uuid:pk>/",
IssueRelationViewSet.as_view( IssueRelationViewSet.as_view(
{ {
"post": "remove_relation", "delete": "destroy",
} }
), ),
name="issue-relation", name="issue-relation",
+3 -18
View File
@@ -7,7 +7,6 @@ from plane.app.views import (
ModuleLinkViewSet, ModuleLinkViewSet,
ModuleFavoriteViewSet, ModuleFavoriteViewSet,
BulkImportModulesEndpoint, BulkImportModulesEndpoint,
ModuleUserPropertiesEndpoint,
) )
@@ -35,26 +34,17 @@ urlpatterns = [
name="project-modules", name="project-modules",
), ),
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/modules/", "workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/module-issues/",
ModuleIssueViewSet.as_view( ModuleIssueViewSet.as_view(
{ {
"post": "create_issue_modules",
}
),
name="issue-module",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/issues/",
ModuleIssueViewSet.as_view(
{
"post": "create_module_issues",
"get": "list", "get": "list",
"post": "create",
} }
), ),
name="project-module-issues", name="project-module-issues",
), ),
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/issues/<uuid:issue_id>/", "workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/module-issues/<uuid:pk>/",
ModuleIssueViewSet.as_view( ModuleIssueViewSet.as_view(
{ {
"get": "retrieve", "get": "retrieve",
@@ -111,9 +101,4 @@ urlpatterns = [
BulkImportModulesEndpoint.as_view(), BulkImportModulesEndpoint.as_view(),
name="bulk-modules-create", name="bulk-modules-create",
), ),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/user-properties/",
ModuleUserPropertiesEndpoint.as_view(),
name="cycle-user-filters",
),
] ]
-6
View File
@@ -5,7 +5,6 @@ from plane.app.views import (
NotificationViewSet, NotificationViewSet,
UnreadNotificationEndpoint, UnreadNotificationEndpoint,
MarkAllReadNotificationViewSet, MarkAllReadNotificationViewSet,
UserNotificationPreferenceEndpoint,
) )
@@ -64,9 +63,4 @@ urlpatterns = [
), ),
name="mark-all-read-notifications", name="mark-all-read-notifications",
), ),
path(
"users/me/notification-preferences/",
UserNotificationPreferenceEndpoint.as_view(),
name="user-notification-preferences",
),
] ]
-24
View File
@@ -18,10 +18,6 @@ from plane.app.views import (
WorkspaceUserProfileEndpoint, WorkspaceUserProfileEndpoint,
WorkspaceUserProfileIssuesEndpoint, WorkspaceUserProfileIssuesEndpoint,
WorkspaceLabelsEndpoint, WorkspaceLabelsEndpoint,
WorkspaceProjectMemberEndpoint,
WorkspaceUserPropertiesEndpoint,
WorkspaceStatesEndpoint,
WorkspaceEstimatesEndpoint,
) )
@@ -96,11 +92,6 @@ urlpatterns = [
WorkSpaceMemberViewSet.as_view({"get": "list"}), WorkSpaceMemberViewSet.as_view({"get": "list"}),
name="workspace-member", name="workspace-member",
), ),
path(
"workspaces/<str:slug>/project-members/",
WorkspaceProjectMemberEndpoint.as_view(),
name="workspace-member-roles",
),
path( path(
"workspaces/<str:slug>/members/<uuid:pk>/", "workspaces/<str:slug>/members/<uuid:pk>/",
WorkSpaceMemberViewSet.as_view( WorkSpaceMemberViewSet.as_view(
@@ -204,19 +195,4 @@ urlpatterns = [
WorkspaceLabelsEndpoint.as_view(), WorkspaceLabelsEndpoint.as_view(),
name="workspace-labels", name="workspace-labels",
), ),
path(
"workspaces/<str:slug>/user-properties/",
WorkspaceUserPropertiesEndpoint.as_view(),
name="workspace-user-filters",
),
path(
"workspaces/<str:slug>/states/",
WorkspaceStatesEndpoint.as_view(),
name="workspace-state",
),
path(
"workspaces/<str:slug>/estimates/",
WorkspaceEstimatesEndpoint.as_view(),
name="workspace-estimate",
),
] ]
+12 -25
View File
@@ -192,7 +192,7 @@ from plane.app.views import (
) )
# TODO: Delete this file #TODO: Delete this file
# This url file has been deprecated use apiserver/plane/urls folder to create new urls # This url file has been deprecated use apiserver/plane/urls folder to create new urls
urlpatterns = [ urlpatterns = [
@@ -204,14 +204,10 @@ urlpatterns = [
path("sign-out/", SignOutEndpoint.as_view(), name="sign-out"), path("sign-out/", SignOutEndpoint.as_view(), name="sign-out"),
# Magic Sign In/Up # Magic Sign In/Up
path( path(
"magic-generate/", "magic-generate/", MagicSignInGenerateEndpoint.as_view(), name="magic-generate"
MagicSignInGenerateEndpoint.as_view(),
name="magic-generate",
), ),
path( path("magic-sign-in/", MagicSignInEndpoint.as_view(), name="magic-sign-in"),
"magic-sign-in/", MagicSignInEndpoint.as_view(), name="magic-sign-in" path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
),
path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
# Email verification # Email verification
path("email-verify/", VerifyEmailEndpoint.as_view(), name="email-verify"), path("email-verify/", VerifyEmailEndpoint.as_view(), name="email-verify"),
path( path(
@@ -276,9 +272,7 @@ urlpatterns = [
# user workspace invitations # user workspace invitations
path( path(
"users/me/invitations/workspaces/", "users/me/invitations/workspaces/",
UserWorkspaceInvitationsEndpoint.as_view( UserWorkspaceInvitationsEndpoint.as_view({"get": "list", "post": "create"}),
{"get": "list", "post": "create"}
),
name="user-workspace-invitations", name="user-workspace-invitations",
), ),
# user workspace invitation # user workspace invitation
@@ -317,9 +311,7 @@ urlpatterns = [
# user project invitations # user project invitations
path( path(
"users/me/invitations/projects/", "users/me/invitations/projects/",
UserProjectInvitationsViewset.as_view( UserProjectInvitationsViewset.as_view({"get": "list", "post": "create"}),
{"get": "list", "post": "create"}
),
name="user-project-invitaions", name="user-project-invitaions",
), ),
## Workspaces ## ## Workspaces ##
@@ -1246,7 +1238,7 @@ urlpatterns = [
"post": "unarchive", "post": "unarchive",
} }
), ),
name="project-page-unarchive", name="project-page-unarchive"
), ),
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-pages/", "workspaces/<str:slug>/projects/<uuid:project_id>/archived-pages/",
@@ -1272,22 +1264,19 @@ urlpatterns = [
{ {
"post": "unlock", "post": "unlock",
} }
), )
), ),
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/transactions/", "workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/transactions/",
PageLogEndpoint.as_view(), PageLogEndpoint.as_view(), name="page-transactions"
name="page-transactions",
), ),
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/transactions/<uuid:transaction>/", "workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/transactions/<uuid:transaction>/",
PageLogEndpoint.as_view(), PageLogEndpoint.as_view(), name="page-transactions"
name="page-transactions",
), ),
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/sub-pages/", "workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/sub-pages/",
SubPagesEndpoint.as_view(), SubPagesEndpoint.as_view(), name="sub-page"
name="sub-page",
), ),
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/", "workspaces/<str:slug>/projects/<uuid:project_id>/estimates/",
@@ -1337,9 +1326,7 @@ urlpatterns = [
## End Pages ## End Pages
# API Tokens # API Tokens
path("api-tokens/", ApiTokenEndpoint.as_view(), name="api-tokens"), path("api-tokens/", ApiTokenEndpoint.as_view(), name="api-tokens"),
path( path("api-tokens/<uuid:pk>/", ApiTokenEndpoint.as_view(), name="api-tokens"),
"api-tokens/<uuid:pk>/", ApiTokenEndpoint.as_view(), name="api-tokens"
),
## End API Tokens ## End API Tokens
# Integrations # Integrations
path( path(
+2 -18
View File
@@ -45,10 +45,6 @@ from .workspace import (
WorkspaceUserProfileEndpoint, WorkspaceUserProfileEndpoint,
WorkspaceUserProfileIssuesEndpoint, WorkspaceUserProfileIssuesEndpoint,
WorkspaceLabelsEndpoint, WorkspaceLabelsEndpoint,
WorkspaceProjectMemberEndpoint,
WorkspaceUserPropertiesEndpoint,
WorkspaceStatesEndpoint,
WorkspaceEstimatesEndpoint,
) )
from .state import StateViewSet from .state import StateViewSet
from .view import ( from .view import (
@@ -63,7 +59,6 @@ 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 (
@@ -108,7 +103,6 @@ from .module import (
ModuleIssueViewSet, ModuleIssueViewSet,
ModuleLinkViewSet, ModuleLinkViewSet,
ModuleFavoriteViewSet, ModuleFavoriteViewSet,
ModuleUserPropertiesEndpoint,
) )
from .api import ApiTokenEndpoint from .api import ApiTokenEndpoint
@@ -142,11 +136,7 @@ from .page import (
from .search import GlobalSearchEndpoint, IssueSearchEndpoint from .search import GlobalSearchEndpoint, IssueSearchEndpoint
from .external import ( from .external import GPTIntegrationEndpoint, ReleaseNotesEndpoint, UnsplashEndpoint
GPTIntegrationEndpoint,
ReleaseNotesEndpoint,
UnsplashEndpoint,
)
from .estimate import ( from .estimate import (
ProjectEstimatePointEndpoint, ProjectEstimatePointEndpoint,
@@ -167,20 +157,14 @@ from .notification import (
NotificationViewSet, NotificationViewSet,
UnreadNotificationEndpoint, UnreadNotificationEndpoint,
MarkAllReadNotificationViewSet, MarkAllReadNotificationViewSet,
UserNotificationPreferenceEndpoint,
) )
from .exporter import ExportIssuesEndpoint from .exporter import ExportIssuesEndpoint
from .config import ConfigurationEndpoint, MobileConfigurationEndpoint from .config import ConfigurationEndpoint
from .webhook import ( from .webhook import (
WebhookEndpoint, WebhookEndpoint,
WebhookLogsEndpoint, WebhookLogsEndpoint,
WebhookSecretRegenerateEndpoint, WebhookSecretRegenerateEndpoint,
) )
from .dashboard import (
DashboardEndpoint,
WidgetsEndpoint
)
+10 -28
View File
@@ -61,9 +61,7 @@ class AnalyticsEndpoint(BaseAPIView):
) )
# If segment is present it cannot be same as x-axis # If segment is present it cannot be same as x-axis
if segment and ( if segment and (segment not in valid_xaxis_segment or x_axis == segment):
segment not in valid_xaxis_segment or x_axis == segment
):
return Response( return Response(
{ {
"error": "Both segment and x axis cannot be same and segment should be valid" "error": "Both segment and x axis cannot be same and segment should be valid"
@@ -112,9 +110,7 @@ class AnalyticsEndpoint(BaseAPIView):
if x_axis in ["assignees__id"] or segment in ["assignees__id"]: if x_axis in ["assignees__id"] or segment in ["assignees__id"]:
assignee_details = ( assignee_details = (
Issue.issue_objects.filter( Issue.issue_objects.filter(
workspace__slug=slug, workspace__slug=slug, **filters, assignees__avatar__isnull=False
**filters,
assignees__avatar__isnull=False,
) )
.order_by("assignees__id") .order_by("assignees__id")
.distinct("assignees__id") .distinct("assignees__id")
@@ -128,9 +124,7 @@ class AnalyticsEndpoint(BaseAPIView):
) )
cycle_details = {} cycle_details = {}
if x_axis in ["issue_cycle__cycle_id"] or segment in [ if x_axis in ["issue_cycle__cycle_id"] or segment in ["issue_cycle__cycle_id"]:
"issue_cycle__cycle_id"
]:
cycle_details = ( cycle_details = (
Issue.issue_objects.filter( Issue.issue_objects.filter(
workspace__slug=slug, workspace__slug=slug,
@@ -192,9 +186,7 @@ class AnalyticViewViewset(BaseViewSet):
def get_queryset(self): def get_queryset(self):
return self.filter_queryset( return self.filter_queryset(
super() super().get_queryset().filter(workspace__slug=self.kwargs.get("slug"))
.get_queryset()
.filter(workspace__slug=self.kwargs.get("slug"))
) )
@@ -204,9 +196,7 @@ class SavedAnalyticEndpoint(BaseAPIView):
] ]
def get(self, request, slug, analytic_id): def get(self, request, slug, analytic_id):
analytic_view = AnalyticView.objects.get( analytic_view = AnalyticView.objects.get(pk=analytic_id, workspace__slug=slug)
pk=analytic_id, workspace__slug=slug
)
filter = analytic_view.query filter = analytic_view.query
queryset = Issue.issue_objects.filter(**filter) queryset = Issue.issue_objects.filter(**filter)
@@ -276,9 +266,7 @@ class ExportAnalyticsEndpoint(BaseAPIView):
) )
# If segment is present it cannot be same as x-axis # If segment is present it cannot be same as x-axis
if segment and ( if segment and (segment not in valid_xaxis_segment or x_axis == segment):
segment not in valid_xaxis_segment or x_axis == segment
):
return Response( return Response(
{ {
"error": "Both segment and x axis cannot be same and segment should be valid" "error": "Both segment and x axis cannot be same and segment should be valid"
@@ -305,9 +293,7 @@ class DefaultAnalyticsEndpoint(BaseAPIView):
def get(self, request, slug): def get(self, request, slug):
filters = issue_filters(request.GET, "GET") filters = issue_filters(request.GET, "GET")
base_issues = Issue.issue_objects.filter( base_issues = Issue.issue_objects.filter(workspace__slug=slug, **filters)
workspace__slug=slug, **filters
)
total_issues = base_issues.count() total_issues = base_issues.count()
@@ -320,9 +306,7 @@ class DefaultAnalyticsEndpoint(BaseAPIView):
) )
open_issues_groups = ["backlog", "unstarted", "started"] open_issues_groups = ["backlog", "unstarted", "started"]
open_issues_queryset = state_groups.filter( open_issues_queryset = state_groups.filter(state__group__in=open_issues_groups)
state__group__in=open_issues_groups
)
open_issues = open_issues_queryset.count() open_issues = open_issues_queryset.count()
open_issues_classified = ( open_issues_classified = (
@@ -377,12 +361,10 @@ class DefaultAnalyticsEndpoint(BaseAPIView):
.order_by("-count") .order_by("-count")
) )
open_estimate_sum = open_issues_queryset.aggregate( open_estimate_sum = open_issues_queryset.aggregate(sum=Sum("estimate_point"))[
sum=Sum("estimate_point")
)["sum"]
total_estimate_sum = base_issues.aggregate(sum=Sum("estimate_point"))[
"sum" "sum"
] ]
total_estimate_sum = base_issues.aggregate(sum=Sum("estimate_point"))["sum"]
return Response( return Response(
{ {
+1 -3
View File
@@ -71,9 +71,7 @@ class ApiTokenEndpoint(BaseAPIView):
user=request.user, user=request.user,
pk=pk, pk=pk,
) )
serializer = APITokenSerializer( serializer = APITokenSerializer(api_token, data=request.data, partial=True)
api_token, data=request.data, partial=True
)
if serializer.is_valid(): if serializer.is_valid():
serializer.save() serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
+11 -33
View File
@@ -10,11 +10,7 @@ from plane.app.serializers import FileAssetSerializer
class FileAssetEndpoint(BaseAPIView): class FileAssetEndpoint(BaseAPIView):
parser_classes = ( parser_classes = (MultiPartParser, FormParser, JSONParser,)
MultiPartParser,
FormParser,
JSONParser,
)
""" """
A viewset for viewing and editing task instances. A viewset for viewing and editing task instances.
@@ -24,18 +20,10 @@ class FileAssetEndpoint(BaseAPIView):
asset_key = str(workspace_id) + "/" + asset_key asset_key = str(workspace_id) + "/" + asset_key
files = FileAsset.objects.filter(asset=asset_key) files = FileAsset.objects.filter(asset=asset_key)
if files.exists(): if files.exists():
serializer = FileAssetSerializer( serializer = FileAssetSerializer(files, context={"request": request}, many=True)
files, context={"request": request}, many=True return Response({"data": serializer.data, "status": True}, status=status.HTTP_200_OK)
)
return Response(
{"data": serializer.data, "status": True},
status=status.HTTP_200_OK,
)
else: else:
return Response( return Response({"error": "Asset key does not exist", "status": False}, status=status.HTTP_200_OK)
{"error": "Asset key does not exist", "status": False},
status=status.HTTP_200_OK,
)
def post(self, request, slug): def post(self, request, slug):
serializer = FileAssetSerializer(data=request.data) serializer = FileAssetSerializer(data=request.data)
@@ -55,6 +43,7 @@ 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)
@@ -67,22 +56,12 @@ class UserAssetsEndpoint(BaseAPIView):
parser_classes = (MultiPartParser, FormParser) parser_classes = (MultiPartParser, FormParser)
def get(self, request, asset_key): def get(self, request, asset_key):
files = FileAsset.objects.filter( files = FileAsset.objects.filter(asset=asset_key, created_by=request.user)
asset=asset_key, created_by=request.user
)
if files.exists(): if files.exists():
serializer = FileAssetSerializer( serializer = FileAssetSerializer(files, context={"request": request})
files, context={"request": request} return Response({"data": serializer.data, "status": True}, status=status.HTTP_200_OK)
)
return Response(
{"data": serializer.data, "status": True},
status=status.HTTP_200_OK,
)
else: else:
return Response( return Response({"error": "Asset key does not exist", "status": False}, status=status.HTTP_200_OK)
{"error": "Asset key does not exist", "status": False},
status=status.HTTP_200_OK,
)
def post(self, request): def post(self, request):
serializer = FileAssetSerializer(data=request.data) serializer = FileAssetSerializer(data=request.data)
@@ -91,10 +70,9 @@ class UserAssetsEndpoint(BaseAPIView):
return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, asset_key): def delete(self, request, asset_key):
file_asset = FileAsset.objects.get( file_asset = FileAsset.objects.get(asset=asset_key, created_by=request.user)
asset=asset_key, created_by=request.user
)
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)
+10 -26
View File
@@ -128,8 +128,7 @@ class ForgotPasswordEndpoint(BaseAPIView):
status=status.HTTP_200_OK, status=status.HTTP_200_OK,
) )
return Response( return Response(
{"error": "Please check the email"}, {"error": "Please check the email"}, status=status.HTTP_400_BAD_REQUEST
status=status.HTTP_400_BAD_REQUEST,
) )
@@ -168,9 +167,7 @@ class ResetPasswordEndpoint(BaseAPIView):
} }
return Response(data, status=status.HTTP_200_OK) return Response(data, status=status.HTTP_200_OK)
return Response( return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
except DjangoUnicodeDecodeError as indentifier: except DjangoUnicodeDecodeError as indentifier:
return Response( return Response(
@@ -194,8 +191,7 @@ 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"}, {"message": "Password updated successfully"}, status=status.HTTP_200_OK
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)
@@ -217,8 +213,7 @@ 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"}, {"error": "Password is not valid"}, status=status.HTTP_400_BAD_REQUEST
status=status.HTTP_400_BAD_REQUEST,
) )
# Set the user password # Set the user password
@@ -286,9 +281,7 @@ 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,
) )
@@ -346,8 +339,7 @@ class EmailCheckEndpoint(BaseAPIView):
if not email: if not email:
return Response( return Response(
{"error": "Email is required"}, {"error": "Email is required"}, status=status.HTTP_400_BAD_REQUEST
status=status.HTTP_400_BAD_REQUEST,
) )
# validate the email # validate the email
@@ -355,8 +347,7 @@ class EmailCheckEndpoint(BaseAPIView):
validate_email(email) validate_email(email)
except ValidationError: except ValidationError:
return Response( return Response(
{"error": "Email is not valid"}, {"error": "Email is not valid"}, status=status.HTTP_400_BAD_REQUEST
status=status.HTTP_400_BAD_REQUEST,
) )
# Check if the user exists # Check if the user exists
@@ -408,18 +399,13 @@ 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,
) )
@@ -447,9 +433,7 @@ 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,
) )
+6 -10
View File
@@ -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", "").strip().lower() key = request.data.get("key", False).strip().lower()
if not key or user_token == "": if not key or user_token == "":
return Response( return Response(
@@ -364,10 +364,8 @@ class MagicSignInEndpoint(BaseAPIView):
user.save() user.save()
# Check if user has any accepted invites for workspace and add them to workspace # Check if user has any accepted invites for workspace and add them to workspace
workspace_member_invites = ( workspace_member_invites = WorkspaceMemberInvite.objects.filter(
WorkspaceMemberInvite.objects.filter( email=user.email, accepted=True
email=user.email, accepted=True
)
) )
WorkspaceMember.objects.bulk_create( WorkspaceMember.objects.bulk_create(
@@ -433,9 +431,7 @@ class MagicSignInEndpoint(BaseAPIView):
else: else:
return Response( return Response(
{ {"error": "Your login code was incorrect. Please try again."},
"error": "Your login code was incorrect. Please try again."
},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
+13 -58
View File
@@ -46,9 +46,7 @@ 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( response = super().finalize_response(request, response, *args, **kwargs)
request, response, *args, **kwargs
)
# Check for the case should webhook be sent # Check for the case should webhook be sent
if ( if (
@@ -90,9 +88,7 @@ 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( raise APIException("Please check the view", status.HTTP_400_BAD_REQUEST)
"Please check the view", status.HTTP_400_BAD_REQUEST
)
def handle_exception(self, exc): def handle_exception(self, exc):
""" """
@@ -103,7 +99,6 @@ 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"},
@@ -117,23 +112,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"The required object does not exist."}, {"error": f"{model_name} does not exist."},
status=status.HTTP_404_NOT_FOUND, status=status.HTTP_404_NOT_FOUND,
) )
if isinstance(e, KeyError): if isinstance(e, KeyError):
capture_exception(e) capture_exception(e)
return Response( return Response(
{"error": f"The required key does not exist."}, {"error": f"key {e} does not exist"},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
print(e) if settings.DEBUG else print("Server Error")
capture_exception(e) capture_exception(e)
return Response( return Response({"error": "Something went wrong please try again later"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
{"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:
@@ -164,24 +159,6 @@ 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 = [
@@ -224,24 +201,20 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
) )
if isinstance(e, ObjectDoesNotExist): if isinstance(e, ObjectDoesNotExist):
model_name = str(exc).split(" matching query does not exist.")[0]
return Response( return Response(
{"error": f"The required object does not exist."}, {"error": f"{model_name} does not exist."},
status=status.HTTP_404_NOT_FOUND, status=status.HTTP_404_NOT_FOUND,
) )
if isinstance(e, KeyError): if isinstance(e, KeyError):
return Response( return Response({"error": f"key {e} does not exist"}, status=status.HTTP_400_BAD_REQUEST)
{"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( return Response({"error": "Something went wrong please try again later"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
{"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:
@@ -266,21 +239,3 @@ 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
+6 -133
View File
@@ -20,6 +20,7 @@ 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,
@@ -89,16 +90,8 @@ class ConfigurationEndpoint(BaseAPIView):
data = {} data = {}
# Authentication # Authentication
data["google_client_id"] = ( data["google_client_id"] = GOOGLE_CLIENT_ID if GOOGLE_CLIENT_ID and GOOGLE_CLIENT_ID != "\"\"" else None
GOOGLE_CLIENT_ID data["github_client_id"] = GITHUB_CLIENT_ID if GITHUB_CLIENT_ID and GITHUB_CLIENT_ID != "\"\"" else None
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)
@@ -119,129 +112,9 @@ 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( data["file_size_limit"] = float(os.environ.get("FILE_SIZE_LIMIT", 5242880))
os.environ.get("FILE_SIZE_LIMIT", 5242880)
)
# is smtp configured # is self managed
data["is_smtp_configured"] = bool(EMAIL_HOST_USER) and bool( data["is_self_managed"] = bool(int(os.environ.get("IS_SELF_MANAGED", "1")))
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)
+92 -196
View File
@@ -11,10 +11,6 @@ from django.db.models import (
Count, Count,
Prefetch, Prefetch,
Sum, Sum,
Case,
When,
Value,
CharField,
) )
from django.core import serializers from django.core import serializers
from django.utils import timezone from django.utils import timezone
@@ -31,15 +27,10 @@ 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,
@@ -49,10 +40,9 @@ 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
@@ -67,8 +57,7 @@ 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"), project_id=self.kwargs.get("project_id"), owned_by=self.request.user
owned_by=self.request.user,
) )
def get_queryset(self): def get_queryset(self):
@@ -147,9 +136,7 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
), ),
) )
) )
.annotate( .annotate(total_estimates=Sum("issue_cycle__issue__estimate_point"))
total_estimates=Sum("issue_cycle__issue__estimate_point")
)
.annotate( .annotate(
completed_estimates=Sum( completed_estimates=Sum(
"issue_cycle__issue__estimate_point", "issue_cycle__issue__estimate_point",
@@ -170,39 +157,16 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
), ),
) )
) )
.annotate(
status=Case(
When(
Q(start_date__lte=timezone.now())
& Q(end_date__gte=timezone.now()),
then=Value("CURRENT"),
),
When(
start_date__gt=timezone.now(), then=Value("UPCOMING")
),
When(end_date__lt=timezone.now(), then=Value("COMPLETED")),
When(
Q(start_date__isnull=True) & Q(end_date__isnull=True),
then=Value("DRAFT"),
),
default=Value("DRAFT"),
output_field=CharField(),
)
)
.prefetch_related( .prefetch_related(
Prefetch( Prefetch(
"issue_cycle__issue__assignees", "issue_cycle__issue__assignees",
queryset=User.objects.only( queryset=User.objects.only("avatar", "first_name", "id").distinct(),
"avatar", "first_name", "id"
).distinct(),
) )
) )
.prefetch_related( .prefetch_related(
Prefetch( Prefetch(
"issue_cycle__issue__labels", "issue_cycle__issue__labels",
queryset=Label.objects.only( queryset=Label.objects.only("name", "color", "id").distinct(),
"name", "color", "id"
).distinct(),
) )
) )
.order_by("-is_favorite", "name") .order_by("-is_favorite", "name")
@@ -212,13 +176,8 @@ 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")
# Current Cycle # Current Cycle
if cycle_view == "current": if cycle_view == "current":
@@ -242,13 +201,13 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
.values("display_name", "assignee_id", "avatar") .values("display_name", "assignee_id", "avatar")
.annotate( .annotate(
total_issues=Count( total_issues=Count(
"id", "assignee_id",
filter=Q(archived_at__isnull=True, is_draft=False), filter=Q(archived_at__isnull=True, is_draft=False),
), ),
) )
.annotate( .annotate(
completed_issues=Count( completed_issues=Count(
"id", "assignee_id",
filter=Q( filter=Q(
completed_at__isnull=False, completed_at__isnull=False,
archived_at__isnull=True, archived_at__isnull=True,
@@ -258,7 +217,7 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
) )
.annotate( .annotate(
pending_issues=Count( pending_issues=Count(
"id", "assignee_id",
filter=Q( filter=Q(
completed_at__isnull=True, completed_at__isnull=True,
archived_at__isnull=True, archived_at__isnull=True,
@@ -281,13 +240,13 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
.values("label_name", "color", "label_id") .values("label_name", "color", "label_id")
.annotate( .annotate(
total_issues=Count( total_issues=Count(
"id", "label_id",
filter=Q(archived_at__isnull=True, is_draft=False), filter=Q(archived_at__isnull=True, is_draft=False),
) )
) )
.annotate( .annotate(
completed_issues=Count( completed_issues=Count(
"id", "label_id",
filter=Q( filter=Q(
completed_at__isnull=False, completed_at__isnull=False,
archived_at__isnull=True, archived_at__isnull=True,
@@ -297,7 +256,7 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
) )
.annotate( .annotate(
pending_issues=Count( pending_issues=Count(
"id", "label_id",
filter=Q( filter=Q(
completed_at__isnull=True, completed_at__isnull=True,
archived_at__isnull=True, archived_at__isnull=True,
@@ -313,9 +272,7 @@ 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"][ data[0]["distribution"]["completion_chart"] = burndown_plot(
"completion_chart"
] = burndown_plot(
queryset=queryset.first(), queryset=queryset.first(),
slug=slug, slug=slug,
project_id=project_id, project_id=project_id,
@@ -324,8 +281,44 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
return Response(data, status=status.HTTP_200_OK) return Response(data, status=status.HTTP_200_OK)
cycles = CycleSerializer(queryset, many=True).data # Upcoming Cycles
return Response(cycles, status=status.HTTP_200_OK) if cycle_view == "upcoming":
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 (
@@ -341,18 +334,8 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
project_id=project_id, project_id=project_id,
owned_by=request.user, owned_by=request.user,
) )
cycle = ( return Response(serializer.data, status=status.HTTP_201_CREATED)
self.get_queryset() return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
.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(
{ {
@@ -362,22 +345,15 @@ 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( cycle = Cycle.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
workspace__slug=slug, project_id=project_id, pk=pk
)
request_data = request.data request_data = request.data
if ( if cycle.end_date is not None and cycle.end_date < timezone.now().date():
cycle.end_date is not None
and cycle.end_date < timezone.now().date()
):
if "sort_order" in request_data: if "sort_order" in request_data:
# Can only change sort order # Can only change sort order
request_data = { request_data = {
"sort_order": request_data.get( "sort_order": request_data.get("sort_order", cycle.sort_order)
"sort_order", cycle.sort_order
)
} }
else: else:
return Response( return Response(
@@ -387,9 +363,7 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
serializer = CycleWriteSerializer( serializer = CycleWriteSerializer(cycle, data=request.data, partial=True)
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,22 +384,16 @@ 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( .values("first_name", "last_name", "assignee_id", "avatar", "display_name")
"first_name",
"last_name",
"assignee_id",
"avatar",
"display_name",
)
.annotate( .annotate(
total_issues=Count( total_issues=Count(
"id", "assignee_id",
filter=Q(archived_at__isnull=True, is_draft=False), filter=Q(archived_at__isnull=True, is_draft=False),
), ),
) )
.annotate( .annotate(
completed_issues=Count( completed_issues=Count(
"id", "assignee_id",
filter=Q( filter=Q(
completed_at__isnull=False, completed_at__isnull=False,
archived_at__isnull=True, archived_at__isnull=True,
@@ -435,7 +403,7 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
) )
.annotate( .annotate(
pending_issues=Count( pending_issues=Count(
"id", "assignee_id",
filter=Q( filter=Q(
completed_at__isnull=True, completed_at__isnull=True,
archived_at__isnull=True, archived_at__isnull=True,
@@ -459,13 +427,13 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
.values("label_name", "color", "label_id") .values("label_name", "color", "label_id")
.annotate( .annotate(
total_issues=Count( total_issues=Count(
"id", "label_id",
filter=Q(archived_at__isnull=True, is_draft=False), filter=Q(archived_at__isnull=True, is_draft=False),
), ),
) )
.annotate( .annotate(
completed_issues=Count( completed_issues=Count(
"id", "label_id",
filter=Q( filter=Q(
completed_at__isnull=False, completed_at__isnull=False,
archived_at__isnull=True, archived_at__isnull=True,
@@ -475,7 +443,7 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
) )
.annotate( .annotate(
pending_issues=Count( pending_issues=Count(
"id", "label_id",
filter=Q( filter=Q(
completed_at__isnull=True, completed_at__isnull=True,
archived_at__isnull=True, archived_at__isnull=True,
@@ -495,10 +463,7 @@ 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, queryset=queryset, slug=slug, project_id=project_id, cycle_id=pk
slug=slug,
project_id=project_id,
cycle_id=pk,
) )
return Response( return Response(
@@ -508,13 +473,11 @@ 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( CycleIssue.objects.filter(cycle_id=self.kwargs.get("pk")).values_list(
cycle_id=self.kwargs.get("pk") "issue", flat=True
).values_list("issue", flat=True) )
)
cycle = Cycle.objects.get(
workspace__slug=slug, project_id=project_id, pk=pk
) )
cycle = Cycle.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
issue_activity.delay( issue_activity.delay(
type="cycle.activity.deleted", type="cycle.activity.deleted",
@@ -530,8 +493,6 @@ 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()
@@ -559,9 +520,7 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
super() super()
.get_queryset() .get_queryset()
.annotate( .annotate(
sub_issues_count=Issue.issue_objects.filter( sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("issue_id"))
parent=OuterRef("issue_id")
)
.order_by() .order_by()
.annotate(count=Func(F("id"), function="Count")) .annotate(count=Func(F("id"), function="Count"))
.values("count") .values("count")
@@ -580,30 +539,28 @@ 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 = [ fields = [field for field in request.GET.get("fields", "").split(",") if field]
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( sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
parent=OuterRef("id")
)
.order_by() .order_by()
.annotate(count=Func(F("id"), function="Count")) .annotate(count=Func(F("id"), function="Count"))
.values("count") .values("count")
) )
.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("workspace", "project", "state", "parent") .select_related("project")
.prefetch_related("assignees", "labels", "issue_module__module") .select_related("workspace")
.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()
@@ -611,43 +568,30 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
.values("count") .values("count")
) )
.annotate( .annotate(
attachment_count=IssueAttachment.objects.filter( attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id"))
issue=OuterRef("id")
)
.order_by() .order_by()
.annotate(count=Func(F("id"), function="Count")) .annotate(count=Func(F("id"), function="Count"))
.values("count") .values("count")
) )
.annotate(
is_subscribed=Exists(
IssueSubscriber.objects.filter(
subscriber=self.request.user, issue_id=OuterRef("id")
)
)
)
) )
serializer = IssueSerializer(
issues, many=True, fields=fields if fields else None issues = IssueStateSerializer(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"}, {"error": "Issues are required"}, status=status.HTTP_400_BAD_REQUEST
status=status.HTTP_400_BAD_REQUEST,
) )
cycle = Cycle.objects.get( cycle = Cycle.objects.get(
workspace__slug=slug, project_id=project_id, pk=cycle_id workspace__slug=slug, project_id=project_id, pk=cycle_id
) )
if ( if cycle.end_date is not None and cycle.end_date < timezone.now().date():
cycle.end_date is not None
and cycle.end_date < timezone.now().date()
):
return Response( return Response(
{ {
"error": "The Cycle has already been completed so no new issues can be added" "error": "The Cycle has already been completed so no new issues can be added"
@@ -718,27 +662,19 @@ 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(
IssueSerializer( CycleIssueSerializer(self.get_queryset(), many=True).data,
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, issue_id): def destroy(self, request, slug, project_id, cycle_id, pk):
cycle_issue = CycleIssue.objects.get( cycle_issue = CycleIssue.objects.get(
issue_id=issue_id, pk=pk, workspace__slug=slug, project_id=project_id, cycle_id=cycle_id
workspace__slug=slug,
project_id=project_id,
cycle_id=cycle_id,
) )
issue_id = cycle_issue.issue_id
issue_activity.delay( issue_activity.delay(
type="cycle.activity.deleted", type="cycle.activity.deleted",
requested_data=json.dumps( requested_data=json.dumps(
@@ -748,12 +684,10 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
} }
), ),
actor_id=str(self.request.user.id), actor_id=str(self.request.user.id),
issue_id=str(issue_id), issue_id=str(cycle_issue.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)
@@ -872,41 +806,3 @@ 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)
-690
View File
@@ -1,690 +0,0 @@
# 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 == "pending":
pending_issues_count = assigned_issues.filter(
state__group__in=["backlog", "started", "unstarted"]
).count()
pending_issues = assigned_issues.filter(
state__group__in=["backlog", "started", "unstarted"]
)[:5]
return Response(
{
"issues": IssueSerializer(
pending_issues, many=True, expand=self.expand
).data,
"count": pending_issues_count,
},
status=status.HTTP_200_OK,
)
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 == "pending":
pending_issues_count = created_issues.filter(
state__group__in=["backlog", "started", "unstarted"]
).count()
pending_issues = created_issues.filter(
state__group__in=["backlog", "started", "unstarted"]
)[:5]
return Response(
{
"issues": IssueSerializer(
pending_issues, many=True, expand=self.expand
).data,
"count": pending_issues_count,
},
status=status.HTTP_200_OK,
)
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
)
+20 -33
View File
@@ -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,13 +39,9 @@ class BulkEstimatePointEndpoint(BaseViewSet):
serializer_class = EstimateSerializer serializer_class = EstimateSerializer
def list(self, request, slug, project_id): def list(self, request, slug, project_id):
estimates = ( estimates = Estimate.objects.filter(
Estimate.objects.filter( workspace__slug=slug, project_id=project_id
workspace__slug=slug, project_id=project_id ).prefetch_related("points").select_related("workspace", "project")
)
.prefetch_related("points")
.select_related("workspace", "project")
)
serializer = EstimateReadSerializer(estimates, many=True) serializer = EstimateReadSerializer(estimates, many=True)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
@@ -58,17 +54,13 @@ class BulkEstimatePointEndpoint(BaseViewSet):
estimate_points = request.data.get("estimate_points", []) estimate_points = request.data.get("estimate_points", [])
serializer = EstimatePointSerializer( if not len(estimate_points) or len(estimate_points) > 8:
data=request.data.get("estimate_points"), many=True
)
if not serializer.is_valid():
return Response( return Response(
serializer.errors, status=status.HTTP_400_BAD_REQUEST {"error": "Estimate points are required"},
status=status.HTTP_400_BAD_REQUEST,
) )
estimate_serializer = EstimateSerializer( estimate_serializer = EstimateSerializer(data=request.data.get("estimate"))
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
@@ -143,8 +135,7 @@ class BulkEstimatePointEndpoint(BaseViewSet):
estimate_points = EstimatePoint.objects.filter( estimate_points = EstimatePoint.objects.filter(
pk__in=[ pk__in=[
estimate_point.get("id") estimate_point.get("id") for estimate_point in estimate_points_data
for estimate_point in estimate_points_data
], ],
workspace__slug=slug, workspace__slug=slug,
project_id=project_id, project_id=project_id,
@@ -166,14 +157,10 @@ 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, updated_estimate_points, ["value"], batch_size=10,
["value"],
batch_size=10,
) )
estimate_point_serializer = EstimatePointSerializer( estimate_point_serializer = EstimatePointSerializer(estimate_points, many=True)
estimate_points, many=True
)
return Response( return Response(
{ {
"estimate": estimate_serializer.data, "estimate": estimate_serializer.data,
+2 -4
View File
@@ -63,11 +63,9 @@ 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( if request.GET.get("per_page", False) and request.GET.get("cursor", False):
"cursor", False
):
return self.paginate( return self.paginate(
request=request, request=request,
queryset=exporter_history, queryset=exporter_history,
+3 -7
View File
@@ -14,10 +14,7 @@ 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 ( from plane.app.serializers import ProjectLiteSerializer, WorkspaceLiteSerializer
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
@@ -54,8 +51,7 @@ class GPTIntegrationEndpoint(BaseAPIView):
if not task: if not task:
return Response( return Response(
{"error": "Task is required"}, {"error": "Task is required"}, status=status.HTTP_400_BAD_REQUEST
status=status.HTTP_400_BAD_REQUEST,
) )
final_text = task + "\n" + prompt final_text = task + "\n" + prompt
@@ -93,7 +89,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",
+156 -154
View File
@@ -1,5 +1,7 @@
# Python imports # Python imports
import uuid import uuid
import json
import requests
# Third party imports # Third party imports
from rest_framework import status from rest_framework import status
@@ -7,7 +9,7 @@ from rest_framework.response import Response
# Django imports # Django imports
from django.db.models import Max, Q from django.db.models import Max, Q
from django.conf import settings
# Module imports # Module imports
from plane.app.views import BaseAPIView from plane.app.views import BaseAPIView
from plane.db.models import ( from plane.db.models import (
@@ -34,22 +36,15 @@ from plane.app.serializers import (
IssueFlatSerializer, IssueFlatSerializer,
ModuleSerializer, ModuleSerializer,
) )
from plane.utils.integrations.github import get_github_repo_details
from plane.utils.importers.jira import (
jira_project_issue_summary,
is_allowed_hostname,
)
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
from plane.bgtasks.importer_task import service_importer
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)
repo = request.GET.get("repo", False) repo = request.GET.get("repo", False)
if not owner or not repo: if not owner or not repo:
return Response( return Response(
{"error": "Owner and repo are required"}, {"error": "Owner and repo are required"},
@@ -60,11 +55,10 @@ class ServiceIssueImportSummaryEndpoint(BaseAPIView):
integration__provider="github", workspace__slug=slug integration__provider="github", workspace__slug=slug
) )
access_tokens_url = workspace_integration.metadata.get( installtion_id = workspace_integration.config.get("installation_id", False)
"access_tokens_url", False
)
if not access_tokens_url: # Check for the installation id
if not installtion_id:
return Response( return Response(
{ {
"error": "There was an error during the installation of the GitHub app. To resolve this issue, we recommend reinstalling the GitHub app." "error": "There was an error during the installation of the GitHub app. To resolve this issue, we recommend reinstalling the GitHub app."
@@ -72,18 +66,33 @@ class ServiceIssueImportSummaryEndpoint(BaseAPIView):
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
issue_count, labels, collaborators = get_github_repo_details( # Request segway for the required information
access_tokens_url, owner, repo if settings.SEGWAY_BASE_URL:
) headers = {
"Content-Type": "application/json",
"x-api-key": settings.SEGWAY_KEY,
}
data = {
"owner": owner,
"repo": repo,
"installationId": installtion_id,
}
res = requests.post(
f"{settings.SEGWAY_BASE_URL}/api/github",
data=json.dumps(data),
headers=headers,
)
if "error" in res.json():
return Response(res.json(), status=status.HTTP_400_BAD_REQUEST)
else:
return Response(
res.json(),
status=status.HTTP_200_OK,
)
return Response( return Response(
{ {"error": "Inetgration service is not available please try later"},
"issue_count": issue_count, status=status.HTTP_400_BAD_REQUEST,
"labels": labels,
"collaborators": collaborators,
},
status=status.HTTP_200_OK,
) )
if service == "jira": if service == "jira":
# Check for all the keys # Check for all the keys
params = { params = {
@@ -96,8 +105,7 @@ 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}, {"error": error_message}, status=status.HTTP_400_BAD_REQUEST
status=status.HTTP_400_BAD_REQUEST,
) )
project_key = request.GET.get("project_key", "") project_key = request.GET.get("project_key", "")
@@ -105,16 +113,35 @@ class ServiceIssueImportSummaryEndpoint(BaseAPIView):
email = request.GET.get("email", "") email = request.GET.get("email", "")
cloud_hostname = request.GET.get("cloud_hostname", "") cloud_hostname = request.GET.get("cloud_hostname", "")
response = jira_project_issue_summary( if settings.SEGWAY_BASE_URL:
email, api_token, project_key, cloud_hostname headers = {
) "Content-Type": "application/json",
if "error" in response: "x-api-key": settings.SEGWAY_KEY,
return Response(response, status=status.HTTP_400_BAD_REQUEST) }
else: data = {
return Response( "project_key": project_key,
response, "api_token": api_token,
status=status.HTTP_200_OK, "email": email,
"cloud_hostname": cloud_hostname,
}
res = requests.post(
f"{settings.SEGWAY_BASE_URL}/api/jira",
data=json.dumps(data),
headers=headers,
) )
if "error" in res.json():
return Response(res.json(), status=status.HTTP_400_BAD_REQUEST)
else:
return Response(
res.json(),
status=status.HTTP_200_OK,
)
return Response(
{"error": "Inetgration service is not available please try later"},
status=status.HTTP_400_BAD_REQUEST,
)
return Response( return Response(
{"error": "Service not supported yet"}, {"error": "Service not supported yet"},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
@@ -127,6 +154,19 @@ class ImportServiceEndpoint(BaseAPIView):
] ]
def post(self, request, slug, service): def post(self, request, slug, service):
if service not in ["github", "jira"]:
return Response(
{"error": "Servivce not supported yet"},
status=status.HTTP_400_BAD_REQUEST,
)
if service == "github":
workspace_integration = WorkspaceIntegration.objects.get(
integration__provider="github", workspace__slug=slug
)
installation_id = workspace_integration.config.get("installation_id", False)
project_id = request.data.get("project_id", False) project_id = request.data.get("project_id", False)
if not project_id: if not project_id:
@@ -134,102 +174,84 @@ class ImportServiceEndpoint(BaseAPIView):
{"error": "Project ID is required"}, {"error": "Project ID is required"},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
workspace = Workspace.objects.get(slug=slug) workspace = Workspace.objects.get(slug=slug)
if service == "github": # Validate the data
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)
if not data or not metadata or not config: if not data or not metadata or not config:
return Response( return Response(
{"error": "Data, config and metadata are required"}, {"error": "Data, config and metadata are required"},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
)
api_token = APIToken.objects.filter(
user=request.user, workspace=workspace
).first()
if api_token is None:
api_token = APIToken.objects.create(
user=request.user,
label="Importer",
workspace=workspace,
)
importer = Importer.objects.create(
service=service,
project_id=project_id,
status="queued",
initiated_by=request.user,
data=data,
metadata=metadata,
token=api_token,
config=config,
created_by=request.user,
updated_by=request.user,
) )
service_importer.delay(service, importer.id) # Update config
serializer = ImporterSerializer(importer) if config and service == "github":
return Response(serializer.data, status=status.HTTP_201_CREATED) config.update({"installation_id": installation_id})
if service == "jira": # Get the api token -- # derecated
data = request.data.get("data", False) api_token = APIToken.objects.filter(
metadata = request.data.get("metadata", False) user=request.user, workspace=workspace
config = request.data.get("config", False) ).first()
if api_token is None:
cloud_hostname = metadata.get("cloud_hostname", False) api_token = APIToken.objects.create(
user=request.user,
if not cloud_hostname: label="Importer",
return Response( workspace=workspace,
{"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:
return Response(
{"error": "Data, config and metadata are required"},
status=status.HTTP_400_BAD_REQUEST,
)
api_token = APIToken.objects.filter(
user=request.user, workspace=workspace
).first()
if api_token is None:
api_token = APIToken.objects.create(
user=request.user,
label="Importer",
workspace=workspace,
)
importer = Importer.objects.create(
service=service,
project_id=project_id,
status="queued",
initiated_by=request.user,
data=data,
metadata=metadata,
token=api_token,
config=config,
created_by=request.user,
updated_by=request.user,
) )
service_importer.delay(service, importer.id) # Create an import
serializer = ImporterSerializer(importer) importer = Importer.objects.create(
return Response(serializer.data, status=status.HTTP_201_CREATED) service=service,
project_id=project_id,
return Response( status="queued",
{"error": "Servivce not supported yet"}, initiated_by=request.user,
status=status.HTTP_400_BAD_REQUEST, data=data,
metadata=metadata,
token=api_token,
config=config,
created_by=request.user,
updated_by=request.user,
) )
# Push it to segway
if settings.SEGWAY_BASE_URL:
headers = {
"Content-Type": "application/json",
"x-api-key": settings.SEGWAY_KEY,
}
data = {
"metadata": metadata,
"data": data,
"config": config,
"workspace_id": str(workspace.id),
"project_id": str(project_id),
"created_by": str(request.user.id),
"importer_id": str(importer.id),
}
res = requests.post(
f"{settings.SEGWAY_BASE_URL}/api/github/import",
data=json.dumps(data),
headers=headers,
)
if "error" in res.json():
importer.status = "failed"
importer.reason = str(res.json())
importer.save()
else:
importer.status = "processing"
importer.save(update_fields=["status"])
else:
importer.status = "failed"
importer.reason = "Segway base url is not present"
importer.save(update_fields=["status", "reason"])
# return the response
serializer = ImporterSerializer(importer)
return Response(serializer.data, status=status.HTTP_201_CREATED)
def get(self, request, slug): def get(self, request, slug):
imports = ( imports = (
Importer.objects.filter(workspace__slug=slug) Importer.objects.filter(workspace__slug=slug)
@@ -240,9 +262,7 @@ class ImportServiceEndpoint(BaseAPIView):
return Response(serializer.data) return Response(serializer.data)
def delete(self, request, slug, service, pk): def delete(self, request, slug, service, pk):
importer = Importer.objects.get( importer = Importer.objects.get(pk=pk, service=service, workspace__slug=slug)
pk=pk, service=service, workspace__slug=slug
)
if importer.imported_data is not None: if importer.imported_data is not None:
# Delete all imported Issues # Delete all imported Issues
@@ -260,12 +280,8 @@ class ImportServiceEndpoint(BaseAPIView):
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
def patch(self, request, slug, service, pk): def patch(self, request, slug, service, pk):
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)
@@ -301,9 +317,9 @@ class BulkImportIssuesEndpoint(BaseAPIView):
).first() ).first()
# Get the maximum sequence_id # Get the maximum sequence_id
last_id = IssueSequence.objects.filter( last_id = IssueSequence.objects.filter(project_id=project_id).aggregate(
project_id=project_id largest=Max("sequence")
).aggregate(largest=Max("sequence"))["largest"] )["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
@@ -336,9 +352,7 @@ 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=issue_data.get("description_html", "<p></p>"),
"description_html", "<p></p>"
),
description_stripped=( description_stripped=(
None None
if ( if (
@@ -450,21 +464,15 @@ class BulkImportIssuesEndpoint(BaseAPIView):
for comment in comments_list for comment in comments_list
] ]
_ = IssueComment.objects.bulk_create( _ = IssueComment.objects.bulk_create(bulk_issue_comments, batch_size=100)
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=issue_data.get("link", {}).get("url", "https://github.com"),
"url", "https://github.com" title=issue_data.get("link", {}).get("title", "Original Issue"),
),
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,
@@ -501,18 +509,14 @@ class BulkImportModulesEndpoint(BaseAPIView):
ignore_conflicts=True, ignore_conflicts=True,
) )
modules = Module.objects.filter( modules = Module.objects.filter(id__in=[module.id for module in modules])
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(
[ [
ModuleLink( ModuleLink(
module=module, module=module,
url=module_data.get("link", {}).get( url=module_data.get("link", {}).get("url", "https://plane.so"),
"url", "https://plane.so"
),
title=module_data.get("link", {}).get( title=module_data.get("link", {}).get(
"title", "Original Issue" "title", "Original Issue"
), ),
@@ -551,8 +555,6 @@ 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,
) )
+71 -83
View File
@@ -62,9 +62,7 @@ class InboxViewSet(BaseViewSet):
serializer.save(project_id=self.kwargs.get("project_id")) serializer.save(project_id=self.kwargs.get("project_id"))
def destroy(self, request, slug, project_id, pk): def destroy(self, request, slug, project_id, pk):
inbox = Inbox.objects.get( inbox = Inbox.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
workspace__slug=slug, project_id=project_id, pk=pk
)
# Handle default inbox delete # Handle default inbox delete
if inbox.is_default: if inbox.is_default:
return Response( return Response(
@@ -88,14 +86,49 @@ class InboxIssueViewSet(BaseViewSet):
] ]
def get_queryset(self): def get_queryset(self):
return ( return self.filter_queryset(
Issue.objects.filter( super()
project_id=self.kwargs.get("project_id"), .get_queryset()
.filter(
Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True),
workspace__slug=self.kwargs.get("slug"), workspace__slug=self.kwargs.get("slug"),
issue_inbox__inbox_id=self.kwargs.get("inbox_id") 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_inbox__inbox_id=inbox_id,
workspace__slug=slug,
project_id=project_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", "issue_module__module") .prefetch_related("assignees", "labels")
.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",
@@ -104,35 +137,8 @@ class InboxIssueViewSet(BaseViewSet):
), ),
) )
) )
.annotate(cycle_id=F("issue_cycle__cycle_id")) )
.annotate( issues_data = IssueStateInboxSerializer(issues, many=True).data
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,
@@ -141,8 +147,7 @@ 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"}, {"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST
status=status.HTTP_400_BAD_REQUEST,
) )
# Check for valid priority # Check for valid priority
@@ -154,8 +159,7 @@ class InboxIssueViewSet(BaseViewSet):
"none", "none",
]: ]:
return Response( return Response(
{"error": "Invalid priority"}, {"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST
status=status.HTTP_400_BAD_REQUEST,
) )
# Create or get state # Create or get state
@@ -188,8 +192,6 @@ 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(
@@ -199,16 +201,12 @@ class InboxIssueViewSet(BaseViewSet):
source=request.data.get("source", "in-app"), source=request.data.get("source", "in-app"),
) )
issue = (self.get_queryset().filter(pk=issue.id).first()) serializer = IssueStateInboxSerializer(issue)
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, issue_id): def partial_update(self, request, slug, project_id, inbox_id, pk):
inbox_issue = InboxIssue.objects.get( inbox_issue = InboxIssue.objects.get(
issue_id=issue_id, pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
workspace__slug=slug,
project_id=project_id,
inbox_id=inbox_id,
) )
# Get the project member # Get the project member
project_member = ProjectMember.objects.get( project_member = ProjectMember.objects.get(
@@ -231,9 +229,7 @@ class InboxIssueViewSet(BaseViewSet):
if bool(issue_data): if bool(issue_data):
issue = Issue.objects.get( issue = Issue.objects.get(
pk=inbox_issue.issue_id, pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_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:
@@ -243,9 +239,7 @@ 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_data.get("description", issue.description),
"description", issue.description
),
} }
issue_serializer = IssueCreateSerializer( issue_serializer = IssueCreateSerializer(
@@ -268,8 +262,6 @@ 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:
@@ -293,9 +285,7 @@ class InboxIssueViewSet(BaseViewSet):
project_id=project_id, project_id=project_id,
) )
state = State.objects.filter( state = State.objects.filter(
group="cancelled", group="cancelled", workspace__slug=slug, project_id=project_id
workspace__slug=slug,
project_id=project_id,
).first() ).first()
if state is not None: if state is not None:
issue.state = state issue.state = state
@@ -313,35 +303,32 @@ 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, workspace__slug=slug, project_id=project_id, default=True
project_id=project_id,
default=True,
).first() ).first()
if state is not None: if state is not None:
issue.state = state issue.state = state
issue.save() issue.save()
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.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, issue_id): return Response(serializer.data, status=status.HTTP_200_OK)
issue = self.get_queryset().filter(pk=issue_id).first() return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
serializer = IssueSerializer(issue, expand=self.expand,) else:
return Response(
InboxIssueSerializer(inbox_issue).data, status=status.HTTP_200_OK
)
def retrieve(self, request, slug, project_id, inbox_id, pk):
inbox_issue = InboxIssue.objects.get(
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
)
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, issue_id): def destroy(self, request, slug, project_id, inbox_id, pk):
inbox_issue = InboxIssue.objects.get( inbox_issue = InboxIssue.objects.get(
issue_id=issue_id, pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
workspace__slug=slug,
project_id=project_id,
inbox_id=inbox_id,
) )
# Get the project member # Get the project member
project_member = ProjectMember.objects.get( project_member = ProjectMember.objects.get(
@@ -363,8 +350,9 @@ 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=issue_id workspace__slug=slug, project_id=project_id, pk=inbox_issue.issue_id
).delete() ).delete()
inbox_issue.delete() inbox_issue.delete()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
+23 -19
View File
@@ -1,9 +1,11 @@
# Python improts # Python improts
import uuid import uuid
import requests import requests
import json
# Django imports # Django imports
from django.contrib.auth.hashers import make_password from django.contrib.auth.hashers import make_password
from django.conf import settings
# Third party imports # Third party imports
from rest_framework.response import Response from rest_framework.response import Response
@@ -20,10 +22,7 @@ from plane.db.models import (
WorkspaceMember, WorkspaceMember,
APIToken, APIToken,
) )
from plane.app.serializers import ( from plane.app.serializers import IntegrationSerializer, WorkspaceIntegrationSerializer
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,
@@ -51,9 +50,7 @@ class IntegrationViewSet(BaseViewSet):
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
serializer = IntegrationSerializer( serializer = IntegrationSerializer(integration, data=request.data, partial=True)
integration, data=request.data, partial=True
)
if serializer.is_valid(): if serializer.is_valid():
serializer.save() serializer.save()
@@ -99,7 +96,21 @@ class WorkspaceIntegrationViewSet(BaseViewSet):
{"error": "Installation ID is required"}, {"error": "Installation ID is required"},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
metadata = get_github_metadata(installation_id) # Push it to segway
if settings.SEGWAY_BASE_URL:
headers = {
"Content-Type": "application/json",
"x-api-key": settings.SEGWAY_KEY,
}
data = {"installationId": installation_id}
res = requests.post(
f"{settings.SEGWAY_BASE_URL}/api/github/metadata",
data=json.dumps(data),
headers=headers,
)
if "error" in res.json():
return Response(res.json(), status=status.HTTP_400_BAD_REQUEST)
metadata = res.json()
config = {"installation_id": installation_id} config = {"installation_id": installation_id}
if provider == "slack": if provider == "slack":
@@ -107,8 +118,7 @@ class WorkspaceIntegrationViewSet(BaseViewSet):
if not code: if not code:
return Response( return Response(
{"error": "Code is required"}, {"error": "Code is required"}, status=status.HTTP_400_BAD_REQUEST
status=status.HTTP_400_BAD_REQUEST,
) )
slack_response = slack_oauth(code=code) slack_response = slack_oauth(code=code)
@@ -118,9 +128,7 @@ 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}
@@ -133,9 +141,7 @@ class WorkspaceIntegrationViewSet(BaseViewSet):
is_password_autoset=True, is_password_autoset=True,
is_bot=True, is_bot=True,
first_name=integration.title, first_name=integration.title,
avatar=integration.avatar_url avatar=integration.avatar_url if integration.avatar_url is not None else "",
if integration.avatar_url is not None
else "",
) )
# Create an API Token for the bot user # Create an API Token for the bot user
@@ -171,9 +177,7 @@ class WorkspaceIntegrationViewSet(BaseViewSet):
) )
if workspace_integration.integration.provider == "github": if workspace_integration.integration.provider == "github":
installation_id = workspace_integration.config.get( installation_id = workspace_integration.config.get("installation_id", False)
"installation_id", False
)
if installation_id: if installation_id:
delete_github_installation(installation_id=installation_id) delete_github_installation(installation_id=installation_id)
+31 -13
View File
@@ -1,8 +1,15 @@
# Python imports
import json
import requests
# Third party imports # Third party imports
from rest_framework import status from rest_framework import status
from rest_framework.response import Response from rest_framework.response import Response
from sentry_sdk import capture_exception from sentry_sdk import capture_exception
# Django imports
from django.conf import settings
# Module imports # Module imports
from plane.app.views import BaseViewSet, BaseAPIView from plane.app.views import BaseViewSet, BaseAPIView
from plane.db.models import ( from plane.db.models import (
@@ -21,10 +28,7 @@ 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 ( from plane.app.permissions import ProjectBasePermission, ProjectEntityPermission
ProjectBasePermission,
ProjectEntityPermission,
)
class GithubRepositoriesEndpoint(BaseAPIView): class GithubRepositoriesEndpoint(BaseAPIView):
@@ -38,19 +42,32 @@ class GithubRepositoriesEndpoint(BaseAPIView):
workspace__slug=slug, pk=workspace_integration_id workspace__slug=slug, pk=workspace_integration_id
) )
if workspace_integration.integration.provider != "github": installation_id = workspace_integration.config.get("installation_id")
if not installation_id:
return Response( return Response(
{"error": "Not a github integration"}, {"error": "Not a github integration"},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
# Push it to segway
access_tokens_url = workspace_integration.metadata["access_tokens_url"] if settings.SEGWAY_BASE_URL:
repositories_url = ( headers = {
workspace_integration.metadata["repositories_url"] "Content-Type": "application/json",
+ f"?per_page=100&page={page}" "x-api-key": settings.SEGWAY_KEY,
) }
repositories = get_github_repos(access_tokens_url, repositories_url) data = {
return Response(repositories, status=status.HTTP_200_OK) "installationId": installation_id,
"page": page,
}
res = requests.post(
f"{settings.SEGWAY_BASE_URL}/api/github/repos",
data=json.dumps(data),
headers=headers,
)
if "error" in res.json():
return Response(res.json(), status=status.HTTP_400_BAD_REQUEST)
else:
return Response(res.json(), status=status.HTTP_200_OK)
class GithubRepositorySyncViewSet(BaseViewSet): class GithubRepositorySyncViewSet(BaseViewSet):
@@ -188,6 +205,7 @@ class BulkCreateGithubIssueSyncEndpoint(BaseAPIView):
class GithubCommentSyncViewSet(BaseViewSet): class GithubCommentSyncViewSet(BaseViewSet):
permission_classes = [ permission_classes = [
ProjectEntityPermission, ProjectEntityPermission,
] ]
+6 -20
View File
@@ -8,16 +8,9 @@ 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 ( from plane.db.models import SlackProjectSync, WorkspaceIntegration, ProjectMember
SlackProjectSync,
WorkspaceIntegration,
ProjectMember,
)
from plane.app.serializers import SlackProjectSyncSerializer from plane.app.serializers import SlackProjectSyncSerializer
from plane.app.permissions import ( from plane.app.permissions import ProjectBasePermission, ProjectEntityPermission
ProjectBasePermission,
ProjectEntityPermission,
)
from plane.utils.integrations.slack import slack_oauth from plane.utils.integrations.slack import slack_oauth
@@ -45,8 +38,7 @@ class SlackProjectSyncViewSet(BaseViewSet):
if not code: if not code:
return Response( return Response(
{"error": "Code is required"}, {"error": "Code is required"}, status=status.HTTP_400_BAD_REQUEST
status=status.HTTP_400_BAD_REQUEST,
) )
slack_response = slack_oauth(code=code) slack_response = slack_oauth(code=code)
@@ -62,9 +54,7 @@ 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( webhook_url=slack_response.get("incoming_webhook", {}).get("url"),
"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"),
@@ -72,9 +62,7 @@ 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, member=workspace_integration.actor, role=20, project_id=project_id
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)
@@ -86,8 +74,6 @@ 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
+157 -222
View File
@@ -7,8 +7,6 @@ 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
@@ -22,13 +20,9 @@ from plane.app.serializers import (
ModuleIssueSerializer, ModuleIssueSerializer,
ModuleLinkSerializer, ModuleLinkSerializer,
ModuleFavoriteSerializer, ModuleFavoriteSerializer,
IssueSerializer, IssueStateSerializer,
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,
@@ -38,8 +32,6 @@ 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
@@ -62,6 +54,7 @@ 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"),
@@ -81,9 +74,7 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
.prefetch_related( .prefetch_related(
Prefetch( Prefetch(
"link_module", "link_module",
queryset=ModuleLink.objects.select_related( queryset=ModuleLink.objects.select_related("module", "created_by"),
"module", "created_by"
),
) )
) )
.annotate( .annotate(
@@ -145,7 +136,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):
@@ -162,18 +153,6 @@ 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)
@@ -188,16 +167,10 @@ 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( .values("first_name", "last_name", "assignee_id", "avatar", "display_name")
"first_name",
"last_name",
"assignee_id",
"avatar",
"display_name",
)
.annotate( .annotate(
total_issues=Count( total_issues=Count(
"id", "assignee_id",
filter=Q( filter=Q(
archived_at__isnull=True, archived_at__isnull=True,
is_draft=False, is_draft=False,
@@ -206,7 +179,7 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
) )
.annotate( .annotate(
completed_issues=Count( completed_issues=Count(
"id", "assignee_id",
filter=Q( filter=Q(
completed_at__isnull=False, completed_at__isnull=False,
archived_at__isnull=True, archived_at__isnull=True,
@@ -216,7 +189,7 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
) )
.annotate( .annotate(
pending_issues=Count( pending_issues=Count(
"id", "assignee_id",
filter=Q( filter=Q(
completed_at__isnull=True, completed_at__isnull=True,
archived_at__isnull=True, archived_at__isnull=True,
@@ -239,7 +212,7 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
.values("label_name", "color", "label_id") .values("label_name", "color", "label_id")
.annotate( .annotate(
total_issues=Count( total_issues=Count(
"id", "label_id",
filter=Q( filter=Q(
archived_at__isnull=True, archived_at__isnull=True,
is_draft=False, is_draft=False,
@@ -248,7 +221,7 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
) )
.annotate( .annotate(
completed_issues=Count( completed_issues=Count(
"id", "label_id",
filter=Q( filter=Q(
completed_at__isnull=False, completed_at__isnull=False,
archived_at__isnull=True, archived_at__isnull=True,
@@ -258,7 +231,7 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
) )
.annotate( .annotate(
pending_issues=Count( pending_issues=Count(
"id", "label_id",
filter=Q( filter=Q(
completed_at__isnull=True, completed_at__isnull=True,
archived_at__isnull=True, archived_at__isnull=True,
@@ -278,10 +251,7 @@ 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, queryset=queryset, slug=slug, project_id=project_id, module_id=pk
slug=slug,
project_id=project_id,
module_id=pk,
) )
return Response( return Response(
@@ -290,28 +260,25 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
) )
def destroy(self, request, slug, project_id, pk): def destroy(self, request, slug, project_id, pk):
module = Module.objects.get( module = Module.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
workspace__slug=slug, project_id=project_id, pk=pk
)
module_issues = list( module_issues = list(
ModuleIssue.objects.filter(module_id=pk).values_list( ModuleIssue.objects.filter(module_id=pk).values_list("issue", flat=True)
"issue", flat=True )
) issue_activity.delay(
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)
@@ -322,6 +289,7 @@ 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",
@@ -331,18 +299,53 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
ProjectEntityPermission, ProjectEntityPermission,
] ]
def get_queryset(self): def get_queryset(self):
return ( return self.filter_queryset(
Issue.objects.filter( super()
project_id=self.kwargs.get("project_id"), .get_queryset()
workspace__slug=self.kwargs.get("slug"), .annotate(
issue_module__module_id=self.kwargs.get("module_id") sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("issue"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
) )
.select_related("workspace", "project", "state", "parent") .filter(workspace__slug=self.kwargs.get("slug"))
.prefetch_related("labels", "assignees") .filter(project_id=self.kwargs.get("project_id"))
.prefetch_related('issue_module__module') .filter(module_id=self.kwargs.get("module_id"))
.annotate(cycle_id=F("issue_cycle__cycle_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 list(self, request, slug, project_id, module_id):
fields = [field for field in request.GET.get("fields", "").split(",") if field]
order_by = request.GET.get("order_by", "created_at")
filters = issue_filters(request.query_params, "GET")
issues = (
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"))
.filter(project_id=project_id)
.filter(workspace__slug=slug)
.select_related("project")
.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()
@@ -350,144 +353,114 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
.values("count") .values("count")
) )
.annotate( .annotate(
attachment_count=IssueAttachment.objects.filter( attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id"))
issue=OuterRef("id")
)
.order_by() .order_by()
.annotate(count=Func(F("id"), function="Count")) .annotate(count=Func(F("id"), function="Count"))
.values("count") .values("count")
) )
.annotate(
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
).distinct()
@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) issues = IssueStateSerializer(issues, many=True, fields=fields if fields else None).data
issue_dict = {str(issue["id"]): issue for issue in issues}
return Response(issue_dict, status=status.HTTP_200_OK)
# create multiple issues inside a module def create(self, request, slug, project_id, module_id):
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"}, {"error": "Issues are required"}, status=status.HTTP_400_BAD_REQUEST
status=status.HTTP_400_BAD_REQUEST,
) )
project = Project.objects.get(pk=project_id) module = Module.objects.get(
_ = ModuleIssue.objects.bulk_create( workspace__slug=slug, project_id=project_id, pk=module_id
[ )
ModuleIssue(
issue_id=str(issue), module_issues = list(ModuleIssue.objects.filter(issue_id__in=issues))
module_id=module_id,
project_id=project_id, update_module_issue_activity = []
workspace_id=project.workspace_id, records_to_update = []
created_by=request.user, record_to_create = []
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(
# create multiple module inside an issue records_to_update,
def create_issue_modules(self, request, slug, project_id, issue_id): ["module"],
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
]
issue = (self.get_queryset().filter(pk=issue_id).first()) # Capture Issue Activity
serializer = IssueSerializer(issue) issue_activity.delay(
return Response(serializer.data, status=status.HTTP_201_CREATED) type="module.activity.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, issue_id): def destroy(self, request, slug, project_id, module_id, pk):
module_issue = ModuleIssue.objects.get( module_issue = ModuleIssue.objects.get(
workspace__slug=slug, workspace__slug=slug, project_id=project_id, module_id=module_id, pk=pk
project_id=project_id,
module_id=module_id,
issue_id=issue_id,
) )
issue_activity.delay( issue_activity.delay(
type="module.activity.deleted", type="module.activity.deleted",
requested_data=json.dumps({"module_id": str(module_id)}), requested_data=json.dumps(
{
"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(issue_id), issue_id=str(module_issue.issue_id),
project_id=str(project_id), project_id=str(project_id),
current_instance=json.dumps({"module_name": module_issue.module.name}), current_instance=None,
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)
@@ -549,41 +522,3 @@ class ModuleFavoriteViewSet(BaseViewSet):
) )
module_favorite.delete() module_favorite.delete()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
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)
+16 -81
View File
@@ -1,5 +1,5 @@
# Django imports # Django imports
from django.db.models import Q, OuterRef, Exists from django.db.models import Q
from django.utils import timezone from django.utils import timezone
# Third party imports # Third party imports
@@ -15,9 +15,8 @@ from plane.db.models import (
IssueSubscriber, IssueSubscriber,
Issue, Issue,
WorkspaceMember, WorkspaceMember,
UserNotificationPreference,
) )
from plane.app.serializers import NotificationSerializer, UserNotificationPreferenceSerializer from plane.app.serializers import NotificationSerializer
class NotificationViewSet(BaseViewSet, BasePaginator): class NotificationViewSet(BaseViewSet, BasePaginator):
@@ -52,10 +51,8 @@ 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()) "true": Q(snoozed_till__lt=timezone.now()) | Q(snoozed_till__isnull=False),
| Q(snoozed_till__isnull=False), "false": Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True),
"false": Q(snoozed_till__gte=timezone.now())
| Q(snoozed_till__isnull=True),
} }
notifications = notifications.filter(snoozed_filters[snoozed]) notifications = notifications.filter(snoozed_filters[snoozed])
@@ -72,39 +69,17 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
# Subscribed issues # Subscribed issues
if type == "watching": if type == "watching":
issue_ids = ( issue_ids = IssueSubscriber.objects.filter(
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)
) 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( notifications = notifications.filter(entity_identifier__in=issue_ids)
entity_identifier__in=issue_ids
)
# Created issues # Created issues
if type == "created": if type == "created":
@@ -119,14 +94,10 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
issue_ids = Issue.objects.filter( issue_ids = Issue.objects.filter(
workspace__slug=slug, created_by=request.user workspace__slug=slug, created_by=request.user
).values_list("pk", flat=True) ).values_list("pk", flat=True)
notifications = notifications.filter( notifications = notifications.filter(entity_identifier__in=issue_ids)
entity_identifier__in=issue_ids
)
# Pagination # Pagination
if request.GET.get("per_page", False) and request.GET.get( if request.GET.get("per_page", False) and request.GET.get("cursor", False):
"cursor", False
):
return self.paginate( return self.paginate(
request=request, request=request,
queryset=(notifications), queryset=(notifications),
@@ -256,13 +227,11 @@ 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__lt=timezone.now()) | Q(snoozed_till__isnull=False)
| Q(snoozed_till__isnull=False)
) )
else: else:
notifications = notifications.filter( notifications = notifications.filter(
Q(snoozed_till__gte=timezone.now()) Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True),
| Q(snoozed_till__isnull=True),
) )
# Filter for archived or unarchive # Filter for archived or unarchive
@@ -276,18 +245,14 @@ 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( notifications = notifications.filter(entity_identifier__in=issue_ids)
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( notifications = notifications.filter(entity_identifier__in=issue_ids)
entity_identifier__in=issue_ids
)
# Created issues # Created issues
if type == "created": if type == "created":
@@ -302,9 +267,7 @@ class MarkAllReadNotificationViewSet(BaseViewSet):
issue_ids = Issue.objects.filter( issue_ids = Issue.objects.filter(
workspace__slug=slug, created_by=request.user workspace__slug=slug, created_by=request.user
).values_list("pk", flat=True) ).values_list("pk", flat=True)
notifications = notifications.filter( notifications = notifications.filter(entity_identifier__in=issue_ids)
entity_identifier__in=issue_ids
)
updated_notifications = [] updated_notifications = []
for notification in notifications: for notification in notifications:
@@ -314,31 +277,3 @@ class MarkAllReadNotificationViewSet(BaseViewSet):
updated_notifications, ["read_at"], batch_size=100 updated_notifications, ["read_at"], batch_size=100
) )
return Response({"message": "Successful"}, status=status.HTTP_200_OK) return Response({"message": "Successful"}, status=status.HTTP_200_OK)
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)
+41 -47
View File
@@ -1,5 +1,5 @@
# Python imports # Python imports
from datetime import date, datetime, timedelta from datetime import timedelta, date, datetime
# Django imports # Django imports
from django.db import connection from django.db import connection
@@ -7,19 +7,30 @@ 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
from plane.app.permissions import ProjectEntityPermission
from plane.app.serializers import (IssueLiteSerializer, PageFavoriteSerializer,
PageLogSerializer, PageSerializer,
SubPageSerializer)
from plane.db.models import (Issue, IssueActivity, IssueAssignee, Page,
PageFavorite, PageLog, ProjectMember)
# Module imports # Module imports
from .base import BaseAPIView, BaseViewSet from .base import BaseViewSet, BaseAPIView
from plane.app.permissions import ProjectEntityPermission
from plane.db.models import (
Page,
PageFavorite,
Issue,
IssueAssignee,
IssueActivity,
PageLog,
ProjectMember,
)
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):
@@ -86,9 +97,7 @@ 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( page = Page.objects.get(pk=pk, workspace__slug=slug, project_id=project_id)
pk=pk, workspace__slug=slug, project_id=project_id
)
if page.is_locked: if page.is_locked:
return Response( return Response(
@@ -118,9 +127,7 @@ 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( return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
except Page.DoesNotExist: except Page.DoesNotExist:
return Response( return Response(
{ {
@@ -150,26 +157,22 @@ 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)
pages = PageSerializer(queryset, many=True).data return Response(
return Response(pages, status=status.HTTP_200_OK) PageSerializer(queryset, many=True).data, status=status.HTTP_200_OK
def archive(self, request, slug, project_id, page_id):
page = Page.objects.get(
pk=page_id, workspace__slug=slug, project_id=project_id
) )
# only the owner or admin can archive the page def archive(self, request, slug, project_id, page_id):
page = Page.objects.get(pk=page_id, workspace__slug=slug, project_id=project_id)
# only the owner and admin can archive the page
if ( if (
ProjectMember.objects.filter( ProjectMember.objects.filter(
project_id=project_id, project_id=project_id, member=request.user, is_active=True, role__gt=20
member=request.user,
is_active=True,
role__lte=15,
).exists() ).exists()
and request.user.id != page.owned_by_id or request.user.id != page.owned_by_id
): ):
return Response( return Response(
{"error": "Only the owner or admin can archive the page"}, {"error": "Only the owner and admin can archive the page"},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
@@ -178,22 +181,17 @@ 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( page = Page.objects.get(pk=page_id, workspace__slug=slug, project_id=project_id)
pk=page_id, workspace__slug=slug, project_id=project_id
)
# only the owner or admin can un archive the page # only the owner and admin can un archive the page
if ( if (
ProjectMember.objects.filter( ProjectMember.objects.filter(
project_id=project_id, project_id=project_id, member=request.user, is_active=True, role__gt=20
member=request.user,
is_active=True,
role__lte=15,
).exists() ).exists()
and request.user.id != page.owned_by_id or request.user.id != page.owned_by_id
): ):
return Response( return Response(
{"error": "Only the owner or admin can un archive the page"}, {"error": "Only the owner and admin can un archive the page"},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
@@ -212,21 +210,17 @@ class PageViewSet(BaseViewSet):
workspace__slug=slug, workspace__slug=slug,
).filter(archived_at__isnull=False) ).filter(archived_at__isnull=False)
pages = PageSerializer(pages, many=True).data return Response(
return Response(pages, status=status.HTTP_200_OK) PageSerializer(pages, many=True).data, 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( page = Page.objects.get(pk=pk, workspace__slug=slug, project_id=project_id)
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, project_id=project_id, member=request.user, is_active=True, role__gt=20
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
): ):
+98 -120
View File
@@ -36,7 +36,6 @@ from plane.app.serializers import (
ProjectFavoriteSerializer, ProjectFavoriteSerializer,
ProjectDeployBoardSerializer, ProjectDeployBoardSerializer,
ProjectMemberAdminSerializer, ProjectMemberAdminSerializer,
ProjectMemberRoleSerializer,
) )
from plane.app.permissions import ( from plane.app.permissions import (
@@ -68,7 +67,7 @@ from plane.bgtasks.project_invitation_task import project_invitation
class ProjectViewSet(WebhookMixin, BaseViewSet): class ProjectViewSet(WebhookMixin, BaseViewSet):
serializer_class = ProjectListSerializer serializer_class = ProjectSerializer
model = Project model = Project
webhook_event = "project" webhook_event = "project"
@@ -76,20 +75,19 @@ 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( .filter(Q(project_projectmember__member=self.request.user) | Q(network=2))
Q(project_projectmember__member=self.request.user)
| Q(network=2)
)
.select_related( .select_related(
"workspace", "workspace", "workspace__owner", "default_assignee", "project_lead"
"workspace__owner",
"default_assignee",
"project_lead",
) )
.annotate( .annotate(
is_favorite=Exists( is_favorite=Exists(
@@ -161,11 +159,7 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
) )
def list(self, request, slug): def list(self, request, slug):
fields = [ fields = [field for field in request.GET.get("fields", "").split(",") if field]
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,
@@ -178,9 +172,7 @@ 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( if request.GET.get("per_page", False) and request.GET.get("cursor", False):
"cursor", False
):
return self.paginate( return self.paginate(
request=request, request=request,
queryset=(projects), queryset=(projects),
@@ -188,10 +180,12 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
projects, many=True projects, many=True
).data, ).data,
) )
projects = ProjectListSerializer(
projects, many=True, fields=fields if fields else None return Response(
).data ProjectListSerializer(
return Response(projects, status=status.HTTP_200_OK) projects, many=True, fields=fields if fields else None
).data
)
def create(self, request, slug): def create(self, request, slug):
try: try:
@@ -205,9 +199,7 @@ 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"], project_id=serializer.data["id"], member=request.user, role=20
member=request.user,
role=20,
) )
# Also create the issue property for the user # Also create the issue property for the user
_ = IssueProperty.objects.create( _ = IssueProperty.objects.create(
@@ -280,15 +272,9 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
] ]
) )
project = ( project = self.get_queryset().filter(pk=serializer.data["id"]).first()
self.get_queryset()
.filter(pk=serializer.data["id"])
.first()
)
serializer = ProjectListSerializer(project) serializer = ProjectListSerializer(project)
return Response( return Response(serializer.data, status=status.HTTP_201_CREATED)
serializer.data, status=status.HTTP_201_CREATED
)
return Response( return Response(
serializer.errors, serializer.errors,
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
@@ -301,8 +287,7 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
) )
except Workspace.DoesNotExist as e: except Workspace.DoesNotExist as e:
return Response( return Response(
{"error": "Workspace does not exist"}, {"error": "Workspace does not exist"}, status=status.HTTP_404_NOT_FOUND
status=status.HTTP_404_NOT_FOUND,
) )
except serializers.ValidationError as e: except serializers.ValidationError as e:
return Response( return Response(
@@ -327,9 +312,7 @@ 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", name=f"{project.name} Inbox", project=project, is_default=True
project=project,
is_default=True,
) )
# Create the triage state in Backlog group # Create the triage state in Backlog group
@@ -341,16 +324,10 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
color="#ff7700", color="#ff7700",
) )
project = ( project = self.get_queryset().filter(pk=serializer.data["id"]).first()
self.get_queryset()
.filter(pk=serializer.data["id"])
.first()
)
serializer = 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( return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
except IntegrityError as e: except IntegrityError as e:
if "already exists" in str(e): if "already exists" in str(e):
@@ -360,8 +337,7 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
) )
except (Project.DoesNotExist, Workspace.DoesNotExist): except (Project.DoesNotExist, Workspace.DoesNotExist):
return Response( return Response(
{"error": "Project does not exist"}, {"error": "Project does not exist"}, status=status.HTTP_404_NOT_FOUND
status=status.HTTP_404_NOT_FOUND,
) )
except serializers.ValidationError as e: except serializers.ValidationError as e:
return Response( return Response(
@@ -396,14 +372,11 @@ 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"}, {"error": "Emails are required"}, status=status.HTTP_400_BAD_REQUEST
status=status.HTTP_400_BAD_REQUEST,
) )
requesting_user = ProjectMember.objects.get( requesting_user = ProjectMember.objects.get(
workspace__slug=slug, workspace__slug=slug, project_id=project_id, member_id=request.user.id
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
@@ -577,9 +550,7 @@ 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 role=15 if project_invite.role >= 15 else project_invite.role,
if project_invite.role >= 15
else project_invite.role,
) )
else: else:
# Else make him active # Else make him active
@@ -685,25 +656,11 @@ 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")) if str(project_member.get("member_id")) == str(member.get("member_id"))
== str(member.get("member_id"))
] ]
bulk_project_members.append( bulk_project_members.append(
ProjectMember( ProjectMember(
@@ -711,9 +668,7 @@ 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 sort_order=sort_order[0] - 10000 if len(sort_order) else 65535,
if len(sort_order)
else 65535,
) )
) )
bulk_issue_props.append( bulk_issue_props.append(
@@ -724,6 +679,25 @@ 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,
@@ -734,12 +708,18 @@ class ProjectMemberViewSet(BaseViewSet):
bulk_issue_props, batch_size=10, ignore_conflicts=True bulk_issue_props, batch_size=10, ignore_conflicts=True
) )
project_members = ProjectMember.objects.filter(project_id=project_id, member_id__in=[member.get("member_id") for member in members]) serializer = ProjectMemberSerializer(project_members, many=True)
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):
# Get the list of project members for the project project_member = ProjectMember.objects.get(
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,
@@ -747,9 +727,10 @@ class ProjectMemberViewSet(BaseViewSet):
is_active=True, is_active=True,
).select_related("project", "member", "workspace") ).select_related("project", "member", "workspace")
serializer = ProjectMemberRoleSerializer( if project_member.role > 10:
project_members, fields=("id", "member", "role"), many=True serializer = ProjectMemberAdminSerializer(project_members, 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):
@@ -777,9 +758,7 @@ class ProjectMemberViewSet(BaseViewSet):
> requested_project_member.role > requested_project_member.role
): ):
return Response( return Response(
{ {"error": "You cannot update a role that is higher than your own role"},
"error": "You cannot update a role that is higher than your own role"
},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
@@ -818,9 +797,7 @@ 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,
) )
@@ -871,8 +848,7 @@ class AddTeamToProjectEndpoint(BaseAPIView):
if len(team_members) == 0: if len(team_members) == 0:
return Response( return Response(
{"error": "No such team exists"}, {"error": "No such team exists"}, status=status.HTTP_400_BAD_REQUEST
status=status.HTTP_400_BAD_REQUEST,
) )
workspace = Workspace.objects.get(slug=slug) workspace = Workspace.objects.get(slug=slug)
@@ -919,8 +895,7 @@ class ProjectIdentifierEndpoint(BaseAPIView):
if name == "": if name == "":
return Response( return Response(
{"error": "Name is required"}, {"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST
status=status.HTTP_400_BAD_REQUEST,
) )
exists = ProjectIdentifier.objects.filter( exists = ProjectIdentifier.objects.filter(
@@ -937,23 +912,16 @@ class ProjectIdentifierEndpoint(BaseAPIView):
if name == "": if name == "":
return Response( return Response(
{"error": "Name is required"}, {"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST
status=status.HTTP_400_BAD_REQUEST,
) )
if Project.objects.filter( if Project.objects.filter(identifier=name, workspace__slug=slug).exists():
identifier=name, workspace__slug=slug
).exists():
return Response( return Response(
{ {"error": "Cannot delete an identifier of an existing project"},
"error": "Cannot delete an identifier of an existing project"
},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
ProjectIdentifier.objects.filter( ProjectIdentifier.objects.filter(name=name, workspace__slug=slug).delete()
name=name, workspace__slug=slug
).delete()
return Response( return Response(
status=status.HTTP_204_NO_CONTENT, status=status.HTTP_204_NO_CONTENT,
@@ -971,9 +939,7 @@ class ProjectUserViewsEndpoint(BaseAPIView):
).first() ).first()
if project_member is None: if project_member is None:
return Response( return Response({"error": "Forbidden"}, status=status.HTTP_403_FORBIDDEN)
{"error": "Forbidden"}, status=status.HTTP_403_FORBIDDEN
)
view_props = project_member.view_props view_props = project_member.view_props
default_props = project_member.default_props default_props = project_member.default_props
@@ -981,12 +947,8 @@ class ProjectUserViewsEndpoint(BaseAPIView):
sort_order = project_member.sort_order sort_order = project_member.sort_order
project_member.view_props = request.data.get("view_props", view_props) project_member.view_props = request.data.get("view_props", view_props)
project_member.default_props = request.data.get( project_member.default_props = request.data.get("default_props", default_props)
"default_props", default_props project_member.preferences = request.data.get("preferences", preferences)
)
project_member.preferences = request.data.get(
"preferences", preferences
)
project_member.sort_order = request.data.get("sort_order", sort_order) project_member.sort_order = request.data.get("sort_order", sort_order)
project_member.save() project_member.save()
@@ -1048,11 +1010,18 @@ class ProjectPublicCoverImagesEndpoint(BaseAPIView):
def get(self, request): def get(self, request):
files = [] files = []
s3 = boto3.client( s3_client_params = {
"s3", "service_name": "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/",
@@ -1065,9 +1034,19 @@ 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"
files.append( if (
f"https://{settings.AWS_STORAGE_BUCKET_NAME}.s3.{settings.AWS_REGION}.amazonaws.com/{content['Key']}" hasattr(settings, "AWS_S3_CUSTOM_DOMAIN")
) 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)
@@ -1134,7 +1113,6 @@ class UserProjectRolesEndpoint(BaseAPIView):
).values("project_id", "role") ).values("project_id", "role")
project_members = { project_members = {
str(member["project_id"]): member["role"] str(member["project_id"]): member["role"] for member in project_members
for member in project_members
} }
return Response(project_members, status=status.HTTP_200_OK) return Response(project_members, status=status.HTTP_200_OK)
+12 -28
View File
@@ -10,15 +10,7 @@ from rest_framework.response import Response
# Module imports # Module imports
from .base import BaseAPIView from .base import BaseAPIView
from plane.db.models import ( from plane.db.models import Workspace, Project, Issue, Cycle, Module, Page, IssueView
Workspace,
Project,
Issue,
Cycle,
Module,
Page,
IssueView,
)
from plane.utils.issue_search import search_issues from plane.utils.issue_search import search_issues
@@ -33,9 +25,7 @@ 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( Workspace.objects.filter(q, workspace_member__member=self.request.user)
q, workspace_member__member=self.request.user
)
.distinct() .distinct()
.values("name", "id", "slug") .values("name", "id", "slug")
) )
@@ -48,8 +38,7 @@ class GlobalSearchEndpoint(BaseAPIView):
return ( return (
Project.objects.filter( Project.objects.filter(
q, q,
Q(project_projectmember__member=self.request.user) Q(project_projectmember__member=self.request.user) | Q(network=2),
| Q(network=2),
workspace__slug=slug, workspace__slug=slug,
) )
.distinct() .distinct()
@@ -61,8 +50,7 @@ class GlobalSearchEndpoint(BaseAPIView):
q = Q() q = Q()
for field in fields: for field in fields:
if field == "sequence_id": if field == "sequence_id":
# Match whole integers only (exclude decimal numbers) sequences = re.findall(r"\d+\.\d+|\d+", query)
sequences = re.findall(r"\b\d+\b", query)
for sequence_id in sequences: for sequence_id in sequences:
q |= Q(**{"sequence_id": sequence_id}) q |= Q(**{"sequence_id": sequence_id})
else: else:
@@ -180,9 +168,7 @@ 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 = request.query_params.get("workspace_search", "false")
"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:
@@ -222,13 +208,11 @@ 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 = request.query_params.get("workspace_search", "false")
"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)
@@ -249,9 +233,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( pk__in=Issue.issue_objects.filter(parent__isnull=False).values_list(
parent__isnull=False "parent_id", flat=True
).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)
@@ -269,8 +253,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: if module == "true":
issues = issues.exclude(issue_module__module=module) issues = issues.exclude(issue_module__isnull=False)
return Response( return Response(
issues.values( issues.values(
+7 -12
View File
@@ -9,12 +9,9 @@ from rest_framework.response import Response
from rest_framework import status from rest_framework import status
# Module imports # Module imports
from . import BaseViewSet, BaseAPIView from . import BaseViewSet
from plane.app.serializers import StateSerializer from plane.app.serializers import StateSerializer
from plane.app.permissions import ( from plane.app.permissions import ProjectEntityPermission
ProjectEntityPermission,
WorkspaceEntityPermission,
)
from plane.db.models import State, Issue from plane.db.models import State, Issue
@@ -25,6 +22,9 @@ 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,19 +77,14 @@ class StateViewSet(BaseViewSet):
) )
if state.default: if state.default:
return Response( return Response({"error": "Default state cannot be deleted"}, status=status.HTTP_400_BAD_REQUEST)
{"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,
) )
+11 -29
View File
@@ -43,9 +43,7 @@ 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( return Response({"is_instance_admin": is_admin}, status=status.HTTP_200_OK)
{"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
@@ -53,12 +51,7 @@ 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( return Response({"error": "You cannot deactivate your account since you are an instance admin"}, status=status.HTTP_400_BAD_REQUEST)
{
"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 = []
@@ -68,10 +61,7 @@ class UserEndpoint(BaseViewSet):
).annotate( ).annotate(
other_admin_exists=Count( other_admin_exists=Count(
Case( Case(
When( When(Q(role=20, is_active=True) & ~Q(member=request.user), then=1),
Q(role=20, is_active=True) & ~Q(member=request.user),
then=1,
),
default=0, default=0,
output_field=IntegerField(), output_field=IntegerField(),
) )
@@ -96,10 +86,7 @@ class UserEndpoint(BaseViewSet):
).annotate( ).annotate(
other_admin_exists=Count( other_admin_exists=Count(
Case( Case(
When( When(Q(role=20, is_active=True) & ~Q(member=request.user), then=1),
Q(role=20, is_active=True) & ~Q(member=request.user),
then=1,
),
default=0, default=0,
output_field=IntegerField(), output_field=IntegerField(),
) )
@@ -108,9 +95,7 @@ class UserEndpoint(BaseViewSet):
) )
for workspace in workspaces: for workspace in workspaces:
if workspace.other_admin_exists > 0 or ( if workspace.other_admin_exists > 0 or (workspace.total_members == 1):
workspace.total_members == 1
):
workspace.is_active = False workspace.is_active = False
workspaces_to_deactivate.append(workspace) workspaces_to_deactivate.append(workspace)
else: else:
@@ -149,9 +134,7 @@ 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( return Response({"message": "Updated successfully"}, status=status.HTTP_200_OK)
{"message": "Updated successfully"}, status=status.HTTP_200_OK
)
class UpdateUserTourCompletedEndpoint(BaseAPIView): class UpdateUserTourCompletedEndpoint(BaseAPIView):
@@ -159,16 +142,14 @@ 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( return Response({"message": "Updated successfully"}, status=status.HTTP_200_OK)
{"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( queryset = IssueActivity.objects.filter(actor=request.user).select_related(
actor=request.user "actor", "workspace", "issue", "project"
).select_related("actor", "workspace", "issue", "project") )
return self.paginate( return self.paginate(
request=request, request=request,
@@ -177,3 +158,4 @@ class UserActivityEndpoint(BaseAPIView, BasePaginator):
issue_activities, many=True issue_activities, many=True
).data, ).data,
) )

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