Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ea581a6948 |
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Adds three new lint plugins over the existing configuration:
|
||||
* This is used to lint staged files only.
|
||||
* We should remove this file once the entire codebase follows these rules.
|
||||
*/
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: [
|
||||
"custom",
|
||||
],
|
||||
parser: "@typescript-eslint/parser",
|
||||
settings: {
|
||||
"import/resolver": {
|
||||
typescript: {},
|
||||
node: {
|
||||
moduleDirectory: ["node_modules", "."],
|
||||
},
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
"import/order": [
|
||||
"error",
|
||||
{
|
||||
groups: ["builtin", "external", "internal", "parent", "sibling"],
|
||||
pathGroups: [
|
||||
{
|
||||
pattern: "react",
|
||||
group: "external",
|
||||
position: "before",
|
||||
},
|
||||
{
|
||||
pattern: "lucide-react",
|
||||
group: "external",
|
||||
position: "after",
|
||||
},
|
||||
{
|
||||
pattern: "@headlessui/**",
|
||||
group: "external",
|
||||
position: "after",
|
||||
},
|
||||
{
|
||||
pattern: "@plane/**",
|
||||
group: "external",
|
||||
position: "after",
|
||||
},
|
||||
{
|
||||
pattern: "@/**",
|
||||
group: "internal",
|
||||
},
|
||||
],
|
||||
pathGroupsExcludedImportTypes: ["builtin", "internal", "react"],
|
||||
alphabetize: {
|
||||
order: "asc",
|
||||
caseInsensitive: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
// This tells ESLint to load the config from the package `eslint-config-custom`
|
||||
extends: ["custom"],
|
||||
settings: {
|
||||
next: {
|
||||
rootDir: ["web/", "space/", "admin/"],
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,126 +0,0 @@
|
||||
name: "Build and Push Docker Image"
|
||||
description: "Reusable action for building and pushing Docker images"
|
||||
inputs:
|
||||
docker-username:
|
||||
description: "The Dockerhub username"
|
||||
required: true
|
||||
docker-token:
|
||||
description: "The Dockerhub Token"
|
||||
required: true
|
||||
|
||||
# Docker Image Options
|
||||
docker-image-owner:
|
||||
description: "The owner of the Docker image"
|
||||
required: true
|
||||
docker-image-name:
|
||||
description: "The name of the Docker image"
|
||||
required: true
|
||||
build-context:
|
||||
description: "The build context"
|
||||
required: true
|
||||
default: "."
|
||||
dockerfile-path:
|
||||
description: "The path to the Dockerfile"
|
||||
required: true
|
||||
build-args:
|
||||
description: "The build arguments"
|
||||
required: false
|
||||
default: ""
|
||||
|
||||
# Buildx Options
|
||||
buildx-driver:
|
||||
description: "Buildx driver"
|
||||
required: true
|
||||
default: "docker-container"
|
||||
buildx-version:
|
||||
description: "Buildx version"
|
||||
required: true
|
||||
default: "latest"
|
||||
buildx-platforms:
|
||||
description: "Buildx platforms"
|
||||
required: true
|
||||
default: "linux/amd64"
|
||||
buildx-endpoint:
|
||||
description: "Buildx endpoint"
|
||||
required: true
|
||||
default: "default"
|
||||
|
||||
# Release Build Options
|
||||
build-release:
|
||||
description: "Flag to publish release"
|
||||
required: false
|
||||
default: "false"
|
||||
build-prerelease:
|
||||
description: "Flag to publish prerelease"
|
||||
required: false
|
||||
default: "false"
|
||||
release-version:
|
||||
description: "The release version"
|
||||
required: false
|
||||
default: "latest"
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Set Docker Tag
|
||||
shell: bash
|
||||
env:
|
||||
IMG_OWNER: ${{ inputs.docker-image-owner }}
|
||||
IMG_NAME: ${{ inputs.docker-image-name }}
|
||||
BUILD_RELEASE: ${{ inputs.build-release }}
|
||||
IS_PRERELEASE: ${{ inputs.build-prerelease }}
|
||||
REL_VERSION: ${{ inputs.release-version }}
|
||||
run: |
|
||||
FLAT_BRANCH_VERSION=$(echo "${{ github.ref_name }}" | sed 's/[^a-zA-Z0-9.-]//g')
|
||||
|
||||
if [ "${{ env.BUILD_RELEASE }}" == "true" ]; then
|
||||
semver_regex="^v([0-9]+)\.([0-9]+)\.([0-9]+)(-[a-zA-Z0-9]+(-[a-zA-Z0-9]+)*)?$"
|
||||
if [[ ! ${{ env.REL_VERSION }} =~ $semver_regex ]]; then
|
||||
echo "Invalid Release Version Format : ${{ env.REL_VERSION }}"
|
||||
echo "Please provide a valid SemVer version"
|
||||
echo "e.g. v1.2.3 or v1.2.3-alpha-1"
|
||||
echo "Exiting the build process"
|
||||
exit 1 # Exit with status 1 to fail the step
|
||||
fi
|
||||
|
||||
TAG=${{ env.IMG_OWNER }}/${{ env.IMG_NAME }}:${{ env.REL_VERSION }}
|
||||
|
||||
if [ "${{ env.IS_PRERELEASE }}" != "true" ]; then
|
||||
TAG=${TAG},${{ env.IMG_OWNER }}/${{ env.IMG_NAME }}:stable
|
||||
fi
|
||||
elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
|
||||
TAG=${{ env.IMG_OWNER }}/${{ env.IMG_NAME }}:latest
|
||||
else
|
||||
TAG=${{ env.IMG_OWNER }}/${{ env.IMG_NAME }}:${FLAT_BRANCH_VERSION}
|
||||
fi
|
||||
|
||||
echo "DOCKER_TAGS=${TAG}" >> $GITHUB_ENV
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ inputs.docker-username }}
|
||||
password: ${{ inputs.docker-token}}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
driver: ${{ inputs.buildx-driver }}
|
||||
version: ${{ inputs.buildx-version }}
|
||||
endpoint: ${{ inputs.buildx-endpoint }}
|
||||
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build and Push Docker Image
|
||||
uses: docker/build-push-action@v5.1.0
|
||||
with:
|
||||
context: ${{ inputs.build-context }}
|
||||
file: ${{ inputs.dockerfile-path }}
|
||||
platforms: ${{ inputs.buildx-platforms }}
|
||||
tags: ${{ env.DOCKER_TAGS }}
|
||||
push: true
|
||||
build-args: ${{ inputs.build-args }}
|
||||
env:
|
||||
DOCKER_BUILDKIT: 1
|
||||
DOCKER_USERNAME: ${{ inputs.docker-username }}
|
||||
DOCKER_PASSWORD: ${{ inputs.docker-token }}
|
||||
@@ -83,7 +83,7 @@ jobs:
|
||||
endpoint: ${{ env.BUILDX_ENDPOINT }}
|
||||
|
||||
- name: Build and Push to Docker Hub
|
||||
uses: docker/build-push-action@v6.9.0
|
||||
uses: docker/build-push-action@v5.1.0
|
||||
with:
|
||||
context: ./aio
|
||||
file: ./aio/Dockerfile-base-full
|
||||
@@ -124,7 +124,7 @@ jobs:
|
||||
endpoint: ${{ env.BUILDX_ENDPOINT }}
|
||||
|
||||
- name: Build and Push to Docker Hub
|
||||
uses: docker/build-push-action@v6.9.0
|
||||
uses: docker/build-push-action@v5.1.0
|
||||
with:
|
||||
context: ./aio
|
||||
file: ./aio/Dockerfile-base-slim
|
||||
|
||||
@@ -128,7 +128,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build and Push to Docker Hub
|
||||
uses: docker/build-push-action@v6.9.0
|
||||
uses: docker/build-push-action@v5.1.0
|
||||
with:
|
||||
context: .
|
||||
file: ./aio/Dockerfile-app
|
||||
@@ -188,7 +188,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build and Push to Docker Hub
|
||||
uses: docker/build-push-action@v6.9.0
|
||||
uses: docker/build-push-action@v5.1.0
|
||||
with:
|
||||
context: .
|
||||
file: ./aio/Dockerfile-app
|
||||
|
||||
+331
-278
@@ -1,45 +1,29 @@
|
||||
name: Branch Build CE
|
||||
name: Branch Build
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
build_type:
|
||||
description: "Type of build to run"
|
||||
required: true
|
||||
type: choice
|
||||
default: "Build"
|
||||
options:
|
||||
- "Build"
|
||||
- "Release"
|
||||
releaseVersion:
|
||||
description: "Release Version"
|
||||
type: string
|
||||
default: v0.0.0
|
||||
isPrerelease:
|
||||
description: "Is Pre-release"
|
||||
type: boolean
|
||||
default: false
|
||||
required: true
|
||||
arm64:
|
||||
description: "Build for ARM64 architecture"
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
# push:
|
||||
# branches:
|
||||
# - master
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- preview
|
||||
release:
|
||||
types: [released, prereleased]
|
||||
|
||||
env:
|
||||
TARGET_BRANCH: ${{ github.ref_name }}
|
||||
TARGET_BRANCH: ${{ github.ref_name || github.event.release.target_commitish }}
|
||||
ARM64_BUILD: ${{ github.event.inputs.arm64 }}
|
||||
BUILD_TYPE: ${{ github.event.inputs.build_type }}
|
||||
RELEASE_VERSION: ${{ github.event.inputs.releaseVersion }}
|
||||
IS_PRERELEASE: ${{ github.event.inputs.isPrerelease }}
|
||||
IS_PRERELEASE: ${{ github.event.release.prerelease }}
|
||||
|
||||
jobs:
|
||||
branch_build_setup:
|
||||
name: Build Setup
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
gh_branch_name: ${{ steps.set_env_variables.outputs.TARGET_BRANCH }}
|
||||
gh_buildx_driver: ${{ steps.set_env_variables.outputs.BUILDX_DRIVER }}
|
||||
@@ -52,24 +36,13 @@ jobs:
|
||||
build_space: ${{ steps.changed_files.outputs.space_any_changed }}
|
||||
build_web: ${{ steps.changed_files.outputs.web_any_changed }}
|
||||
build_live: ${{ steps.changed_files.outputs.live_any_changed }}
|
||||
|
||||
dh_img_web: ${{ steps.set_env_variables.outputs.DH_IMG_WEB }}
|
||||
dh_img_space: ${{ steps.set_env_variables.outputs.DH_IMG_SPACE }}
|
||||
dh_img_admin: ${{ steps.set_env_variables.outputs.DH_IMG_ADMIN }}
|
||||
dh_img_live: ${{ steps.set_env_variables.outputs.DH_IMG_LIVE }}
|
||||
dh_img_backend: ${{ steps.set_env_variables.outputs.DH_IMG_BACKEND }}
|
||||
dh_img_proxy: ${{ steps.set_env_variables.outputs.DH_IMG_PROXY }}
|
||||
|
||||
build_type: ${{steps.set_env_variables.outputs.BUILD_TYPE}}
|
||||
build_release: ${{ steps.set_env_variables.outputs.BUILD_RELEASE }}
|
||||
build_prerelease: ${{ steps.set_env_variables.outputs.BUILD_PRERELEASE }}
|
||||
release_version: ${{ steps.set_env_variables.outputs.RELEASE_VERSION }}
|
||||
flat_branch_name: ${{ steps.set_env_variables.outputs.FLAT_BRANCH_NAME }}
|
||||
|
||||
steps:
|
||||
- id: set_env_variables
|
||||
name: Set Environment Variables
|
||||
run: |
|
||||
if [ "${{ env.ARM64_BUILD }}" == "true" ] || ([ "${{ env.BUILD_TYPE }}" == "Release" ] && [ "${{ env.IS_PRERELEASE }}" != "true" ]); then
|
||||
if [ "${{ env.TARGET_BRANCH }}" == "master" ] || [ "${{ env.ARM64_BUILD }}" == "true" ] || ([ "${{ github.event_name }}" == "release" ] && [ "${{ env.IS_PRERELEASE }}" != "true" ]); then
|
||||
echo "BUILDX_DRIVER=cloud" >> $GITHUB_OUTPUT
|
||||
echo "BUILDX_VERSION=lab:latest" >> $GITHUB_OUTPUT
|
||||
echo "BUILDX_PLATFORMS=linux/amd64,linux/arm64" >> $GITHUB_OUTPUT
|
||||
@@ -80,43 +53,9 @@ jobs:
|
||||
echo "BUILDX_PLATFORMS=linux/amd64" >> $GITHUB_OUTPUT
|
||||
echo "BUILDX_ENDPOINT=" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
BR_NAME=$( echo "${{ env.TARGET_BRANCH }}" |sed 's/[^a-zA-Z0-9.-]//g')
|
||||
echo "TARGET_BRANCH=$BR_NAME" >> $GITHUB_OUTPUT
|
||||
|
||||
echo "DH_IMG_WEB=plane-frontend" >> $GITHUB_OUTPUT
|
||||
echo "DH_IMG_SPACE=plane-space" >> $GITHUB_OUTPUT
|
||||
echo "DH_IMG_ADMIN=plane-admin" >> $GITHUB_OUTPUT
|
||||
echo "DH_IMG_LIVE=plane-live" >> $GITHUB_OUTPUT
|
||||
echo "DH_IMG_BACKEND=plane-backend" >> $GITHUB_OUTPUT
|
||||
echo "DH_IMG_PROXY=plane-proxy" >> $GITHUB_OUTPUT
|
||||
|
||||
echo "BUILD_TYPE=${{env.BUILD_TYPE}}" >> $GITHUB_OUTPUT
|
||||
BUILD_RELEASE=false
|
||||
BUILD_PRERELEASE=false
|
||||
RELVERSION="latest"
|
||||
|
||||
if [ "${{ env.BUILD_TYPE }}" == "Release" ]; then
|
||||
FLAT_RELEASE_VERSION=$(echo "${{ env.RELEASE_VERSION }}" | sed 's/[^a-zA-Z0-9.-]//g')
|
||||
echo "FLAT_RELEASE_VERSION=${FLAT_RELEASE_VERSION}" >> $GITHUB_OUTPUT
|
||||
|
||||
semver_regex="^v([0-9]+)\.([0-9]+)\.([0-9]+)(-[a-zA-Z0-9]+(-[a-zA-Z0-9]+)*)?$"
|
||||
if [[ ! $FLAT_RELEASE_VERSION =~ $semver_regex ]]; then
|
||||
echo "Invalid Release Version Format : $FLAT_RELEASE_VERSION"
|
||||
echo "Please provide a valid SemVer version"
|
||||
echo "e.g. v1.2.3 or v1.2.3-alpha-1"
|
||||
echo "Exiting the build process"
|
||||
exit 1 # Exit with status 1 to fail the step
|
||||
fi
|
||||
BUILD_RELEASE=true
|
||||
RELVERSION=$FLAT_RELEASE_VERSION
|
||||
|
||||
if [ "${{ env.IS_PRERELEASE }}" == "true" ]; then
|
||||
BUILD_PRERELEASE=true
|
||||
fi
|
||||
fi
|
||||
echo "BUILD_RELEASE=${BUILD_RELEASE}" >> $GITHUB_OUTPUT
|
||||
echo "BUILD_PRERELEASE=${BUILD_PRERELEASE}" >> $GITHUB_OUTPUT
|
||||
echo "RELEASE_VERSION=${RELVERSION}" >> $GITHUB_OUTPUT
|
||||
echo "TARGET_BRANCH=${{ env.TARGET_BRANCH }}" >> $GITHUB_OUTPUT
|
||||
flat_branch_name=$(echo ${{ env.TARGET_BRANCH }} | sed 's/[^a-zA-Z0-9\._]/-/g')
|
||||
echo "FLAT_BRANCH_NAME=${flat_branch_name}" >> $GITHUB_OUTPUT
|
||||
|
||||
- id: checkout_files
|
||||
name: Checkout Files
|
||||
@@ -134,24 +73,24 @@ jobs:
|
||||
admin:
|
||||
- admin/**
|
||||
- packages/**
|
||||
- "package.json"
|
||||
- "yarn.lock"
|
||||
- "tsconfig.json"
|
||||
- "turbo.json"
|
||||
- 'package.json'
|
||||
- 'yarn.lock'
|
||||
- 'tsconfig.json'
|
||||
- 'turbo.json'
|
||||
space:
|
||||
- space/**
|
||||
- packages/**
|
||||
- "package.json"
|
||||
- "yarn.lock"
|
||||
- "tsconfig.json"
|
||||
- "turbo.json"
|
||||
- 'package.json'
|
||||
- 'yarn.lock'
|
||||
- 'tsconfig.json'
|
||||
- 'turbo.json'
|
||||
web:
|
||||
- web/**
|
||||
- packages/**
|
||||
- "package.json"
|
||||
- "yarn.lock"
|
||||
- "tsconfig.json"
|
||||
- "turbo.json"
|
||||
- 'package.json'
|
||||
- 'yarn.lock'
|
||||
- 'tsconfig.json'
|
||||
- 'turbo.json'
|
||||
live:
|
||||
- live/**
|
||||
- packages/**
|
||||
@@ -160,224 +99,338 @@ jobs:
|
||||
- 'tsconfig.json'
|
||||
- 'turbo.json'
|
||||
|
||||
branch_build_push_admin:
|
||||
if: ${{ needs.branch_build_setup.outputs.build_admin == 'true' || github.event_name == 'workflow_dispatch' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
|
||||
name: Build-Push Admin Docker Image
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [branch_build_setup]
|
||||
steps:
|
||||
- id: checkout_files
|
||||
name: Checkout Files
|
||||
uses: actions/checkout@v4
|
||||
- name: Admin Build and Push
|
||||
uses: ./.github/actions/build-push-ce
|
||||
with:
|
||||
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
|
||||
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
|
||||
release-version: ${{ needs.branch_build_setup.outputs.release_version }}
|
||||
docker-username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
docker-token: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
docker-image-owner: makeplane
|
||||
docker-image-name: ${{ needs.branch_build_setup.outputs.dh_img_admin }}
|
||||
build-context: .
|
||||
dockerfile-path: ./admin/Dockerfile.admin
|
||||
buildx-driver: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
|
||||
buildx-version: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
|
||||
buildx-platforms: ${{ needs.branch_build_setup.outputs.gh_buildx_platforms }}
|
||||
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
|
||||
|
||||
branch_build_push_web:
|
||||
if: ${{ needs.branch_build_setup.outputs.build_web == 'true' || github.event_name == 'workflow_dispatch' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
|
||||
if: ${{ needs.branch_build_setup.outputs.build_web == 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
|
||||
name: Build-Push Web Docker Image
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [branch_build_setup]
|
||||
env:
|
||||
FRONTEND_TAG: makeplane/plane-frontend:${{ needs.branch_build_setup.outputs.flat_branch_name }}
|
||||
TARGET_BRANCH: ${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
||||
BUILDX_DRIVER: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
|
||||
BUILDX_VERSION: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
|
||||
BUILDX_PLATFORMS: ${{ needs.branch_build_setup.outputs.gh_buildx_platforms }}
|
||||
BUILDX_ENDPOINT: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
|
||||
steps:
|
||||
- id: checkout_files
|
||||
name: Checkout Files
|
||||
uses: actions/checkout@v4
|
||||
- name: Web Build and Push
|
||||
uses: ./.github/actions/build-push-ce
|
||||
- name: Set Frontend Docker Tag
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" == "release" ]; then
|
||||
TAG=makeplane/plane-frontend:${{ github.event.release.tag_name }}
|
||||
if [ "${{ env.IS_PRERELEASE }}" != "true" ]; then
|
||||
TAG=${TAG},makeplane/plane-frontend:stable
|
||||
fi
|
||||
elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
|
||||
TAG=makeplane/plane-frontend:latest
|
||||
else
|
||||
TAG=${{ env.FRONTEND_TAG }}
|
||||
fi
|
||||
echo "FRONTEND_TAG=${TAG}" >> $GITHUB_ENV
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
|
||||
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
|
||||
release-version: ${{ needs.branch_build_setup.outputs.release_version }}
|
||||
docker-username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
docker-token: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
docker-image-owner: makeplane
|
||||
docker-image-name: ${{ needs.branch_build_setup.outputs.dh_img_web }}
|
||||
build-context: .
|
||||
dockerfile-path: ./web/Dockerfile.web
|
||||
buildx-driver: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
|
||||
buildx-version: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
|
||||
buildx-platforms: ${{ needs.branch_build_setup.outputs.gh_buildx_platforms }}
|
||||
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
driver: ${{ env.BUILDX_DRIVER }}
|
||||
version: ${{ env.BUILDX_VERSION }}
|
||||
endpoint: ${{ env.BUILDX_ENDPOINT }}
|
||||
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build and Push Frontend to Docker Container Registry
|
||||
uses: docker/build-push-action@v5.1.0
|
||||
with:
|
||||
context: .
|
||||
file: ./web/Dockerfile.web
|
||||
platforms: ${{ env.BUILDX_PLATFORMS }}
|
||||
tags: ${{ env.FRONTEND_TAG }}
|
||||
push: true
|
||||
env:
|
||||
DOCKER_BUILDKIT: 1
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
branch_build_push_admin:
|
||||
if: ${{ needs.branch_build_setup.outputs.build_admin== 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
|
||||
name: Build-Push Admin Docker Image
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [branch_build_setup]
|
||||
env:
|
||||
ADMIN_TAG: makeplane/plane-admin:${{ needs.branch_build_setup.outputs.flat_branch_name }}
|
||||
TARGET_BRANCH: ${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
||||
BUILDX_DRIVER: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
|
||||
BUILDX_VERSION: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
|
||||
BUILDX_PLATFORMS: ${{ needs.branch_build_setup.outputs.gh_buildx_platforms }}
|
||||
BUILDX_ENDPOINT: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
|
||||
steps:
|
||||
- name: Set Admin Docker Tag
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" == "release" ]; then
|
||||
TAG=makeplane/plane-admin:${{ github.event.release.tag_name }}
|
||||
if [ "${{ env.IS_PRERELEASE }}" != "true" ]; then
|
||||
TAG=${TAG},makeplane/plane-admin:stable
|
||||
fi
|
||||
elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
|
||||
TAG=makeplane/plane-admin:latest
|
||||
else
|
||||
TAG=${{ env.ADMIN_TAG }}
|
||||
fi
|
||||
echo "ADMIN_TAG=${TAG}" >> $GITHUB_ENV
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
driver: ${{ env.BUILDX_DRIVER }}
|
||||
version: ${{ env.BUILDX_VERSION }}
|
||||
endpoint: ${{ env.BUILDX_ENDPOINT }}
|
||||
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build and Push Frontend to Docker Container Registry
|
||||
uses: docker/build-push-action@v5.1.0
|
||||
with:
|
||||
context: .
|
||||
file: ./admin/Dockerfile.admin
|
||||
platforms: ${{ env.BUILDX_PLATFORMS }}
|
||||
tags: ${{ env.ADMIN_TAG }}
|
||||
push: true
|
||||
env:
|
||||
DOCKER_BUILDKIT: 1
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
branch_build_push_space:
|
||||
if: ${{ needs.branch_build_setup.outputs.build_space == 'true' || github.event_name == 'workflow_dispatch' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
|
||||
if: ${{ needs.branch_build_setup.outputs.build_space == 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
|
||||
name: Build-Push Space Docker Image
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [branch_build_setup]
|
||||
env:
|
||||
SPACE_TAG: makeplane/plane-space:${{ needs.branch_build_setup.outputs.flat_branch_name }}
|
||||
TARGET_BRANCH: ${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
||||
BUILDX_DRIVER: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
|
||||
BUILDX_VERSION: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
|
||||
BUILDX_PLATFORMS: ${{ needs.branch_build_setup.outputs.gh_buildx_platforms }}
|
||||
BUILDX_ENDPOINT: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
|
||||
steps:
|
||||
- id: checkout_files
|
||||
name: Checkout Files
|
||||
uses: actions/checkout@v4
|
||||
- name: Space Build and Push
|
||||
uses: ./.github/actions/build-push-ce
|
||||
with:
|
||||
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
|
||||
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
|
||||
release-version: ${{ needs.branch_build_setup.outputs.release_version }}
|
||||
docker-username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
docker-token: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
docker-image-owner: makeplane
|
||||
docker-image-name: ${{ needs.branch_build_setup.outputs.dh_img_space }}
|
||||
build-context: .
|
||||
dockerfile-path: ./space/Dockerfile.space
|
||||
buildx-driver: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
|
||||
buildx-version: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
|
||||
buildx-platforms: ${{ needs.branch_build_setup.outputs.gh_buildx_platforms }}
|
||||
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
|
||||
- name: Set Space Docker Tag
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" == "release" ]; then
|
||||
TAG=makeplane/plane-space:${{ github.event.release.tag_name }}
|
||||
if [ "${{ env.IS_PRERELEASE }}" != "true" ]; then
|
||||
TAG=${TAG},makeplane/plane-space:stable
|
||||
fi
|
||||
elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
|
||||
TAG=makeplane/plane-space:latest
|
||||
else
|
||||
TAG=${{ env.SPACE_TAG }}
|
||||
fi
|
||||
echo "SPACE_TAG=${TAG}" >> $GITHUB_ENV
|
||||
|
||||
branch_build_push_live:
|
||||
if: ${{ needs.branch_build_setup.outputs.build_live == 'true' || github.event_name == 'workflow_dispatch' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
|
||||
name: Build-Push Live Collaboration Docker Image
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [branch_build_setup]
|
||||
steps:
|
||||
- id: checkout_files
|
||||
name: Checkout Files
|
||||
uses: actions/checkout@v4
|
||||
- name: Live Build and Push
|
||||
uses: ./.github/actions/build-push-ce
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
|
||||
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
|
||||
release-version: ${{ needs.branch_build_setup.outputs.release_version }}
|
||||
docker-username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
docker-token: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
docker-image-owner: makeplane
|
||||
docker-image-name: ${{ needs.branch_build_setup.outputs.dh_img_live }}
|
||||
build-context: .
|
||||
dockerfile-path: ./live/Dockerfile.live
|
||||
buildx-driver: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
|
||||
buildx-version: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
|
||||
buildx-platforms: ${{ needs.branch_build_setup.outputs.gh_buildx_platforms }}
|
||||
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
driver: ${{ env.BUILDX_DRIVER }}
|
||||
version: ${{ env.BUILDX_VERSION }}
|
||||
endpoint: ${{ env.BUILDX_ENDPOINT }}
|
||||
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build and Push Space to Docker Hub
|
||||
uses: docker/build-push-action@v5.1.0
|
||||
with:
|
||||
context: .
|
||||
file: ./space/Dockerfile.space
|
||||
platforms: ${{ env.BUILDX_PLATFORMS }}
|
||||
tags: ${{ env.SPACE_TAG }}
|
||||
push: true
|
||||
env:
|
||||
DOCKER_BUILDKIT: 1
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
branch_build_push_apiserver:
|
||||
if: ${{ needs.branch_build_setup.outputs.build_apiserver == 'true' || github.event_name == 'workflow_dispatch' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
|
||||
if: ${{ needs.branch_build_setup.outputs.build_apiserver == 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
|
||||
name: Build-Push API Server Docker Image
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [branch_build_setup]
|
||||
env:
|
||||
BACKEND_TAG: makeplane/plane-backend:${{ needs.branch_build_setup.outputs.flat_branch_name }}
|
||||
TARGET_BRANCH: ${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
||||
BUILDX_DRIVER: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
|
||||
BUILDX_VERSION: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
|
||||
BUILDX_PLATFORMS: ${{ needs.branch_build_setup.outputs.gh_buildx_platforms }}
|
||||
BUILDX_ENDPOINT: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
|
||||
steps:
|
||||
- id: checkout_files
|
||||
name: Checkout Files
|
||||
uses: actions/checkout@v4
|
||||
- name: Backend Build and Push
|
||||
uses: ./.github/actions/build-push-ce
|
||||
- name: Set Backend Docker Tag
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" == "release" ]; then
|
||||
TAG=makeplane/plane-backend:${{ github.event.release.tag_name }}
|
||||
if [ "${{ env.IS_PRERELEASE }}" != "true" ]; then
|
||||
TAG=${TAG},makeplane/plane-backend:stable
|
||||
fi
|
||||
elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
|
||||
TAG=makeplane/plane-backend:latest
|
||||
else
|
||||
TAG=${{ env.BACKEND_TAG }}
|
||||
fi
|
||||
echo "BACKEND_TAG=${TAG}" >> $GITHUB_ENV
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
|
||||
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
|
||||
release-version: ${{ needs.branch_build_setup.outputs.release_version }}
|
||||
docker-username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
docker-token: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
docker-image-owner: makeplane
|
||||
docker-image-name: ${{ needs.branch_build_setup.outputs.dh_img_backend }}
|
||||
build-context: ./apiserver
|
||||
dockerfile-path: ./apiserver/Dockerfile.api
|
||||
buildx-driver: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
|
||||
buildx-version: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
|
||||
buildx-platforms: ${{ needs.branch_build_setup.outputs.gh_buildx_platforms }}
|
||||
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
driver: ${{ env.BUILDX_DRIVER }}
|
||||
version: ${{ env.BUILDX_VERSION }}
|
||||
endpoint: ${{ env.BUILDX_ENDPOINT }}
|
||||
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build and Push Backend to Docker Hub
|
||||
uses: docker/build-push-action@v5.1.0
|
||||
with:
|
||||
context: ./apiserver
|
||||
file: ./apiserver/Dockerfile.api
|
||||
platforms: ${{ env.BUILDX_PLATFORMS }}
|
||||
push: true
|
||||
tags: ${{ env.BACKEND_TAG }}
|
||||
env:
|
||||
DOCKER_BUILDKIT: 1
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
branch_build_push_live:
|
||||
if: ${{ needs.branch_build_setup.outputs.build_live == 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
|
||||
name: Build-Push Live Collaboration Docker Image
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [branch_build_setup]
|
||||
env:
|
||||
LIVE_TAG: makeplane/plane-live:${{ needs.branch_build_setup.outputs.flat_branch_name }}
|
||||
TARGET_BRANCH: ${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
||||
BUILDX_DRIVER: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
|
||||
BUILDX_VERSION: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
|
||||
BUILDX_PLATFORMS: ${{ needs.branch_build_setup.outputs.gh_buildx_platforms }}
|
||||
BUILDX_ENDPOINT: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
|
||||
steps:
|
||||
- name: Set Live Docker Tag
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" == "release" ]; then
|
||||
TAG=makeplane/plane-live:${{ github.event.release.tag_name }}
|
||||
if [ "${{ github.event.release.prerelease }}" != "true" ]; then
|
||||
TAG=${TAG},makeplane/plane-live:stable
|
||||
fi
|
||||
elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
|
||||
TAG=makeplane/plane-live:latest
|
||||
else
|
||||
TAG=${{ env.LIVE_TAG }}
|
||||
fi
|
||||
echo "LIVE_TAG=${TAG}" >> $GITHUB_ENV
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
driver: ${{ env.BUILDX_DRIVER }}
|
||||
version: ${{ env.BUILDX_VERSION }}
|
||||
endpoint: ${{ env.BUILDX_ENDPOINT }}
|
||||
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build and Push Live Server to Docker Hub
|
||||
uses: docker/build-push-action@v5.1.0
|
||||
with:
|
||||
context: .
|
||||
file: ./live/Dockerfile.live
|
||||
platforms: ${{ env.BUILDX_PLATFORMS }}
|
||||
tags: ${{ env.LIVE_TAG }}
|
||||
push: true
|
||||
env:
|
||||
DOCKER_BUILDKIT: 1
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
branch_build_push_proxy:
|
||||
if: ${{ needs.branch_build_setup.outputs.build_proxy == 'true' || github.event_name == 'workflow_dispatch' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
|
||||
if: ${{ needs.branch_build_setup.outputs.build_proxy == 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
|
||||
name: Build-Push Proxy Docker Image
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [branch_build_setup]
|
||||
steps:
|
||||
- id: checkout_files
|
||||
name: Checkout Files
|
||||
uses: actions/checkout@v4
|
||||
- name: Proxy Build and Push
|
||||
uses: ./.github/actions/build-push-ce
|
||||
with:
|
||||
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
|
||||
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
|
||||
release-version: ${{ needs.branch_build_setup.outputs.release_version }}
|
||||
docker-username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
docker-token: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
docker-image-owner: makeplane
|
||||
docker-image-name: ${{ needs.branch_build_setup.outputs.dh_img_proxy }}
|
||||
build-context: ./nginx
|
||||
dockerfile-path: ./nginx/Dockerfile
|
||||
buildx-driver: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
|
||||
buildx-version: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
|
||||
buildx-platforms: ${{ needs.branch_build_setup.outputs.gh_buildx_platforms }}
|
||||
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
|
||||
|
||||
attach_assets_to_build:
|
||||
if: ${{ needs.branch_build_setup.outputs.build_type == 'Build' }}
|
||||
name: Attach Assets to Build
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [branch_build_setup]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Update Assets
|
||||
run: |
|
||||
cp ./deploy/selfhost/install.sh deploy/selfhost/setup.sh
|
||||
|
||||
- name: Attach Assets
|
||||
id: attach_assets
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: selfhost-assets
|
||||
retention-days: 2
|
||||
path: |
|
||||
${{ github.workspace }}/deploy/selfhost/setup.sh
|
||||
${{ github.workspace }}/deploy/selfhost/restore.sh
|
||||
${{ github.workspace }}/deploy/selfhost/docker-compose.yml
|
||||
${{ github.workspace }}/deploy/selfhost/variables.env
|
||||
|
||||
publish_release:
|
||||
if: ${{ needs.branch_build_setup.outputs.build_type == 'Release' }}
|
||||
name: Build Release
|
||||
runs-on: ubuntu-20.04
|
||||
needs:
|
||||
[
|
||||
branch_build_setup,
|
||||
branch_build_push_admin,
|
||||
branch_build_push_web,
|
||||
branch_build_push_space,
|
||||
branch_build_push_live,
|
||||
branch_build_push_apiserver,
|
||||
branch_build_push_proxy,
|
||||
]
|
||||
env:
|
||||
REL_VERSION: ${{ needs.branch_build_setup.outputs.release_version }}
|
||||
PROXY_TAG: makeplane/plane-proxy:${{ needs.branch_build_setup.outputs.flat_branch_name }}
|
||||
TARGET_BRANCH: ${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
||||
BUILDX_DRIVER: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
|
||||
BUILDX_VERSION: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
|
||||
BUILDX_PLATFORMS: ${{ needs.branch_build_setup.outputs.gh_buildx_platforms }}
|
||||
BUILDX_ENDPOINT: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
- name: Set Proxy Docker Tag
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" == "release" ]; then
|
||||
TAG=makeplane/plane-proxy:${{ github.event.release.tag_name }}
|
||||
if [ "${{ env.IS_PRERELEASE }}" != "true" ]; then
|
||||
TAG=${TAG},makeplane/plane-proxy:stable
|
||||
fi
|
||||
elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
|
||||
TAG=makeplane/plane-proxy:latest
|
||||
else
|
||||
TAG=${{ env.PROXY_TAG }}
|
||||
fi
|
||||
echo "PROXY_TAG=${TAG}" >> $GITHUB_ENV
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
driver: ${{ env.BUILDX_DRIVER }}
|
||||
version: ${{ env.BUILDX_VERSION }}
|
||||
endpoint: ${{ env.BUILDX_ENDPOINT }}
|
||||
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Update Assets
|
||||
run: |
|
||||
cp ./deploy/selfhost/install.sh deploy/selfhost/setup.sh
|
||||
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: softprops/action-gh-release@v2.1.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token
|
||||
- name: Build and Push Plane-Proxy to Docker Hub
|
||||
uses: docker/build-push-action@v5.1.0
|
||||
with:
|
||||
tag_name: ${{ env.REL_VERSION }}
|
||||
name: ${{ env.REL_VERSION }}
|
||||
draft: false
|
||||
prerelease: ${{ env.IS_PRERELEASE }}
|
||||
generate_release_notes: true
|
||||
files: |
|
||||
${{ github.workspace }}/deploy/selfhost/setup.sh
|
||||
${{ github.workspace }}/deploy/selfhost/restore.sh
|
||||
${{ github.workspace }}/deploy/selfhost/docker-compose.yml
|
||||
${{ github.workspace }}/deploy/selfhost/variables.env
|
||||
context: ./nginx
|
||||
file: ./nginx/Dockerfile
|
||||
platforms: ${{ env.BUILDX_PLATFORMS }}
|
||||
tags: ${{ env.PROXY_TAG }}
|
||||
push: true
|
||||
env:
|
||||
DOCKER_BUILDKIT: 1
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
@@ -29,11 +29,11 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
@@ -59,6 +59,6 @@ jobs:
|
||||
# ./location_of_script_within_repo/buildscript.sh
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
uses: github/codeql-action/analyze@v2
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
@@ -8,20 +8,37 @@ on:
|
||||
|
||||
env:
|
||||
CURRENT_BRANCH: ${{ github.ref_name }}
|
||||
TARGET_BRANCH: "preview" # The target branch that you would like to merge changes like develop
|
||||
SOURCE_BRANCH: ${{ vars.SYNC_SOURCE_BRANCH_NAME }} # The sync branch such as "sync/ce"
|
||||
TARGET_BRANCH: ${{ vars.SYNC_TARGET_BRANCH_NAME }} # The target branch that you would like to merge changes like develop
|
||||
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }} # Personal access token required to modify contents and workflows
|
||||
REVIEWER: ${{ vars.SYNC_PR_REVIEWER }}
|
||||
ACCOUNT_USER_NAME: ${{ vars.ACCOUNT_USER_NAME }}
|
||||
ACCOUNT_USER_EMAIL: ${{ vars.ACCOUNT_USER_EMAIL }}
|
||||
|
||||
jobs:
|
||||
create_pull_request:
|
||||
Check_Branch:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
BRANCH_MATCH: ${{ steps.check-branch.outputs.MATCH }}
|
||||
steps:
|
||||
- name: Check if current branch matches the secret
|
||||
id: check-branch
|
||||
run: |
|
||||
if [ "$CURRENT_BRANCH" = "$SOURCE_BRANCH" ]; then
|
||||
echo "MATCH=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "MATCH=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
Create_PR:
|
||||
if: ${{ needs.Check_Branch.outputs.BRANCH_MATCH == 'true' }}
|
||||
needs: [Check_Branch]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v4.1.1
|
||||
with:
|
||||
fetch-depth: 0 # Fetch all history for all branches and tags
|
||||
|
||||
@@ -42,11 +59,11 @@ jobs:
|
||||
- name: Create PR to Target Branch
|
||||
run: |
|
||||
# get all pull requests and check if there is already a PR
|
||||
PR_EXISTS=$(gh pr list --base $TARGET_BRANCH --head $CURRENT_BRANCH --state open --json number | jq '.[] | .number')
|
||||
PR_EXISTS=$(gh pr list --base $TARGET_BRANCH --head $SOURCE_BRANCH --state open --json number | jq '.[] | .number')
|
||||
if [ -n "$PR_EXISTS" ]; then
|
||||
echo "Pull Request already exists: $PR_EXISTS"
|
||||
else
|
||||
echo "Creating new pull request"
|
||||
PR_URL=$(gh pr create --base $TARGET_BRANCH --head $CURRENT_BRANCH --title "${{ vars.SYNC_PR_TITLE }}" --body "")
|
||||
PR_URL=$(gh pr create --base $TARGET_BRANCH --head $SOURCE_BRANCH --title "sync: community changes" --body "")
|
||||
echo "Pull Request created: $PR_URL"
|
||||
fi
|
||||
@@ -79,7 +79,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build and Push to Docker Hub
|
||||
uses: docker/build-push-action@v6.9.0
|
||||
uses: docker/build-push-action@v5.1.0
|
||||
with:
|
||||
context: .
|
||||
file: ./aio/Dockerfile-app
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v4.1.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"*.{ts,tsx,js,jsx}": ["eslint -c ./.eslintrc-staged.js", "prettier --check"]
|
||||
}
|
||||
+1
-1
@@ -4,7 +4,7 @@ Thank you for showing an interest in contributing to Plane! All kinds of contrib
|
||||
|
||||
## Submitting an issue
|
||||
|
||||
Before submitting a new issue, please search the [issues](https://github.com/makeplane/plane/issues) tab. Maybe an issue or discussion already exists and might inform you of workarounds. Otherwise, you can give new information.
|
||||
Before submitting a new issue, please search the [issues](https://github.com/makeplane/plane/issues) tab. Maybe an issue or discussion already exists and might inform you of workarounds. Otherwise, you can give new informplaneation.
|
||||
|
||||
While we want to fix all the [issues](https://github.com/makeplane/plane/issues), before fixing a bug we need to be able to reproduce and confirm it. Please provide us with a minimal reproduction scenario using a repository or [Gist](https://gist.github.com/). Having a live, reproducible scenario gives us the information without asking questions back & forth with additional questions like:
|
||||
|
||||
|
||||
+35
-30
@@ -1,39 +1,44 @@
|
||||
# Security policy
|
||||
This document outlines the security protocols and vulnerability reporting guidelines for the Plane project. Ensuring the security of our systems is a top priority, and while we work diligently to maintain robust protection, vulnerabilities may still occur. We highly value the community’s role in identifying and reporting security concerns to uphold the integrity of our systems and safeguard our users.
|
||||
# Security Policy
|
||||
|
||||
## Reporting a vulnerability
|
||||
If you have identified a security vulnerability, submit your findings to [security@plane.so](mailto:security@plane.so).
|
||||
Ensure your report includes all relevant information needed for us to reproduce and assess the issue. Include the IP address or URL of the affected system.
|
||||
This document outlines security procedures and vulnerabilities reporting for the Plane project.
|
||||
|
||||
To ensure a responsible and effective disclosure process, please adhere to the following:
|
||||
At Plane, we safeguarding the security of our systems with top priority. Despite our efforts, vulnerabilities may still exist. We greatly appreciate your assistance in identifying and reporting any such vulnerabilities to help us maintain the integrity of our systems and protect our clients.
|
||||
|
||||
- Maintain confidentiality and refrain from publicly disclosing the vulnerability until we have had the opportunity to investigate and address the issue.
|
||||
- Refrain from running automated vulnerability scans on our infrastructure or dashboard without prior consent. Contact us to set up a sandbox environment if necessary.
|
||||
- Do not exploit any discovered vulnerabilities for malicious purposes, such as accessing or altering user data.
|
||||
- Do not engage in physical security attacks, social engineering, distributed denial of service (DDoS) attacks, spam campaigns, or attacks on third-party applications as part of your vulnerability testing.
|
||||
To report a security vulnerability, please email us directly at security@plane.so with a detailed description of the vulnerability and steps to reproduce it. Please refrain from disclosing the vulnerability publicly until we have had an opportunity to review and address it.
|
||||
|
||||
## Out of scope
|
||||
While we appreciate all efforts to assist in improving our security, please note that the following types of vulnerabilities are considered out of scope:
|
||||
## Out of Scope Vulnerabilities
|
||||
|
||||
- Vulnerabilities requiring man-in-the-middle (MITM) attacks or physical access to a user’s device.
|
||||
- Content spoofing or text injection issues without a clear attack vector or the ability to modify HTML/CSS.
|
||||
- Issues related to email spoofing.
|
||||
- Missing DNSSEC, CAA, or CSP headers.
|
||||
- Absence of secure or HTTP-only flags on non-sensitive cookies.
|
||||
We appreciate your help in identifying vulnerabilities. However, please note that the following types of vulnerabilities are considered out of scope:
|
||||
|
||||
## Our commitment
|
||||
- Attacks requiring MITM or physical access to a user's device.
|
||||
- Content spoofing and text injection issues without demonstrating an attack vector or ability to modify HTML/CSS.
|
||||
- Email spoofing.
|
||||
- Missing DNSSEC, CAA, CSP headers.
|
||||
- Lack of Secure or HTTP only flag on non-sensitive cookies.
|
||||
|
||||
At Plane, we are committed to maintaining transparent and collaborative communication throughout the vulnerability resolution process. Here's what you can expect from us:
|
||||
## Reporting Process
|
||||
|
||||
- **Response Time** <br/>
|
||||
We will acknowledge receipt of your vulnerability report within three business days and provide an estimated timeline for resolution.
|
||||
- **Legal Protection** <br/>
|
||||
We will not initiate legal action against you for reporting vulnerabilities, provided you adhere to the reporting guidelines.
|
||||
- **Confidentiality** <br/>
|
||||
Your report will be treated with confidentiality. We will not disclose your personal information to third parties without your consent.
|
||||
- **Recognition** <br/>
|
||||
With your permission, we are happy to publicly acknowledge your contribution to improving our security once the issue is resolved.
|
||||
- **Timely Resolution** <br/>
|
||||
We are committed to working closely with you throughout the resolution process, providing timely updates as necessary. Our goal is to address all reported vulnerabilities swiftly, and we will actively engage with you to coordinate a responsible disclosure once the issue is fully resolved.
|
||||
If you discover a vulnerability, please adhere to the following reporting process:
|
||||
|
||||
We appreciate your help in ensuring the security of our platform. Your contributions are crucial to protecting our users and maintaining a secure environment. Thank you for working with us to keep Plane safe.
|
||||
1. Email your findings to security@plane.so.
|
||||
2. Refrain from running automated scanners on our infrastructure or dashboard without prior consent. Contact us to set up a sandbox environment if necessary.
|
||||
3. Do not exploit the vulnerability for malicious purposes, such as downloading excessive data or altering user data.
|
||||
4. Maintain confidentiality and refrain from disclosing the vulnerability until it has been resolved.
|
||||
5. Avoid using physical security attacks, social engineering, distributed denial of service, spam, or third-party applications.
|
||||
|
||||
When reporting a vulnerability, please provide sufficient information to allow us to reproduce and address the issue promptly. Include the IP address or URL of the affected system, along with a detailed description of the vulnerability.
|
||||
|
||||
## Our Commitment
|
||||
|
||||
We are committed to promptly addressing reported vulnerabilities and maintaining open communication throughout the resolution process. Here's what you can expect from us:
|
||||
|
||||
- **Response Time:** We will acknowledge receipt of your report within three business days and provide an expected resolution date.
|
||||
- **Legal Protection:** We will not pursue legal action against you for reporting vulnerabilities, provided you adhere to the reporting guidelines.
|
||||
- **Confidentiality:** Your report will be treated with strict confidentiality. We will not disclose your personal information to third parties without your consent.
|
||||
- **Progress Updates:** We will keep you informed of our progress in resolving the reported vulnerability.
|
||||
- **Recognition:** With your permission, we will publicly acknowledge you as the discoverer of the vulnerability.
|
||||
- **Timely Resolution:** We strive to resolve all reported vulnerabilities promptly and will actively participate in the publication process once the issue is resolved.
|
||||
|
||||
We appreciate your cooperation in helping us maintain the security of our systems and protecting our clients. Thank you for your contributions to our security efforts.
|
||||
|
||||
reference: https://supabase.com/.well-known/security.txt
|
||||
|
||||
+48
-4
@@ -1,8 +1,52 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["@plane/eslint-config/next.js"],
|
||||
extends: ["custom"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
project: true,
|
||||
settings: {
|
||||
"import/resolver": {
|
||||
typescript: {},
|
||||
node: {
|
||||
moduleDirectory: ["node_modules", "."],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
rules: {
|
||||
"import/order": [
|
||||
"error",
|
||||
{
|
||||
groups: ["builtin", "external", "internal", "parent", "sibling",],
|
||||
pathGroups: [
|
||||
{
|
||||
pattern: "react",
|
||||
group: "external",
|
||||
position: "before",
|
||||
},
|
||||
{
|
||||
pattern: "lucide-react",
|
||||
group: "external",
|
||||
position: "after",
|
||||
},
|
||||
{
|
||||
pattern: "@headlessui/**",
|
||||
group: "external",
|
||||
position: "after",
|
||||
},
|
||||
{
|
||||
pattern: "@plane/**",
|
||||
group: "external",
|
||||
position: "after",
|
||||
},
|
||||
{
|
||||
pattern: "@/**",
|
||||
group: "internal",
|
||||
}
|
||||
],
|
||||
pathGroupsExcludedImportTypes: ["builtin", "internal", "react"],
|
||||
alphabetize: {
|
||||
order: "asc",
|
||||
caseInsensitive: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
@@ -2,14 +2,15 @@
|
||||
|
||||
import { FC, useEffect, useRef } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// plane helpers
|
||||
import { useOutsideClickDetector } from "@plane/helpers";
|
||||
// components
|
||||
import { HelpSection, SidebarMenu, SidebarDropdown } from "@/components/admin-sidebar";
|
||||
// hooks
|
||||
import { HelpSection, SidebarMenu, SidebarDropdown } from "@/components/admin-sidebar";
|
||||
import { useTheme } from "@/hooks/store";
|
||||
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
||||
// components
|
||||
|
||||
export const InstanceSidebar: FC = observer(() => {
|
||||
export interface IInstanceSidebar {}
|
||||
|
||||
export const InstanceSidebar: FC<IInstanceSidebar> = observer(() => {
|
||||
// store
|
||||
const { isSidebarCollapsed, toggleSidebar } = useTheme();
|
||||
|
||||
|
||||
@@ -5,13 +5,11 @@ import { observer } from "mobx-react";
|
||||
import { useTheme as useNextTheme } from "next-themes";
|
||||
import { LogOut, UserCog2, Palette } from "lucide-react";
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
// plane ui
|
||||
import { Avatar } from "@plane/ui";
|
||||
// helpers
|
||||
import { API_BASE_URL, cn } from "@/helpers/common.helper";
|
||||
import { getFileURL } from "@/helpers/file.helper";
|
||||
// hooks
|
||||
import { API_BASE_URL, cn } from "@/helpers/common.helper";
|
||||
import { useTheme, useUser } from "@/hooks/store";
|
||||
// helpers
|
||||
// services
|
||||
import { AuthService } from "@/services/auth.service";
|
||||
|
||||
@@ -124,7 +122,7 @@ export const SidebarDropdown = observer(() => {
|
||||
<Menu.Button className="grid place-items-center outline-none">
|
||||
<Avatar
|
||||
name={currentUser.display_name}
|
||||
src={getFileURL(currentUser.avatar_url)}
|
||||
src={currentUser.avatar ?? undefined}
|
||||
size={24}
|
||||
shape="square"
|
||||
className="!text-base"
|
||||
|
||||
@@ -7,7 +7,11 @@ import { Button } from "@plane/ui";
|
||||
import InstanceFailureDarkImage from "@/public/instance/instance-failure-dark.svg";
|
||||
import InstanceFailureImage from "@/public/instance/instance-failure.svg";
|
||||
|
||||
export const InstanceFailureView: FC = () => {
|
||||
type InstanceFailureViewProps = {
|
||||
// mutate: () => void;
|
||||
};
|
||||
|
||||
export const InstanceFailureView: FC<InstanceFailureViewProps> = () => {
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
||||
const instanceImage = resolvedTheme === "dark" ? InstanceFailureDarkImage : InstanceFailureImage;
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect } from "react";
|
||||
|
||||
const useOutsideClickDetector = (ref: React.RefObject<HTMLElement>, callback: () => void) => {
|
||||
const handleClick = (event: MouseEvent) => {
|
||||
if (ref.current && !ref.current.contains(event.target as Node)) {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener("mousedown", handleClick);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClick);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export default useOutsideClickDetector;
|
||||
@@ -1,5 +1,5 @@
|
||||
// helpers
|
||||
import { API_BASE_URL } from "@/helpers/common.helper";
|
||||
import { API_BASE_URL } from "helpers/common.helper";
|
||||
// services
|
||||
import { APIService } from "@/services/api.service";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// helpers
|
||||
import { API_BASE_URL } from "helpers/common.helper";
|
||||
// types
|
||||
import type { IUser } from "@plane/types";
|
||||
// helpers
|
||||
import { API_BASE_URL } from "@/helpers/common.helper";
|
||||
// services
|
||||
import { APIService } from "@/services/api.service";
|
||||
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
// helpers
|
||||
import { API_BASE_URL } from "@/helpers/common.helper";
|
||||
|
||||
/**
|
||||
* @description combine the file path with the base URL
|
||||
* @param {string} path
|
||||
* @returns {string} final URL with the base URL
|
||||
*/
|
||||
export const getFileURL = (path: string): string | undefined => {
|
||||
if (!path) return undefined;
|
||||
const isValidURL = path.startsWith("http");
|
||||
if (isValidURL) return path;
|
||||
return `${API_BASE_URL}${path}`;
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
/**
|
||||
* @description
|
||||
* This function test whether a URL is valid or not.
|
||||
*
|
||||
* It accepts URLs with or without the protocol.
|
||||
* @param {string} url
|
||||
* @returns {boolean}
|
||||
* @example
|
||||
* checkURLValidity("https://example.com") => true
|
||||
* checkURLValidity("example.com") => true
|
||||
* checkURLValidity("example") => false
|
||||
*/
|
||||
export const checkURLValidity = (url: string): boolean => {
|
||||
if (!url) return false;
|
||||
|
||||
// regex to support complex query parameters and fragments
|
||||
const urlPattern =
|
||||
/^(https?:\/\/)?((([a-z\d-]+\.)*[a-z\d-]+\.[a-z]{2,6})|(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}))(:\d+)?(\/[\w.-]*)*(\?[^#\s]*)?(#[\w-]*)?$/i;
|
||||
|
||||
return urlPattern.test(url);
|
||||
};
|
||||
Vendored
+1
-1
@@ -2,4 +2,4 @@
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
|
||||
+10
-11
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "admin",
|
||||
"version": "0.23.1",
|
||||
"version": "0.22.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "turbo run develop",
|
||||
@@ -8,25 +8,23 @@
|
||||
"build": "next build",
|
||||
"preview": "next build && next start",
|
||||
"start": "next start",
|
||||
"lint": "eslint . --ext .ts,.tsx",
|
||||
"lint:errors": "eslint . --ext .ts,.tsx --quiet"
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^1.7.19",
|
||||
"@plane/constants": "*",
|
||||
"@plane/helpers": "*",
|
||||
"@plane/types": "*",
|
||||
"@plane/ui": "*",
|
||||
"@sentry/nextjs": "^8.32.0",
|
||||
"@plane/constants": "*",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"@types/lodash": "^4.17.0",
|
||||
"autoprefixer": "10.4.14",
|
||||
"axios": "^1.7.4",
|
||||
"js-cookie": "^3.0.5",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.356.0",
|
||||
"mobx": "^6.12.0",
|
||||
"mobx-react": "^9.1.1",
|
||||
"next": "^14.2.12",
|
||||
"next": "^14.2.3",
|
||||
"next-themes": "^0.2.1",
|
||||
"postcss": "^8.4.38",
|
||||
"react": "^18.3.1",
|
||||
@@ -38,14 +36,15 @@
|
||||
"zxcvbn": "^4.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@plane/eslint-config": "*",
|
||||
"@plane/typescript-config": "*",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/node": "18.16.1",
|
||||
"@types/react": "^18.3.11",
|
||||
"@types/react": "^18.2.48",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@types/zxcvbn": "^4.4.4",
|
||||
"eslint-config-custom": "*",
|
||||
"tailwind-config-custom": "*",
|
||||
"typescript": "5.3.3"
|
||||
"tsconfig": "*",
|
||||
"typescript": "^5.4.2"
|
||||
}
|
||||
}
|
||||
|
||||
+12
-6
@@ -1,15 +1,21 @@
|
||||
{
|
||||
"extends": "@plane/typescript-config/nextjs.json",
|
||||
"extends": "tsconfig/nextjs.json",
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"],
|
||||
"compilerOptions": {
|
||||
"plugins": [{ "name": "next" }],
|
||||
"baseUrl": ".",
|
||||
"jsx": "preserve",
|
||||
"esModuleInterop": true,
|
||||
"paths": {
|
||||
"@/*": ["core/*"],
|
||||
"@/helpers/*": ["helpers/*"],
|
||||
"@/public/*": ["public/*"],
|
||||
"@/plane-admin/*": ["ce/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "next.config.js", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,5 @@ ADMIN_BASE_URL=
|
||||
SPACE_BASE_URL=
|
||||
APP_BASE_URL=
|
||||
|
||||
|
||||
# Hard delete files after days
|
||||
HARD_DELETE_AFTER_DAYS=60
|
||||
HARD_DELETE_AFTER_DAYS=60
|
||||
|
||||
@@ -4,7 +4,6 @@ FROM python:3.12.5-alpine AS backend
|
||||
ENV PYTHONDONTWRITEBYTECODE 1
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
|
||||
ENV INSTANCE_CHANGELOG_URL https://api.plane.so/api/public/anchor/8e1c2e4c7bc5493eb7731be3862f6960/pages/
|
||||
|
||||
WORKDIR /code
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ FROM python:3.12.5-alpine AS backend
|
||||
ENV PYTHONDONTWRITEBYTECODE 1
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
|
||||
ENV INSTANCE_CHANGELOG_URL https://api.plane.so/api/public/anchor/8e1c2e4c7bc5493eb7731be3862f6960/pages/
|
||||
|
||||
RUN apk --no-cache add \
|
||||
"bash~=5.2" \
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"name": "plane-api",
|
||||
"version": "0.23.1"
|
||||
"version": "0.22.0"
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ from .issue import (
|
||||
IssueSerializer,
|
||||
LabelSerializer,
|
||||
IssueLinkSerializer,
|
||||
IssueAttachmentSerializer,
|
||||
IssueCommentSerializer,
|
||||
IssueAttachmentSerializer,
|
||||
IssueActivitySerializer,
|
||||
@@ -18,4 +19,4 @@ from .module import (
|
||||
ModuleIssueSerializer,
|
||||
ModuleLiteSerializer,
|
||||
)
|
||||
from .intake import IntakeIssueSerializer
|
||||
from .inbox import InboxIssueSerializer
|
||||
|
||||
+3
-5
@@ -1,17 +1,15 @@
|
||||
# Module improts
|
||||
from .base import BaseSerializer
|
||||
from .issue import IssueExpandSerializer
|
||||
from plane.db.models import IntakeIssue
|
||||
from rest_framework import serializers
|
||||
from plane.db.models import InboxIssue
|
||||
|
||||
|
||||
class IntakeIssueSerializer(BaseSerializer):
|
||||
class InboxIssueSerializer(BaseSerializer):
|
||||
|
||||
issue_detail = IssueExpandSerializer(read_only=True, source="issue")
|
||||
inbox = serializers.UUIDField(source="intake.id", read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = IntakeIssue
|
||||
model = InboxIssue
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"id",
|
||||
@@ -11,7 +11,7 @@ from plane.db.models import (
|
||||
IssueType,
|
||||
IssueActivity,
|
||||
IssueAssignee,
|
||||
FileAsset,
|
||||
IssueAttachment,
|
||||
IssueComment,
|
||||
IssueLabel,
|
||||
IssueLink,
|
||||
@@ -31,7 +31,6 @@ from .user import UserLiteSerializer
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import URLValidator
|
||||
|
||||
|
||||
class IssueSerializer(BaseSerializer):
|
||||
assignees = serializers.ListField(
|
||||
child=serializers.PrimaryKeyRelatedField(
|
||||
@@ -316,7 +315,7 @@ class IssueLinkSerializer(BaseSerializer):
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
|
||||
|
||||
def validate_url(self, value):
|
||||
# Check URL format
|
||||
validate_url = URLValidator()
|
||||
@@ -360,7 +359,7 @@ class IssueLinkSerializer(BaseSerializer):
|
||||
|
||||
class IssueAttachmentSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = FileAsset
|
||||
model = IssueAttachment
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"id",
|
||||
|
||||
@@ -19,8 +19,6 @@ class ProjectSerializer(BaseSerializer):
|
||||
sort_order = serializers.FloatField(read_only=True)
|
||||
member_role = serializers.IntegerField(read_only=True)
|
||||
is_deployed = serializers.BooleanField(read_only=True)
|
||||
cover_image_url = serializers.CharField(read_only=True)
|
||||
inbox_view = serializers.BooleanField(read_only=True, source="intake_view")
|
||||
|
||||
class Meta:
|
||||
model = Project
|
||||
@@ -34,7 +32,6 @@ class ProjectSerializer(BaseSerializer):
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"deleted_at",
|
||||
"cover_image_url",
|
||||
]
|
||||
|
||||
def validate(self, data):
|
||||
@@ -90,8 +87,6 @@ class ProjectSerializer(BaseSerializer):
|
||||
|
||||
|
||||
class ProjectLiteSerializer(BaseSerializer):
|
||||
cover_image_url = serializers.CharField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Project
|
||||
fields = [
|
||||
@@ -102,6 +97,5 @@ class ProjectLiteSerializer(BaseSerializer):
|
||||
"icon_prop",
|
||||
"emoji",
|
||||
"description",
|
||||
"cover_image_url",
|
||||
]
|
||||
read_only_fields = fields
|
||||
|
||||
@@ -13,7 +13,6 @@ class UserLiteSerializer(BaseSerializer):
|
||||
"last_name",
|
||||
"email",
|
||||
"avatar",
|
||||
"avatar_url",
|
||||
"display_name",
|
||||
"email",
|
||||
]
|
||||
|
||||
@@ -3,7 +3,7 @@ from .state import urlpatterns as state_patterns
|
||||
from .issue import urlpatterns as issue_patterns
|
||||
from .cycle import urlpatterns as cycle_patterns
|
||||
from .module import urlpatterns as module_patterns
|
||||
from .intake import urlpatterns as intake_patterns
|
||||
from .inbox import urlpatterns as inbox_patterns
|
||||
from .member import urlpatterns as member_patterns
|
||||
|
||||
urlpatterns = [
|
||||
@@ -12,6 +12,6 @@ urlpatterns = [
|
||||
*issue_patterns,
|
||||
*cycle_patterns,
|
||||
*module_patterns,
|
||||
*intake_patterns,
|
||||
*inbox_patterns,
|
||||
*member_patterns,
|
||||
]
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
from django.urls import path
|
||||
|
||||
from plane.api.views import InboxIssueAPIEndpoint
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/",
|
||||
InboxIssueAPIEndpoint.as_view(),
|
||||
name="inbox-issue",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/<uuid:issue_id>/",
|
||||
InboxIssueAPIEndpoint.as_view(),
|
||||
name="inbox-issue",
|
||||
),
|
||||
]
|
||||
@@ -1,27 +0,0 @@
|
||||
from django.urls import path
|
||||
|
||||
from plane.api.views import IntakeIssueAPIEndpoint
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/",
|
||||
IntakeIssueAPIEndpoint.as_view(),
|
||||
name="inbox-issue",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/<uuid:issue_id>/",
|
||||
IntakeIssueAPIEndpoint.as_view(),
|
||||
name="inbox-issue",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/intake-issues/",
|
||||
IntakeIssueAPIEndpoint.as_view(),
|
||||
name="intake-issue",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/intake-issues/<uuid:issue_id>/",
|
||||
IntakeIssueAPIEndpoint.as_view(),
|
||||
name="intake-issue",
|
||||
),
|
||||
]
|
||||
@@ -27,4 +27,5 @@ from .module import (
|
||||
|
||||
from .member import ProjectMemberAPIEndpoint
|
||||
|
||||
from .intake import IntakeIssueAPIEndpoint
|
||||
from .inbox import InboxIssueAPIEndpoint
|
||||
|
||||
|
||||
@@ -13,12 +13,8 @@ from django.db.models import (
|
||||
Q,
|
||||
Sum,
|
||||
FloatField,
|
||||
Case,
|
||||
When,
|
||||
Value,
|
||||
)
|
||||
from django.db.models.functions import Cast, Concat
|
||||
from django.db import models
|
||||
from django.db.models.functions import Cast
|
||||
|
||||
# Third party imports
|
||||
from rest_framework import status
|
||||
@@ -36,7 +32,7 @@ from plane.db.models import (
|
||||
CycleIssue,
|
||||
Issue,
|
||||
Project,
|
||||
FileAsset,
|
||||
IssueAttachment,
|
||||
IssueLink,
|
||||
ProjectMember,
|
||||
UserFavorite,
|
||||
@@ -78,7 +74,6 @@ class CycleAPIEndpoint(BaseAPIView):
|
||||
filter=Q(
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -89,7 +84,6 @@ class CycleAPIEndpoint(BaseAPIView):
|
||||
issue_cycle__issue__state__group="completed",
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -100,7 +94,6 @@ class CycleAPIEndpoint(BaseAPIView):
|
||||
issue_cycle__issue__state__group="cancelled",
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -111,7 +104,6 @@ class CycleAPIEndpoint(BaseAPIView):
|
||||
issue_cycle__issue__state__group="started",
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -122,7 +114,6 @@ class CycleAPIEndpoint(BaseAPIView):
|
||||
issue_cycle__issue__state__group="unstarted",
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -133,7 +124,6 @@ class CycleAPIEndpoint(BaseAPIView):
|
||||
issue_cycle__issue__state__group="backlog",
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -217,7 +207,8 @@ class CycleAPIEndpoint(BaseAPIView):
|
||||
# Incomplete Cycles
|
||||
if cycle_view == "incomplete":
|
||||
queryset = queryset.filter(
|
||||
Q(end_date__gte=timezone.now()) | Q(end_date__isnull=True),
|
||||
Q(end_date__gte=timezone.now().date())
|
||||
| Q(end_date__isnull=True),
|
||||
)
|
||||
return self.paginate(
|
||||
request=request,
|
||||
@@ -318,7 +309,10 @@ class CycleAPIEndpoint(BaseAPIView):
|
||||
|
||||
request_data = request.data
|
||||
|
||||
if cycle.end_date is not None and cycle.end_date < timezone.now():
|
||||
if (
|
||||
cycle.end_date is not None
|
||||
and cycle.end_date < timezone.now().date()
|
||||
):
|
||||
if "sort_order" in request_data:
|
||||
# Can only change sort order
|
||||
request_data = {
|
||||
@@ -411,6 +405,10 @@ class CycleAPIEndpoint(BaseAPIView):
|
||||
)
|
||||
# Delete the cycle
|
||||
cycle.delete()
|
||||
# Delete the cycle issues
|
||||
CycleIssue.objects.filter(
|
||||
cycle_id=self.kwargs.get("pk"),
|
||||
).delete()
|
||||
# Delete the user favorite cycle
|
||||
UserFavorite.objects.filter(
|
||||
entity_type="cycle",
|
||||
@@ -443,7 +441,6 @@ class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView):
|
||||
filter=Q(
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -454,7 +451,6 @@ class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView):
|
||||
issue_cycle__issue__state__group="completed",
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -465,7 +461,6 @@ class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView):
|
||||
issue_cycle__issue__state__group="cancelled",
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -476,7 +471,6 @@ class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView):
|
||||
issue_cycle__issue__state__group="started",
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -487,7 +481,6 @@ class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView):
|
||||
issue_cycle__issue__state__group="unstarted",
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -498,7 +491,6 @@ class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView):
|
||||
issue_cycle__issue__state__group="backlog",
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -512,7 +504,6 @@ class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView):
|
||||
issue_cycle__issue__state__group="completed",
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -523,7 +514,6 @@ class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView):
|
||||
issue_cycle__issue__state__group="started",
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -547,7 +537,7 @@ class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView):
|
||||
cycle = Cycle.objects.get(
|
||||
pk=cycle_id, project_id=project_id, workspace__slug=slug
|
||||
)
|
||||
if cycle.end_date >= timezone.now():
|
||||
if cycle.end_date >= timezone.now().date():
|
||||
return Response(
|
||||
{"error": "Only completed cycles can be archived"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
@@ -629,10 +619,7 @@ class CycleIssueAPIEndpoint(BaseAPIView):
|
||||
# List
|
||||
order_by = request.GET.get("order_by", "created_at")
|
||||
issues = (
|
||||
Issue.issue_objects.filter(
|
||||
issue_cycle__cycle_id=cycle_id,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
)
|
||||
Issue.issue_objects.filter(issue_cycle__cycle_id=cycle_id)
|
||||
.annotate(
|
||||
sub_issues_count=Issue.issue_objects.filter(
|
||||
parent=OuterRef("id")
|
||||
@@ -658,9 +645,8 @@ class CycleIssueAPIEndpoint(BaseAPIView):
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
attachment_count=FileAsset.objects.filter(
|
||||
issue_id=OuterRef("id"),
|
||||
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
|
||||
attachment_count=IssueAttachment.objects.filter(
|
||||
issue=OuterRef("id")
|
||||
)
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
@@ -829,7 +815,6 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
|
||||
filter=Q(
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -840,7 +825,6 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
|
||||
issue_cycle__issue__state__group="completed",
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -851,7 +835,6 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
|
||||
issue_cycle__issue__state__group="cancelled",
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -862,7 +845,6 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
|
||||
issue_cycle__issue__state__group="started",
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -873,7 +855,6 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
|
||||
issue_cycle__issue__state__group="unstarted",
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -884,7 +865,6 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
|
||||
issue_cycle__issue__state__group="backlog",
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -901,34 +881,13 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
|
||||
assignee_estimate_data = (
|
||||
Issue.issue_objects.filter(
|
||||
issue_cycle__cycle_id=cycle_id,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
)
|
||||
.annotate(display_name=F("assignees__display_name"))
|
||||
.annotate(assignee_id=F("assignees__id"))
|
||||
.annotate(avatar=F("assignees__avatar"))
|
||||
.annotate(
|
||||
avatar_url=Case(
|
||||
# If `avatar_asset` exists, use it to generate the asset URL
|
||||
When(
|
||||
assignees__avatar_asset__isnull=False,
|
||||
then=Concat(
|
||||
Value("/api/assets/v2/static/"),
|
||||
"assignees__avatar_asset", # Assuming avatar_asset has an id or relevant field
|
||||
Value("/"),
|
||||
),
|
||||
),
|
||||
# If `avatar_asset` is None, fall back to using `avatar` field directly
|
||||
When(
|
||||
assignees__avatar_asset__isnull=True,
|
||||
then="assignees__avatar",
|
||||
),
|
||||
default=Value(None),
|
||||
output_field=models.CharField(),
|
||||
)
|
||||
)
|
||||
.values("display_name", "assignee_id", "avatar", "avatar_url")
|
||||
.values("display_name", "assignee_id", "avatar")
|
||||
.annotate(
|
||||
total_estimates=Sum(
|
||||
Cast("estimate_point__value", FloatField())
|
||||
@@ -965,8 +924,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
|
||||
if item["assignee_id"]
|
||||
else None
|
||||
),
|
||||
"avatar": item.get("avatar", None),
|
||||
"avatar_url": item.get("avatar_url", None),
|
||||
"avatar": item["avatar"],
|
||||
"total_estimates": item["total_estimates"],
|
||||
"completed_estimates": item["completed_estimates"],
|
||||
"pending_estimates": item["pending_estimates"],
|
||||
@@ -977,7 +935,6 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
|
||||
label_distribution_data = (
|
||||
Issue.issue_objects.filter(
|
||||
issue_cycle__cycle_id=cycle_id,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
)
|
||||
@@ -1039,34 +996,13 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
|
||||
assignee_distribution = (
|
||||
Issue.issue_objects.filter(
|
||||
issue_cycle__cycle_id=cycle_id,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
)
|
||||
.annotate(display_name=F("assignees__display_name"))
|
||||
.annotate(assignee_id=F("assignees__id"))
|
||||
.annotate(avatar=F("assignees__avatar"))
|
||||
.annotate(
|
||||
avatar_url=Case(
|
||||
# If `avatar_asset` exists, use it to generate the asset URL
|
||||
When(
|
||||
assignees__avatar_asset__isnull=False,
|
||||
then=Concat(
|
||||
Value("/api/assets/v2/static/"),
|
||||
"assignees__avatar_asset", # Assuming avatar_asset has an id or relevant field
|
||||
Value("/"),
|
||||
),
|
||||
),
|
||||
# If `avatar_asset` is None, fall back to using `avatar` field directly
|
||||
When(
|
||||
assignees__avatar_asset__isnull=True,
|
||||
then="assignees__avatar",
|
||||
),
|
||||
default=Value(None),
|
||||
output_field=models.CharField(),
|
||||
)
|
||||
)
|
||||
.values("display_name", "assignee_id", "avatar_url")
|
||||
.values("display_name", "assignee_id", "avatar")
|
||||
.annotate(
|
||||
total_issues=Count(
|
||||
"id",
|
||||
@@ -1105,8 +1041,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
|
||||
"assignee_id": (
|
||||
str(item["assignee_id"]) if item["assignee_id"] else None
|
||||
),
|
||||
"avatar": item.get("avatar", None),
|
||||
"avatar_url": item.get("avatar_url", None),
|
||||
"avatar": item["avatar"],
|
||||
"total_issues": item["total_issues"],
|
||||
"completed_issues": item["completed_issues"],
|
||||
"pending_issues": item["pending_issues"],
|
||||
@@ -1118,7 +1053,6 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
|
||||
label_distribution = (
|
||||
Issue.issue_objects.filter(
|
||||
issue_cycle__cycle_id=cycle_id,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
)
|
||||
@@ -1212,7 +1146,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
|
||||
|
||||
if (
|
||||
new_cycle.end_date is not None
|
||||
and new_cycle.end_date < timezone.now()
|
||||
and new_cycle.end_date < timezone.now().date()
|
||||
):
|
||||
return Response(
|
||||
{
|
||||
|
||||
@@ -14,12 +14,12 @@ from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
|
||||
# Module imports
|
||||
from plane.api.serializers import IntakeIssueSerializer, IssueSerializer
|
||||
from plane.api.serializers import InboxIssueSerializer, IssueSerializer
|
||||
from plane.app.permissions import ProjectLitePermission
|
||||
from plane.bgtasks.issue_activities_task import issue_activity
|
||||
from plane.db.models import (
|
||||
Intake,
|
||||
IntakeIssue,
|
||||
Inbox,
|
||||
InboxIssue,
|
||||
Issue,
|
||||
Project,
|
||||
ProjectMember,
|
||||
@@ -29,10 +29,10 @@ from plane.db.models import (
|
||||
from .base import BaseAPIView
|
||||
|
||||
|
||||
class IntakeIssueAPIEndpoint(BaseAPIView):
|
||||
class InboxIssueAPIEndpoint(BaseAPIView):
|
||||
"""
|
||||
This viewset automatically provides `list`, `create`, `retrieve`,
|
||||
`update` and `destroy` actions related to intake issues.
|
||||
`update` and `destroy` actions related to inbox issues.
|
||||
|
||||
"""
|
||||
|
||||
@@ -40,15 +40,15 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
|
||||
ProjectLitePermission,
|
||||
]
|
||||
|
||||
serializer_class = IntakeIssueSerializer
|
||||
model = IntakeIssue
|
||||
serializer_class = InboxIssueSerializer
|
||||
model = InboxIssue
|
||||
|
||||
filterset_fields = [
|
||||
"status",
|
||||
]
|
||||
|
||||
def get_queryset(self):
|
||||
intake = Intake.objects.filter(
|
||||
inbox = Inbox.objects.filter(
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
).first()
|
||||
@@ -58,16 +58,16 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
|
||||
pk=self.kwargs.get("project_id"),
|
||||
)
|
||||
|
||||
if intake is None and not project.intake_view:
|
||||
return IntakeIssue.objects.none()
|
||||
if inbox is None and not project.inbox_view:
|
||||
return InboxIssue.objects.none()
|
||||
|
||||
return (
|
||||
IntakeIssue.objects.filter(
|
||||
InboxIssue.objects.filter(
|
||||
Q(snoozed_till__gte=timezone.now())
|
||||
| Q(snoozed_till__isnull=True),
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
intake_id=intake.id,
|
||||
inbox_id=inbox.id,
|
||||
)
|
||||
.select_related("issue", "workspace", "project")
|
||||
.order_by(self.kwargs.get("order_by", "-created_at"))
|
||||
@@ -75,22 +75,22 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
|
||||
|
||||
def get(self, request, slug, project_id, issue_id=None):
|
||||
if issue_id:
|
||||
intake_issue_queryset = self.get_queryset().get(issue_id=issue_id)
|
||||
intake_issue_data = IntakeIssueSerializer(
|
||||
intake_issue_queryset,
|
||||
inbox_issue_queryset = self.get_queryset().get(issue_id=issue_id)
|
||||
inbox_issue_data = InboxIssueSerializer(
|
||||
inbox_issue_queryset,
|
||||
fields=self.fields,
|
||||
expand=self.expand,
|
||||
).data
|
||||
return Response(
|
||||
intake_issue_data,
|
||||
inbox_issue_data,
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
issue_queryset = self.get_queryset()
|
||||
return self.paginate(
|
||||
request=request,
|
||||
queryset=(issue_queryset),
|
||||
on_results=lambda intake_issues: IntakeIssueSerializer(
|
||||
intake_issues,
|
||||
on_results=lambda inbox_issues: InboxIssueSerializer(
|
||||
inbox_issues,
|
||||
many=True,
|
||||
fields=self.fields,
|
||||
expand=self.expand,
|
||||
@@ -104,7 +104,7 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
intake = Intake.objects.filter(
|
||||
inbox = Inbox.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
).first()
|
||||
|
||||
@@ -113,11 +113,11 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
|
||||
pk=project_id,
|
||||
)
|
||||
|
||||
# Intake view
|
||||
if intake is None and not project.intake_view:
|
||||
# Inbox view
|
||||
if inbox is None and not project.inbox_view:
|
||||
return Response(
|
||||
{
|
||||
"error": "Intake is not enabled for this project enable it through the project's api"
|
||||
"error": "Inbox is not enabled for this project enable it through the project's api"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
@@ -139,7 +139,7 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
|
||||
state, _ = State.objects.get_or_create(
|
||||
name="Triage",
|
||||
group="triage",
|
||||
description="Default state for managing all Intake Issues",
|
||||
description="Default state for managing all Inbox Issues",
|
||||
project_id=project_id,
|
||||
color="#ff7700",
|
||||
is_triage=True,
|
||||
@@ -157,12 +157,12 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
|
||||
state=state,
|
||||
)
|
||||
|
||||
# create an intake issue
|
||||
intake_issue = IntakeIssue.objects.create(
|
||||
intake_id=intake.id,
|
||||
# create an inbox issue
|
||||
inbox_issue = InboxIssue.objects.create(
|
||||
inbox_id=inbox.id,
|
||||
project_id=project_id,
|
||||
issue=issue,
|
||||
source=request.data.get("source", "IN-APP"),
|
||||
source=request.data.get("source", "in-app"),
|
||||
)
|
||||
# Create an Issue Activity
|
||||
issue_activity.delay(
|
||||
@@ -173,37 +173,32 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
|
||||
project_id=str(project_id),
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
intake=str(intake_issue.id),
|
||||
inbox=str(inbox_issue.id),
|
||||
)
|
||||
|
||||
serializer = IntakeIssueSerializer(intake_issue)
|
||||
serializer = InboxIssueSerializer(inbox_issue)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
def patch(self, request, slug, project_id, issue_id):
|
||||
intake = Intake.objects.filter(
|
||||
inbox = Inbox.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
).first()
|
||||
|
||||
project = Project.objects.get(
|
||||
workspace__slug=slug,
|
||||
pk=project_id,
|
||||
)
|
||||
|
||||
# Intake view
|
||||
if intake is None and not project.intake_view:
|
||||
# Inbox view
|
||||
if inbox is None:
|
||||
return Response(
|
||||
{
|
||||
"error": "Intake is not enabled for this project enable it through the project's api"
|
||||
"error": "Inbox is not enabled for this project enable it through the project's api"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Get the intake issue
|
||||
intake_issue = IntakeIssue.objects.get(
|
||||
# Get the inbox issue
|
||||
inbox_issue = InboxIssue.objects.get(
|
||||
issue_id=issue_id,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
intake_id=intake.id,
|
||||
inbox_id=inbox.id,
|
||||
)
|
||||
|
||||
# Get the project member
|
||||
@@ -215,11 +210,11 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
|
||||
)
|
||||
|
||||
# Only project members admins and created_by users can access this endpoint
|
||||
if project_member.role <= 5 and str(intake_issue.created_by_id) != str(
|
||||
if project_member.role <= 10 and str(inbox_issue.created_by_id) != str(
|
||||
request.user.id
|
||||
):
|
||||
return Response(
|
||||
{"error": "You cannot edit intake issues"},
|
||||
{"error": "You cannot edit inbox issues"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@@ -232,10 +227,7 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
|
||||
ArrayAgg(
|
||||
"labels__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(labels__id__isnull=True)
|
||||
& Q(label_issue__deleted_at__isnull=True),
|
||||
),
|
||||
filter=~Q(labels__id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
@@ -243,11 +235,7 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
|
||||
ArrayAgg(
|
||||
"assignees__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True)
|
||||
& Q(issue_assignee__deleted_at__isnull=True)
|
||||
),
|
||||
filter=~Q(assignees__id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
@@ -256,8 +244,9 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
)
|
||||
# Only allow guests to edit name and description
|
||||
if project_member.role <= 5:
|
||||
# Only allow guests and viewers to edit name and description
|
||||
if project_member.role <= 10:
|
||||
# viewers and guests since only viewers and guests
|
||||
issue_data = {
|
||||
"name": issue_data.get("name", issue.name),
|
||||
"description_html": issue_data.get(
|
||||
@@ -288,7 +277,7 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
|
||||
cls=DjangoJSONEncoder,
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
intake=(intake_issue.id),
|
||||
inbox=(inbox_issue.id),
|
||||
)
|
||||
issue_serializer.save()
|
||||
else:
|
||||
@@ -296,13 +285,13 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
|
||||
issue_serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
# Only project admins and members can edit intake issue attributes
|
||||
if project_member.role > 15:
|
||||
serializer = IntakeIssueSerializer(
|
||||
intake_issue, data=request.data, partial=True
|
||||
# Only project admins and members can edit inbox issue attributes
|
||||
if project_member.role > 10:
|
||||
serializer = InboxIssueSerializer(
|
||||
inbox_issue, data=request.data, partial=True
|
||||
)
|
||||
current_instance = json.dumps(
|
||||
IntakeIssueSerializer(intake_issue).data, cls=DjangoJSONEncoder
|
||||
InboxIssueSerializer(inbox_issue).data, cls=DjangoJSONEncoder
|
||||
)
|
||||
|
||||
if serializer.is_valid():
|
||||
@@ -345,7 +334,7 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
|
||||
|
||||
# create a activity for status change
|
||||
issue_activity.delay(
|
||||
type="intake.activity.created",
|
||||
type="inbox.activity.created",
|
||||
requested_data=json.dumps(
|
||||
request.data, cls=DjangoJSONEncoder
|
||||
),
|
||||
@@ -356,7 +345,7 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=False,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
intake=str(intake_issue.id),
|
||||
inbox=str(inbox_issue.id),
|
||||
)
|
||||
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
@@ -365,12 +354,12 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
|
||||
)
|
||||
else:
|
||||
return Response(
|
||||
IntakeIssueSerializer(intake_issue).data,
|
||||
InboxIssueSerializer(inbox_issue).data,
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
def delete(self, request, slug, project_id, issue_id):
|
||||
intake = Intake.objects.filter(
|
||||
inbox = Inbox.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
).first()
|
||||
|
||||
@@ -379,25 +368,25 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
|
||||
pk=project_id,
|
||||
)
|
||||
|
||||
# Intake view
|
||||
if intake is None and not project.intake_view:
|
||||
# Inbox view
|
||||
if inbox is None and not project.inbox_view:
|
||||
return Response(
|
||||
{
|
||||
"error": "Intake is not enabled for this project enable it through the project's api"
|
||||
"error": "Inbox is not enabled for this project enable it through the project's api"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Get the intake issue
|
||||
intake_issue = IntakeIssue.objects.get(
|
||||
# Get the inbox issue
|
||||
inbox_issue = InboxIssue.objects.get(
|
||||
issue_id=issue_id,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
intake_id=intake.id,
|
||||
inbox_id=inbox.id,
|
||||
)
|
||||
|
||||
# Check the issue status
|
||||
if intake_issue.status in [-2, -1, 0, 2]:
|
||||
if inbox_issue.status in [-2, -1, 0, 2]:
|
||||
# Delete the issue also
|
||||
issue = Issue.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id, pk=issue_id
|
||||
@@ -417,5 +406,5 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
|
||||
)
|
||||
issue.delete()
|
||||
|
||||
intake_issue.delete()
|
||||
inbox_issue.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
@@ -16,7 +16,6 @@ from django.db.models import (
|
||||
Q,
|
||||
Value,
|
||||
When,
|
||||
Subquery,
|
||||
)
|
||||
from django.utils import timezone
|
||||
|
||||
@@ -43,13 +42,12 @@ from plane.bgtasks.issue_activities_task import issue_activity
|
||||
from plane.db.models import (
|
||||
Issue,
|
||||
IssueActivity,
|
||||
FileAsset,
|
||||
IssueAttachment,
|
||||
IssueComment,
|
||||
IssueLink,
|
||||
Label,
|
||||
Project,
|
||||
ProjectMember,
|
||||
CycleIssue,
|
||||
)
|
||||
|
||||
from .base import BaseAPIView
|
||||
@@ -204,13 +202,7 @@ class IssueAPIEndpoint(BaseAPIView):
|
||||
|
||||
issue_queryset = (
|
||||
self.get_queryset()
|
||||
.annotate(
|
||||
cycle_id=Subquery(
|
||||
CycleIssue.objects.filter(
|
||||
issue=OuterRef("id"), deleted_at__isnull=True
|
||||
).values("cycle_id")[:1]
|
||||
)
|
||||
)
|
||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||
.annotate(
|
||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||
.order_by()
|
||||
@@ -218,9 +210,8 @@ class IssueAPIEndpoint(BaseAPIView):
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
attachment_count=FileAsset.objects.filter(
|
||||
issue_id=OuterRef("id"),
|
||||
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
|
||||
attachment_count=IssueAttachment.objects.filter(
|
||||
issue=OuterRef("id")
|
||||
)
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
@@ -1071,7 +1062,7 @@ class IssueAttachmentEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
model = FileAsset
|
||||
model = IssueAttachment
|
||||
parser_classes = (MultiPartParser, FormParser)
|
||||
|
||||
def post(self, request, slug, project_id, issue_id):
|
||||
@@ -1079,7 +1070,7 @@ class IssueAttachmentEndpoint(BaseAPIView):
|
||||
if (
|
||||
request.data.get("external_id")
|
||||
and request.data.get("external_source")
|
||||
and FileAsset.objects.filter(
|
||||
and IssueAttachment.objects.filter(
|
||||
project_id=project_id,
|
||||
workspace__slug=slug,
|
||||
issue_id=issue_id,
|
||||
@@ -1087,7 +1078,7 @@ class IssueAttachmentEndpoint(BaseAPIView):
|
||||
external_id=request.data.get("external_id"),
|
||||
).exists()
|
||||
):
|
||||
issue_attachment = FileAsset.objects.filter(
|
||||
issue_attachment = IssueAttachment.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
external_id=request.data.get("external_id"),
|
||||
@@ -1121,7 +1112,7 @@ class IssueAttachmentEndpoint(BaseAPIView):
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def delete(self, request, slug, project_id, issue_id, pk):
|
||||
issue_attachment = FileAsset.objects.get(pk=pk)
|
||||
issue_attachment = IssueAttachment.objects.get(pk=pk)
|
||||
issue_attachment.asset.delete(save=False)
|
||||
issue_attachment.delete()
|
||||
issue_activity.delay(
|
||||
@@ -1139,7 +1130,7 @@ class IssueAttachmentEndpoint(BaseAPIView):
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
def get(self, request, slug, project_id, issue_id):
|
||||
issue_attachments = FileAsset.objects.filter(
|
||||
issue_attachments = IssueAttachment.objects.filter(
|
||||
issue_id=issue_id, workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
serializer = IssueAttachmentSerializer(issue_attachments, many=True)
|
||||
|
||||
@@ -133,7 +133,7 @@ class ProjectMemberAPIEndpoint(BaseAPIView):
|
||||
workspace_member = WorkspaceMember.objects.create(
|
||||
workspace=workspace,
|
||||
member=user,
|
||||
role=request.data.get("role", 5),
|
||||
role=request.data.get("role", 10),
|
||||
)
|
||||
workspace_member.save()
|
||||
|
||||
@@ -142,7 +142,7 @@ class ProjectMemberAPIEndpoint(BaseAPIView):
|
||||
project_member = ProjectMember.objects.create(
|
||||
project=project,
|
||||
member=user,
|
||||
role=request.data.get("role", 5),
|
||||
role=request.data.get("role", 10),
|
||||
)
|
||||
project_member.save()
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ from plane.app.permissions import ProjectEntityPermission
|
||||
from plane.bgtasks.issue_activities_task import issue_activity
|
||||
from plane.db.models import (
|
||||
Issue,
|
||||
FileAsset,
|
||||
IssueAttachment,
|
||||
IssueLink,
|
||||
Module,
|
||||
ModuleIssue,
|
||||
@@ -71,7 +71,6 @@ class ModuleAPIEndpoint(BaseAPIView):
|
||||
filter=Q(
|
||||
issue_module__issue__archived_at__isnull=True,
|
||||
issue_module__issue__is_draft=False,
|
||||
issue_module__deleted_at__isnull=True,
|
||||
),
|
||||
distinct=True,
|
||||
),
|
||||
@@ -83,7 +82,6 @@ class ModuleAPIEndpoint(BaseAPIView):
|
||||
issue_module__issue__state__group="completed",
|
||||
issue_module__issue__archived_at__isnull=True,
|
||||
issue_module__issue__is_draft=False,
|
||||
issue_module__deleted_at__isnull=True,
|
||||
),
|
||||
distinct=True,
|
||||
)
|
||||
@@ -95,7 +93,6 @@ class ModuleAPIEndpoint(BaseAPIView):
|
||||
issue_module__issue__state__group="cancelled",
|
||||
issue_module__issue__archived_at__isnull=True,
|
||||
issue_module__issue__is_draft=False,
|
||||
issue_module__deleted_at__isnull=True,
|
||||
),
|
||||
distinct=True,
|
||||
)
|
||||
@@ -107,7 +104,6 @@ class ModuleAPIEndpoint(BaseAPIView):
|
||||
issue_module__issue__state__group="started",
|
||||
issue_module__issue__archived_at__isnull=True,
|
||||
issue_module__issue__is_draft=False,
|
||||
issue_module__deleted_at__isnull=True,
|
||||
),
|
||||
distinct=True,
|
||||
)
|
||||
@@ -119,7 +115,6 @@ class ModuleAPIEndpoint(BaseAPIView):
|
||||
issue_module__issue__state__group="unstarted",
|
||||
issue_module__issue__archived_at__isnull=True,
|
||||
issue_module__issue__is_draft=False,
|
||||
issue_module__deleted_at__isnull=True,
|
||||
),
|
||||
distinct=True,
|
||||
)
|
||||
@@ -131,7 +126,6 @@ class ModuleAPIEndpoint(BaseAPIView):
|
||||
issue_module__issue__state__group="backlog",
|
||||
issue_module__issue__archived_at__isnull=True,
|
||||
issue_module__issue__is_draft=False,
|
||||
issue_module__deleted_at__isnull=True,
|
||||
),
|
||||
distinct=True,
|
||||
)
|
||||
@@ -373,10 +367,7 @@ class ModuleIssueAPIEndpoint(BaseAPIView):
|
||||
def get(self, request, slug, project_id, module_id):
|
||||
order_by = request.GET.get("order_by", "created_at")
|
||||
issues = (
|
||||
Issue.issue_objects.filter(
|
||||
issue_module__module_id=module_id,
|
||||
issue_module__deleted_at__isnull=True,
|
||||
)
|
||||
Issue.issue_objects.filter(issue_module__module_id=module_id)
|
||||
.annotate(
|
||||
sub_issues_count=Issue.issue_objects.filter(
|
||||
parent=OuterRef("id")
|
||||
@@ -402,9 +393,8 @@ class ModuleIssueAPIEndpoint(BaseAPIView):
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
attachment_count=FileAsset.objects.filter(
|
||||
issue_id=OuterRef("id"),
|
||||
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
|
||||
attachment_count=IssueAttachment.objects.filter(
|
||||
issue=OuterRef("id")
|
||||
)
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
@@ -561,7 +551,6 @@ class ModuleArchiveUnarchiveAPIEndpoint(BaseAPIView):
|
||||
filter=Q(
|
||||
issue_module__issue__archived_at__isnull=True,
|
||||
issue_module__issue__is_draft=False,
|
||||
issue_module__deleted_at__isnull=True,
|
||||
),
|
||||
distinct=True,
|
||||
),
|
||||
@@ -573,7 +562,6 @@ class ModuleArchiveUnarchiveAPIEndpoint(BaseAPIView):
|
||||
issue_module__issue__state__group="completed",
|
||||
issue_module__issue__archived_at__isnull=True,
|
||||
issue_module__issue__is_draft=False,
|
||||
issue_module__deleted_at__isnull=True,
|
||||
),
|
||||
distinct=True,
|
||||
)
|
||||
@@ -585,7 +573,6 @@ class ModuleArchiveUnarchiveAPIEndpoint(BaseAPIView):
|
||||
issue_module__issue__state__group="cancelled",
|
||||
issue_module__issue__archived_at__isnull=True,
|
||||
issue_module__issue__is_draft=False,
|
||||
issue_module__deleted_at__isnull=True,
|
||||
),
|
||||
distinct=True,
|
||||
)
|
||||
@@ -597,7 +584,6 @@ class ModuleArchiveUnarchiveAPIEndpoint(BaseAPIView):
|
||||
issue_module__issue__state__group="started",
|
||||
issue_module__issue__archived_at__isnull=True,
|
||||
issue_module__issue__is_draft=False,
|
||||
issue_module__deleted_at__isnull=True,
|
||||
),
|
||||
distinct=True,
|
||||
)
|
||||
@@ -609,7 +595,6 @@ class ModuleArchiveUnarchiveAPIEndpoint(BaseAPIView):
|
||||
issue_module__issue__state__group="unstarted",
|
||||
issue_module__issue__archived_at__isnull=True,
|
||||
issue_module__issue__is_draft=False,
|
||||
issue_module__deleted_at__isnull=True,
|
||||
),
|
||||
distinct=True,
|
||||
)
|
||||
@@ -621,7 +606,6 @@ class ModuleArchiveUnarchiveAPIEndpoint(BaseAPIView):
|
||||
issue_module__issue__state__group="backlog",
|
||||
issue_module__issue__archived_at__isnull=True,
|
||||
issue_module__issue__is_draft=False,
|
||||
issue_module__deleted_at__isnull=True,
|
||||
),
|
||||
distinct=True,
|
||||
)
|
||||
|
||||
@@ -18,7 +18,7 @@ from plane.app.permissions import ProjectBasePermission
|
||||
# Module imports
|
||||
from plane.db.models import (
|
||||
Cycle,
|
||||
Intake,
|
||||
Inbox,
|
||||
IssueUserProperty,
|
||||
Module,
|
||||
Project,
|
||||
@@ -285,11 +285,6 @@ class ProjectAPIEndpoint(BaseAPIView):
|
||||
current_instance = json.dumps(
|
||||
ProjectSerializer(project).data, cls=DjangoJSONEncoder
|
||||
)
|
||||
|
||||
intake_view = request.data.get(
|
||||
"inbox_view", request.data.get("intake_view", False)
|
||||
)
|
||||
|
||||
if project.archived_at:
|
||||
return Response(
|
||||
{"error": "Archived project cannot be updated"},
|
||||
@@ -298,33 +293,25 @@ class ProjectAPIEndpoint(BaseAPIView):
|
||||
|
||||
serializer = ProjectSerializer(
|
||||
project,
|
||||
data={
|
||||
**request.data,
|
||||
"intake_view": intake_view,
|
||||
},
|
||||
data={**request.data},
|
||||
context={"workspace_id": workspace.id},
|
||||
partial=True,
|
||||
)
|
||||
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
if serializer.data["intake_view"]:
|
||||
intake = Intake.objects.filter(
|
||||
if serializer.data["inbox_view"]:
|
||||
Inbox.objects.get_or_create(
|
||||
name=f"{project.name} Inbox",
|
||||
project=project,
|
||||
is_default=True,
|
||||
).first()
|
||||
if not intake:
|
||||
Intake.objects.create(
|
||||
name=f"{project.name} Intake",
|
||||
project=project,
|
||||
is_default=True,
|
||||
)
|
||||
)
|
||||
|
||||
# Create the triage state in Backlog group
|
||||
State.objects.get_or_create(
|
||||
name="Triage",
|
||||
group="triage",
|
||||
description="Default state for managing all Intake Issues",
|
||||
description="Default state for managing all Inbox Issues",
|
||||
project_id=pk,
|
||||
color="#ff7700",
|
||||
is_triage=True,
|
||||
|
||||
@@ -8,6 +8,7 @@ from enum import Enum
|
||||
class ROLE(Enum):
|
||||
ADMIN = 20
|
||||
MEMBER = 15
|
||||
VIEWER = 10
|
||||
GUEST = 5
|
||||
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ from plane.db.models import ProjectMember, WorkspaceMember
|
||||
# Permission Mappings
|
||||
Admin = 20
|
||||
Member = 15
|
||||
Viewer = 10
|
||||
Guest = 5
|
||||
|
||||
|
||||
|
||||
@@ -6,8 +6,9 @@ from plane.db.models import WorkspaceMember
|
||||
|
||||
|
||||
# Permission Mappings
|
||||
Admin = 20
|
||||
Member = 15
|
||||
Owner = 20
|
||||
Admin = 15
|
||||
Member = 10
|
||||
Guest = 5
|
||||
|
||||
|
||||
@@ -30,7 +31,7 @@ class WorkSpaceBasePermission(BasePermission):
|
||||
return WorkspaceMember.objects.filter(
|
||||
member=request.user,
|
||||
workspace__slug=view.workspace_slug,
|
||||
role__in=[Admin, Member],
|
||||
role__in=[Owner, Admin],
|
||||
is_active=True,
|
||||
).exists()
|
||||
|
||||
@@ -39,7 +40,7 @@ class WorkSpaceBasePermission(BasePermission):
|
||||
return WorkspaceMember.objects.filter(
|
||||
member=request.user,
|
||||
workspace__slug=view.workspace_slug,
|
||||
role=Admin,
|
||||
role=Owner,
|
||||
is_active=True,
|
||||
).exists()
|
||||
|
||||
@@ -52,7 +53,7 @@ class WorkspaceOwnerPermission(BasePermission):
|
||||
return WorkspaceMember.objects.filter(
|
||||
workspace__slug=view.workspace_slug,
|
||||
member=request.user,
|
||||
role=Admin,
|
||||
role=Owner,
|
||||
).exists()
|
||||
|
||||
|
||||
@@ -64,7 +65,7 @@ class WorkSpaceAdminPermission(BasePermission):
|
||||
return WorkspaceMember.objects.filter(
|
||||
member=request.user,
|
||||
workspace__slug=view.workspace_slug,
|
||||
role__in=[Admin, Member],
|
||||
role__in=[Owner, Admin],
|
||||
is_active=True,
|
||||
).exists()
|
||||
|
||||
@@ -85,7 +86,7 @@ class WorkspaceEntityPermission(BasePermission):
|
||||
return WorkspaceMember.objects.filter(
|
||||
member=request.user,
|
||||
workspace__slug=view.workspace_slug,
|
||||
role__in=[Admin, Member],
|
||||
role__in=[Owner, Admin],
|
||||
is_active=True,
|
||||
).exists()
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ from .issue import (
|
||||
IssueFlatSerializer,
|
||||
IssueStateSerializer,
|
||||
IssueLinkSerializer,
|
||||
IssueIntakeSerializer,
|
||||
IssueInboxSerializer,
|
||||
IssueLiteSerializer,
|
||||
IssueAttachmentSerializer,
|
||||
IssueSubscriberSerializer,
|
||||
@@ -102,12 +102,12 @@ from .estimate import (
|
||||
WorkspaceEstimateSerializer,
|
||||
)
|
||||
|
||||
from .intake import (
|
||||
IntakeSerializer,
|
||||
IntakeIssueSerializer,
|
||||
IssueStateIntakeSerializer,
|
||||
IntakeIssueLiteSerializer,
|
||||
IntakeIssueDetailSerializer,
|
||||
from .inbox import (
|
||||
InboxSerializer,
|
||||
InboxIssueSerializer,
|
||||
IssueStateInboxSerializer,
|
||||
InboxIssueLiteSerializer,
|
||||
InboxIssueDetailSerializer,
|
||||
)
|
||||
|
||||
from .analytic import AnalyticViewSerializer
|
||||
@@ -124,9 +124,3 @@ from .webhook import WebhookSerializer, WebhookLogSerializer
|
||||
from .dashboard import DashboardSerializer, WidgetSerializer
|
||||
|
||||
from .favorite import UserFavoriteSerializer
|
||||
|
||||
from .draft import (
|
||||
DraftIssueCreateSerializer,
|
||||
DraftIssueSerializer,
|
||||
DraftIssueDetailSerializer,
|
||||
)
|
||||
|
||||
@@ -60,10 +60,10 @@ class DynamicBaseSerializer(BaseSerializer):
|
||||
CycleIssueSerializer,
|
||||
IssueLiteSerializer,
|
||||
IssueRelationSerializer,
|
||||
IntakeIssueLiteSerializer,
|
||||
InboxIssueLiteSerializer,
|
||||
IssueReactionLiteSerializer,
|
||||
IssueAttachmentLiteSerializer,
|
||||
IssueLinkLiteSerializer,
|
||||
RelatedIssueSerializer,
|
||||
)
|
||||
|
||||
# Expansion mapper
|
||||
@@ -84,14 +84,13 @@ class DynamicBaseSerializer(BaseSerializer):
|
||||
"issue_cycle": CycleIssueSerializer,
|
||||
"parent": IssueLiteSerializer,
|
||||
"issue_relation": IssueRelationSerializer,
|
||||
"issue_intake": IntakeIssueLiteSerializer,
|
||||
"issue_related": RelatedIssueSerializer,
|
||||
"issue_inbox": InboxIssueLiteSerializer,
|
||||
"issue_reactions": IssueReactionLiteSerializer,
|
||||
"issue_attachment": IssueAttachmentLiteSerializer,
|
||||
"issue_link": IssueLinkLiteSerializer,
|
||||
"sub_issues": IssueLiteSerializer,
|
||||
}
|
||||
|
||||
if field not in self.fields and field in expansion:
|
||||
self.fields[field] = expansion[field](
|
||||
many=(
|
||||
True
|
||||
@@ -102,12 +101,11 @@ class DynamicBaseSerializer(BaseSerializer):
|
||||
"labels",
|
||||
"issue_cycle",
|
||||
"issue_relation",
|
||||
"issue_intake",
|
||||
"issue_inbox",
|
||||
"issue_reactions",
|
||||
"issue_attachment",
|
||||
"issue_link",
|
||||
"sub_issues",
|
||||
"issue_related",
|
||||
]
|
||||
else False
|
||||
)
|
||||
@@ -132,12 +130,11 @@ class DynamicBaseSerializer(BaseSerializer):
|
||||
LabelSerializer,
|
||||
CycleIssueSerializer,
|
||||
IssueRelationSerializer,
|
||||
IntakeIssueLiteSerializer,
|
||||
InboxIssueLiteSerializer,
|
||||
IssueLiteSerializer,
|
||||
IssueReactionLiteSerializer,
|
||||
IssueAttachmentLiteSerializer,
|
||||
IssueLinkLiteSerializer,
|
||||
RelatedIssueSerializer,
|
||||
)
|
||||
|
||||
# Expansion mapper
|
||||
@@ -158,8 +155,7 @@ class DynamicBaseSerializer(BaseSerializer):
|
||||
"issue_cycle": CycleIssueSerializer,
|
||||
"parent": IssueLiteSerializer,
|
||||
"issue_relation": IssueRelationSerializer,
|
||||
"issue_intake": IntakeIssueLiteSerializer,
|
||||
"issue_related": RelatedIssueSerializer,
|
||||
"issue_inbox": InboxIssueLiteSerializer,
|
||||
"issue_reactions": IssueReactionLiteSerializer,
|
||||
"issue_attachment": IssueAttachmentLiteSerializer,
|
||||
"issue_link": IssueLinkLiteSerializer,
|
||||
@@ -182,29 +178,4 @@ class DynamicBaseSerializer(BaseSerializer):
|
||||
instance, f"{expand}_id", None
|
||||
)
|
||||
|
||||
# Check if issue_attachments is in fields or expand
|
||||
if (
|
||||
"issue_attachments" in self.fields
|
||||
or "issue_attachments" in self.expand
|
||||
):
|
||||
# Import the model here to avoid circular imports
|
||||
from plane.db.models import FileAsset
|
||||
|
||||
issue_id = getattr(instance, "id", None)
|
||||
|
||||
if issue_id:
|
||||
# Fetch related issue_attachments
|
||||
issue_attachments = FileAsset.objects.filter(
|
||||
issue_id=issue_id,
|
||||
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
|
||||
)
|
||||
# Serialize issue_attachments and add them to the response
|
||||
response["issue_attachments"] = (
|
||||
IssueAttachmentLiteSerializer(
|
||||
issue_attachments, many=True
|
||||
).data
|
||||
)
|
||||
else:
|
||||
response["issue_attachments"] = []
|
||||
|
||||
return response
|
||||
|
||||
@@ -1,292 +0,0 @@
|
||||
# Django imports
|
||||
from django.utils import timezone
|
||||
|
||||
# Third Party imports
|
||||
from rest_framework import serializers
|
||||
|
||||
# Module imports
|
||||
from .base import BaseSerializer
|
||||
from plane.db.models import (
|
||||
User,
|
||||
Issue,
|
||||
Label,
|
||||
State,
|
||||
DraftIssue,
|
||||
DraftIssueAssignee,
|
||||
DraftIssueLabel,
|
||||
DraftIssueCycle,
|
||||
DraftIssueModule,
|
||||
)
|
||||
|
||||
|
||||
class DraftIssueCreateSerializer(BaseSerializer):
|
||||
# ids
|
||||
state_id = serializers.PrimaryKeyRelatedField(
|
||||
source="state",
|
||||
queryset=State.objects.all(),
|
||||
required=False,
|
||||
allow_null=True,
|
||||
)
|
||||
parent_id = serializers.PrimaryKeyRelatedField(
|
||||
source="parent",
|
||||
queryset=Issue.objects.all(),
|
||||
required=False,
|
||||
allow_null=True,
|
||||
)
|
||||
label_ids = serializers.ListField(
|
||||
child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()),
|
||||
write_only=True,
|
||||
required=False,
|
||||
)
|
||||
assignee_ids = serializers.ListField(
|
||||
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
|
||||
write_only=True,
|
||||
required=False,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = DraftIssue
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
|
||||
def to_representation(self, instance):
|
||||
data = super().to_representation(instance)
|
||||
assignee_ids = self.initial_data.get("assignee_ids")
|
||||
data["assignee_ids"] = assignee_ids if assignee_ids else []
|
||||
label_ids = self.initial_data.get("label_ids")
|
||||
data["label_ids"] = label_ids if label_ids else []
|
||||
return data
|
||||
|
||||
def validate(self, data):
|
||||
if (
|
||||
data.get("start_date", None) is not None
|
||||
and data.get("target_date", None) is not None
|
||||
and data.get("start_date", None) > data.get("target_date", None)
|
||||
):
|
||||
raise serializers.ValidationError(
|
||||
"Start date cannot exceed target date"
|
||||
)
|
||||
return data
|
||||
|
||||
def create(self, validated_data):
|
||||
assignees = validated_data.pop("assignee_ids", None)
|
||||
labels = validated_data.pop("label_ids", None)
|
||||
modules = validated_data.pop("module_ids", None)
|
||||
cycle_id = self.initial_data.get("cycle_id", None)
|
||||
modules = self.initial_data.get("module_ids", None)
|
||||
|
||||
workspace_id = self.context["workspace_id"]
|
||||
project_id = self.context["project_id"]
|
||||
|
||||
# Create Issue
|
||||
issue = DraftIssue.objects.create(
|
||||
**validated_data,
|
||||
workspace_id=workspace_id,
|
||||
project_id=project_id,
|
||||
)
|
||||
|
||||
# Issue Audit Users
|
||||
created_by_id = issue.created_by_id
|
||||
updated_by_id = issue.updated_by_id
|
||||
|
||||
if assignees is not None and len(assignees):
|
||||
DraftIssueAssignee.objects.bulk_create(
|
||||
[
|
||||
DraftIssueAssignee(
|
||||
assignee=user,
|
||||
draft_issue=issue,
|
||||
workspace_id=workspace_id,
|
||||
project_id=project_id,
|
||||
created_by_id=created_by_id,
|
||||
updated_by_id=updated_by_id,
|
||||
)
|
||||
for user in assignees
|
||||
],
|
||||
batch_size=10,
|
||||
)
|
||||
|
||||
if labels is not None and len(labels):
|
||||
DraftIssueLabel.objects.bulk_create(
|
||||
[
|
||||
DraftIssueLabel(
|
||||
label=label,
|
||||
draft_issue=issue,
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
created_by_id=created_by_id,
|
||||
updated_by_id=updated_by_id,
|
||||
)
|
||||
for label in labels
|
||||
],
|
||||
batch_size=10,
|
||||
)
|
||||
|
||||
if cycle_id is not None:
|
||||
DraftIssueCycle.objects.create(
|
||||
cycle_id=cycle_id,
|
||||
draft_issue=issue,
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
created_by_id=created_by_id,
|
||||
updated_by_id=updated_by_id,
|
||||
)
|
||||
|
||||
if modules is not None and len(modules):
|
||||
DraftIssueModule.objects.bulk_create(
|
||||
[
|
||||
DraftIssueModule(
|
||||
module_id=module_id,
|
||||
draft_issue=issue,
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
created_by_id=created_by_id,
|
||||
updated_by_id=updated_by_id,
|
||||
)
|
||||
for module_id in modules
|
||||
],
|
||||
batch_size=10,
|
||||
)
|
||||
|
||||
return issue
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
assignees = validated_data.pop("assignee_ids", None)
|
||||
labels = validated_data.pop("label_ids", None)
|
||||
cycle_id = self.context.get("cycle_id", None)
|
||||
modules = self.initial_data.get("module_ids", None)
|
||||
|
||||
# Related models
|
||||
workspace_id = instance.workspace_id
|
||||
project_id = instance.project_id
|
||||
|
||||
created_by_id = instance.created_by_id
|
||||
updated_by_id = instance.updated_by_id
|
||||
|
||||
if assignees is not None:
|
||||
DraftIssueAssignee.objects.filter(draft_issue=instance).delete()
|
||||
DraftIssueAssignee.objects.bulk_create(
|
||||
[
|
||||
DraftIssueAssignee(
|
||||
assignee=user,
|
||||
draft_issue=instance,
|
||||
workspace_id=workspace_id,
|
||||
project_id=project_id,
|
||||
created_by_id=created_by_id,
|
||||
updated_by_id=updated_by_id,
|
||||
)
|
||||
for user in assignees
|
||||
],
|
||||
batch_size=10,
|
||||
)
|
||||
|
||||
if labels is not None:
|
||||
DraftIssueLabel.objects.filter(draft_issue=instance).delete()
|
||||
DraftIssueLabel.objects.bulk_create(
|
||||
[
|
||||
DraftIssueLabel(
|
||||
label=label,
|
||||
draft_issue=instance,
|
||||
workspace_id=workspace_id,
|
||||
project_id=project_id,
|
||||
created_by_id=created_by_id,
|
||||
updated_by_id=updated_by_id,
|
||||
)
|
||||
for label in labels
|
||||
],
|
||||
batch_size=10,
|
||||
)
|
||||
|
||||
if cycle_id != "not_provided":
|
||||
DraftIssueCycle.objects.filter(draft_issue=instance).delete()
|
||||
if cycle_id:
|
||||
DraftIssueCycle.objects.create(
|
||||
cycle_id=cycle_id,
|
||||
draft_issue=instance,
|
||||
workspace_id=workspace_id,
|
||||
project_id=project_id,
|
||||
created_by_id=created_by_id,
|
||||
updated_by_id=updated_by_id,
|
||||
)
|
||||
|
||||
if modules is not None:
|
||||
DraftIssueModule.objects.filter(draft_issue=instance).delete()
|
||||
DraftIssueModule.objects.bulk_create(
|
||||
[
|
||||
DraftIssueModule(
|
||||
module_id=module_id,
|
||||
draft_issue=instance,
|
||||
workspace_id=workspace_id,
|
||||
project_id=project_id,
|
||||
created_by_id=created_by_id,
|
||||
updated_by_id=updated_by_id,
|
||||
)
|
||||
for module_id in modules
|
||||
],
|
||||
batch_size=10,
|
||||
)
|
||||
|
||||
# Time updation occurs even when other related models are updated
|
||||
instance.updated_at = timezone.now()
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
|
||||
class DraftIssueSerializer(BaseSerializer):
|
||||
# ids
|
||||
cycle_id = serializers.PrimaryKeyRelatedField(read_only=True)
|
||||
module_ids = serializers.ListField(
|
||||
child=serializers.UUIDField(),
|
||||
required=False,
|
||||
)
|
||||
|
||||
# Many to many
|
||||
label_ids = serializers.ListField(
|
||||
child=serializers.UUIDField(),
|
||||
required=False,
|
||||
)
|
||||
assignee_ids = serializers.ListField(
|
||||
child=serializers.UUIDField(),
|
||||
required=False,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = DraftIssue
|
||||
fields = [
|
||||
"id",
|
||||
"name",
|
||||
"state_id",
|
||||
"sort_order",
|
||||
"completed_at",
|
||||
"estimate_point",
|
||||
"priority",
|
||||
"start_date",
|
||||
"target_date",
|
||||
"project_id",
|
||||
"parent_id",
|
||||
"cycle_id",
|
||||
"module_ids",
|
||||
"label_ids",
|
||||
"assignee_ids",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"type_id",
|
||||
"description_html",
|
||||
]
|
||||
read_only_fields = fields
|
||||
|
||||
|
||||
class DraftIssueDetailSerializer(DraftIssueSerializer):
|
||||
description_html = serializers.CharField()
|
||||
|
||||
class Meta(DraftIssueSerializer.Meta):
|
||||
fields = DraftIssueSerializer.Meta.fields + [
|
||||
"description_html",
|
||||
]
|
||||
read_only_fields = fields
|
||||
+14
-14
@@ -4,22 +4,22 @@ from rest_framework import serializers
|
||||
# Module imports
|
||||
from .base import BaseSerializer
|
||||
from .issue import (
|
||||
IssueIntakeSerializer,
|
||||
IssueInboxSerializer,
|
||||
LabelLiteSerializer,
|
||||
IssueDetailSerializer,
|
||||
)
|
||||
from .project import ProjectLiteSerializer
|
||||
from .state import StateLiteSerializer
|
||||
from .user import UserLiteSerializer
|
||||
from plane.db.models import Intake, IntakeIssue, Issue
|
||||
from plane.db.models import Inbox, InboxIssue, Issue
|
||||
|
||||
|
||||
class IntakeSerializer(BaseSerializer):
|
||||
class InboxSerializer(BaseSerializer):
|
||||
project_detail = ProjectLiteSerializer(source="project", read_only=True)
|
||||
pending_issue_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Intake
|
||||
model = Inbox
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"project",
|
||||
@@ -27,11 +27,11 @@ class IntakeSerializer(BaseSerializer):
|
||||
]
|
||||
|
||||
|
||||
class IntakeIssueSerializer(BaseSerializer):
|
||||
issue = IssueIntakeSerializer(read_only=True)
|
||||
class InboxIssueSerializer(BaseSerializer):
|
||||
issue = IssueInboxSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = IntakeIssue
|
||||
model = InboxIssue
|
||||
fields = [
|
||||
"id",
|
||||
"status",
|
||||
@@ -53,14 +53,14 @@ class IntakeIssueSerializer(BaseSerializer):
|
||||
return super().to_representation(instance)
|
||||
|
||||
|
||||
class IntakeIssueDetailSerializer(BaseSerializer):
|
||||
class InboxIssueDetailSerializer(BaseSerializer):
|
||||
issue = IssueDetailSerializer(read_only=True)
|
||||
duplicate_issue_detail = IssueIntakeSerializer(
|
||||
duplicate_issue_detail = IssueInboxSerializer(
|
||||
read_only=True, source="duplicate_to"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = IntakeIssue
|
||||
model = InboxIssue
|
||||
fields = [
|
||||
"id",
|
||||
"status",
|
||||
@@ -85,14 +85,14 @@ class IntakeIssueDetailSerializer(BaseSerializer):
|
||||
return super().to_representation(instance)
|
||||
|
||||
|
||||
class IntakeIssueLiteSerializer(BaseSerializer):
|
||||
class InboxIssueLiteSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = IntakeIssue
|
||||
model = InboxIssue
|
||||
fields = ["id", "status", "duplicate_to", "snoozed_till", "source"]
|
||||
read_only_fields = fields
|
||||
|
||||
|
||||
class IssueStateIntakeSerializer(BaseSerializer):
|
||||
class IssueStateInboxSerializer(BaseSerializer):
|
||||
state_detail = StateLiteSerializer(read_only=True, source="state")
|
||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||
label_details = LabelLiteSerializer(
|
||||
@@ -102,7 +102,7 @@ class IssueStateIntakeSerializer(BaseSerializer):
|
||||
read_only=True, source="assignees", many=True
|
||||
)
|
||||
sub_issues_count = serializers.IntegerField(read_only=True)
|
||||
issue_intake = IntakeIssueLiteSerializer(read_only=True, many=True)
|
||||
issue_inbox = InboxIssueLiteSerializer(read_only=True, many=True)
|
||||
|
||||
class Meta:
|
||||
model = Issue
|
||||
@@ -27,7 +27,7 @@ from plane.db.models import (
|
||||
Module,
|
||||
ModuleIssue,
|
||||
IssueLink,
|
||||
FileAsset,
|
||||
IssueAttachment,
|
||||
IssueReaction,
|
||||
CommentReaction,
|
||||
IssueVote,
|
||||
@@ -95,8 +95,6 @@ class IssueCreateSerializer(BaseSerializer):
|
||||
write_only=True,
|
||||
required=False,
|
||||
)
|
||||
project_id = serializers.UUIDField(source="project.id", read_only=True)
|
||||
workspace_id = serializers.UUIDField(source="workspace.id", read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Issue
|
||||
@@ -500,11 +498,8 @@ class IssueLinkLiteSerializer(BaseSerializer):
|
||||
|
||||
|
||||
class IssueAttachmentSerializer(BaseSerializer):
|
||||
|
||||
asset_url = serializers.CharField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = FileAsset
|
||||
model = IssueAttachment
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"created_by",
|
||||
@@ -519,15 +514,14 @@ class IssueAttachmentSerializer(BaseSerializer):
|
||||
|
||||
class IssueAttachmentLiteSerializer(DynamicBaseSerializer):
|
||||
class Meta:
|
||||
model = FileAsset
|
||||
model = IssueAttachment
|
||||
fields = [
|
||||
"id",
|
||||
"asset",
|
||||
"attributes",
|
||||
# "issue_id",
|
||||
"issue_id",
|
||||
"updated_at",
|
||||
"updated_by",
|
||||
"asset_url",
|
||||
]
|
||||
read_only_fields = fields
|
||||
|
||||
@@ -645,7 +639,7 @@ class IssueStateSerializer(DynamicBaseSerializer):
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class IssueIntakeSerializer(DynamicBaseSerializer):
|
||||
class IssueInboxSerializer(DynamicBaseSerializer):
|
||||
label_ids = serializers.ListField(
|
||||
child=serializers.UUIDField(),
|
||||
required=False,
|
||||
|
||||
@@ -12,7 +12,6 @@ class NotificationSerializer(BaseSerializer):
|
||||
read_only=True, source="triggered_by"
|
||||
)
|
||||
is_inbox_issue = serializers.BooleanField(read_only=True)
|
||||
is_intake_issue = serializers.BooleanField(read_only=True)
|
||||
is_mentioned_notification = serializers.BooleanField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
|
||||
@@ -22,7 +22,6 @@ class ProjectSerializer(BaseSerializer):
|
||||
workspace_detail = WorkspaceLiteSerializer(
|
||||
source="workspace", read_only=True
|
||||
)
|
||||
inbox_view = serializers.BooleanField(read_only=True, source="intake_view")
|
||||
|
||||
class Meta:
|
||||
model = Project
|
||||
@@ -96,7 +95,6 @@ class ProjectLiteSerializer(BaseSerializer):
|
||||
"identifier",
|
||||
"name",
|
||||
"cover_image",
|
||||
"cover_image_url",
|
||||
"logo_props",
|
||||
"description",
|
||||
]
|
||||
@@ -119,8 +117,6 @@ class ProjectListSerializer(DynamicBaseSerializer):
|
||||
member_role = serializers.IntegerField(read_only=True)
|
||||
anchor = serializers.CharField(read_only=True)
|
||||
members = serializers.SerializerMethodField()
|
||||
cover_image_url = serializers.CharField(read_only=True)
|
||||
inbox_view = serializers.BooleanField(read_only=True, source="intake_view")
|
||||
|
||||
def get_members(self, obj):
|
||||
project_members = getattr(obj, "members_list", None)
|
||||
@@ -132,7 +128,6 @@ class ProjectListSerializer(DynamicBaseSerializer):
|
||||
"member_id": member.member_id,
|
||||
"member__display_name": member.member.display_name,
|
||||
"member__avatar": member.member.avatar,
|
||||
"member__avatar_url": member.member.avatar_url,
|
||||
}
|
||||
for member in project_members
|
||||
]
|
||||
|
||||
@@ -56,15 +56,12 @@ class UserSerializer(BaseSerializer):
|
||||
|
||||
|
||||
class UserMeSerializer(BaseSerializer):
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = [
|
||||
"id",
|
||||
"avatar",
|
||||
"cover_image",
|
||||
"avatar_url",
|
||||
"cover_image_url",
|
||||
"date_joined",
|
||||
"display_name",
|
||||
"email",
|
||||
@@ -159,7 +156,6 @@ class UserLiteSerializer(BaseSerializer):
|
||||
"first_name",
|
||||
"last_name",
|
||||
"avatar",
|
||||
"avatar_url",
|
||||
"is_bot",
|
||||
"display_name",
|
||||
]
|
||||
@@ -177,11 +173,9 @@ class UserAdminLiteSerializer(BaseSerializer):
|
||||
"first_name",
|
||||
"last_name",
|
||||
"avatar",
|
||||
"avatar_url",
|
||||
"is_bot",
|
||||
"display_name",
|
||||
"email",
|
||||
"last_login_medium",
|
||||
]
|
||||
read_only_fields = [
|
||||
"id",
|
||||
|
||||
@@ -22,7 +22,6 @@ class WorkSpaceSerializer(DynamicBaseSerializer):
|
||||
owner = UserLiteSerializer(read_only=True)
|
||||
total_members = serializers.IntegerField(read_only=True)
|
||||
total_issues = serializers.IntegerField(read_only=True)
|
||||
logo_url = serializers.CharField(read_only=True)
|
||||
|
||||
def validate_slug(self, value):
|
||||
# Check if the slug is restricted
|
||||
@@ -40,7 +39,6 @@ class WorkSpaceSerializer(DynamicBaseSerializer):
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"owner",
|
||||
"logo_url",
|
||||
]
|
||||
|
||||
|
||||
@@ -65,7 +63,6 @@ class WorkSpaceMemberSerializer(DynamicBaseSerializer):
|
||||
|
||||
|
||||
class WorkspaceMemberMeSerializer(BaseSerializer):
|
||||
draft_issue_count = serializers.IntegerField(read_only=True)
|
||||
class Meta:
|
||||
model = WorkspaceMember
|
||||
fields = "__all__"
|
||||
|
||||
@@ -5,7 +5,7 @@ from .cycle import urlpatterns as cycle_urls
|
||||
from .dashboard import urlpatterns as dashboard_urls
|
||||
from .estimate import urlpatterns as estimate_urls
|
||||
from .external import urlpatterns as external_urls
|
||||
from .intake import urlpatterns as intake_urls
|
||||
from .inbox import urlpatterns as inbox_urls
|
||||
from .issue import urlpatterns as issue_urls
|
||||
from .module import urlpatterns as module_urls
|
||||
from .notification import urlpatterns as notification_urls
|
||||
@@ -25,7 +25,7 @@ urlpatterns = [
|
||||
*dashboard_urls,
|
||||
*estimate_urls,
|
||||
*external_urls,
|
||||
*intake_urls,
|
||||
*inbox_urls,
|
||||
*issue_urls,
|
||||
*module_urls,
|
||||
*notification_urls,
|
||||
|
||||
@@ -5,13 +5,6 @@ from plane.app.views import (
|
||||
FileAssetEndpoint,
|
||||
UserAssetsEndpoint,
|
||||
FileAssetViewSet,
|
||||
# V2 Endpoints
|
||||
WorkspaceFileAssetEndpoint,
|
||||
UserAssetsV2Endpoint,
|
||||
StaticFileAssetEndpoint,
|
||||
AssetRestoreEndpoint,
|
||||
ProjectAssetEndpoint,
|
||||
ProjectBulkAssetEndpoint,
|
||||
)
|
||||
|
||||
|
||||
@@ -45,49 +38,4 @@ urlpatterns = [
|
||||
),
|
||||
name="file-assets-restore",
|
||||
),
|
||||
# V2 Endpoints
|
||||
path(
|
||||
"assets/v2/workspaces/<str:slug>/",
|
||||
WorkspaceFileAssetEndpoint.as_view(),
|
||||
name="workspace-file-assets",
|
||||
),
|
||||
path(
|
||||
"assets/v2/workspaces/<str:slug>/<uuid:asset_id>/",
|
||||
WorkspaceFileAssetEndpoint.as_view(),
|
||||
name="workspace-file-assets",
|
||||
),
|
||||
path(
|
||||
"assets/v2/user-assets/",
|
||||
UserAssetsV2Endpoint.as_view(),
|
||||
name="user-file-assets",
|
||||
),
|
||||
path(
|
||||
"assets/v2/user-assets/<uuid:asset_id>/",
|
||||
UserAssetsV2Endpoint.as_view(),
|
||||
name="user-file-assets",
|
||||
),
|
||||
path(
|
||||
"assets/v2/workspaces/<str:slug>/restore/<uuid:asset_id>/",
|
||||
AssetRestoreEndpoint.as_view(),
|
||||
name="asset-restore",
|
||||
),
|
||||
path(
|
||||
"assets/v2/static/<uuid:asset_id>/",
|
||||
StaticFileAssetEndpoint.as_view(),
|
||||
name="static-file-asset",
|
||||
),
|
||||
path(
|
||||
"assets/v2/workspaces/<str:slug>/projects/<uuid:project_id>/",
|
||||
ProjectAssetEndpoint.as_view(),
|
||||
name="bulk-asset-update",
|
||||
),
|
||||
path(
|
||||
"assets/v2/workspaces/<str:slug>/projects/<uuid:project_id>/<uuid:pk>/",
|
||||
ProjectAssetEndpoint.as_view(),
|
||||
name="bulk-asset-update",
|
||||
),
|
||||
path(
|
||||
"assets/v2/workspaces/<str:slug>/projects/<uuid:project_id>/<uuid:entity_id>/bulk/",
|
||||
ProjectBulkAssetEndpoint.as_view(),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
from django.urls import path
|
||||
|
||||
|
||||
from plane.app.views import (
|
||||
InboxViewSet,
|
||||
InboxIssueViewSet,
|
||||
)
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/",
|
||||
InboxViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="inbox",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/<uuid:pk>/",
|
||||
InboxViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"patch": "partial_update",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="inbox",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/",
|
||||
InboxIssueViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="inbox-issue",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/<uuid:pk>/",
|
||||
InboxIssueViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"patch": "partial_update",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="inbox-issue",
|
||||
),
|
||||
]
|
||||
@@ -1,95 +0,0 @@
|
||||
from django.urls import path
|
||||
|
||||
|
||||
from plane.app.views import (
|
||||
IntakeViewSet,
|
||||
IntakeIssueViewSet,
|
||||
)
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/intakes/",
|
||||
IntakeViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="intake",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/intakes/<uuid:pk>/",
|
||||
IntakeViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"patch": "partial_update",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="intake",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/intake-issues/",
|
||||
IntakeIssueViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="intake-issue",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/intake-issues/<uuid:pk>/",
|
||||
IntakeIssueViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"patch": "partial_update",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="intake-issue",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/",
|
||||
IntakeViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="inbox",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/<uuid:pk>/",
|
||||
IntakeViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"patch": "partial_update",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="inbox",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/",
|
||||
IntakeIssueViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="inbox-issue",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/<uuid:pk>/",
|
||||
IntakeIssueViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"patch": "partial_update",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="inbox-issue",
|
||||
),
|
||||
]
|
||||
@@ -11,6 +11,7 @@ from plane.app.views import (
|
||||
IssueActivityEndpoint,
|
||||
IssueArchiveViewSet,
|
||||
IssueCommentViewSet,
|
||||
IssueDraftViewSet,
|
||||
IssueListEndpoint,
|
||||
IssueReactionViewSet,
|
||||
IssueRelationViewSet,
|
||||
@@ -19,11 +20,7 @@ from plane.app.views import (
|
||||
IssueViewSet,
|
||||
LabelViewSet,
|
||||
BulkArchiveIssuesEndpoint,
|
||||
DeletedIssuesListViewSet,
|
||||
IssuePaginatedViewSet,
|
||||
IssueDetailEndpoint,
|
||||
IssueAttachmentV2Endpoint,
|
||||
IssueBulkUpdateDateEndpoint,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
@@ -42,13 +39,7 @@ urlpatterns = [
|
||||
),
|
||||
name="project-issue",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues-detail/",
|
||||
IssueDetailEndpoint.as_view(),
|
||||
name="project-issue-detail",
|
||||
),
|
||||
# updated v1 paginated issues
|
||||
# updated v2 paginated issues
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/v2/issues/",
|
||||
IssuePaginatedViewSet.as_view({"get": "list"}),
|
||||
@@ -141,18 +132,6 @@ urlpatterns = [
|
||||
IssueAttachmentEndpoint.as_view(),
|
||||
name="project-issue-attachments",
|
||||
),
|
||||
# V2 Attachments
|
||||
path(
|
||||
"assets/v2/workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/attachments/",
|
||||
IssueAttachmentV2Endpoint.as_view(),
|
||||
name="project-issue-attachments",
|
||||
),
|
||||
path(
|
||||
"assets/v2/workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/attachments/<uuid:pk>/",
|
||||
IssueAttachmentV2Endpoint.as_view(),
|
||||
name="project-issue-attachments",
|
||||
),
|
||||
## Export Issues
|
||||
path(
|
||||
"workspaces/<str:slug>/export-issues/",
|
||||
ExportIssuesEndpoint.as_view(),
|
||||
@@ -310,14 +289,26 @@ urlpatterns = [
|
||||
name="issue-relation",
|
||||
),
|
||||
## End Issue Relation
|
||||
## Issue Drafts
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/deleted-issues/",
|
||||
DeletedIssuesListViewSet.as_view(),
|
||||
name="deleted-issues",
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-drafts/",
|
||||
IssueDraftViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="project-issue-draft",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-dates/",
|
||||
IssueBulkUpdateDateEndpoint.as_view(),
|
||||
name="project-issue-dates",
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-drafts/<uuid:pk>/",
|
||||
IssueDraftViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"patch": "partial_update",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="project-issue-draft",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -27,7 +27,6 @@ from plane.app.views import (
|
||||
WorkspaceCyclesEndpoint,
|
||||
WorkspaceFavoriteEndpoint,
|
||||
WorkspaceFavoriteGroupEndpoint,
|
||||
WorkspaceDraftIssueViewSet,
|
||||
)
|
||||
|
||||
|
||||
@@ -255,30 +254,4 @@ urlpatterns = [
|
||||
WorkspaceFavoriteGroupEndpoint.as_view(),
|
||||
name="workspace-user-favorites-groups",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/draft-issues/",
|
||||
WorkspaceDraftIssueViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="workspace-draft-issues",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/draft-issues/<uuid:pk>/",
|
||||
WorkspaceDraftIssueViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"patch": "partial_update",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="workspace-drafts-issues",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/draft-to-issue/<uuid:draft_id>/",
|
||||
WorkspaceDraftIssueViewSet.as_view({"post": "create_draft_to_issue"}),
|
||||
name="workspace-drafts-issues",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -40,8 +40,6 @@ from .workspace.base import (
|
||||
ExportWorkspaceUserActivityEndpoint,
|
||||
)
|
||||
|
||||
from .workspace.draft import WorkspaceDraftIssueViewSet
|
||||
|
||||
from .workspace.favorite import (
|
||||
WorkspaceFavoriteEndpoint,
|
||||
WorkspaceFavoriteGroupEndpoint,
|
||||
@@ -110,28 +108,13 @@ from .cycle.archive import (
|
||||
CycleArchiveUnarchiveEndpoint,
|
||||
)
|
||||
|
||||
from .asset.base import (
|
||||
FileAssetEndpoint,
|
||||
UserAssetsEndpoint,
|
||||
FileAssetViewSet,
|
||||
)
|
||||
from .asset.v2 import (
|
||||
WorkspaceFileAssetEndpoint,
|
||||
UserAssetsV2Endpoint,
|
||||
StaticFileAssetEndpoint,
|
||||
AssetRestoreEndpoint,
|
||||
ProjectAssetEndpoint,
|
||||
ProjectBulkAssetEndpoint,
|
||||
)
|
||||
from .asset.base import FileAssetEndpoint, UserAssetsEndpoint, FileAssetViewSet
|
||||
from .issue.base import (
|
||||
IssueListEndpoint,
|
||||
IssueViewSet,
|
||||
IssueUserDisplayPropertyEndpoint,
|
||||
BulkDeleteIssuesEndpoint,
|
||||
DeletedIssuesListViewSet,
|
||||
IssuePaginatedViewSet,
|
||||
IssueDetailEndpoint,
|
||||
IssueBulkUpdateDateEndpoint,
|
||||
)
|
||||
|
||||
from .issue.activity import (
|
||||
@@ -142,8 +125,6 @@ from .issue.archive import IssueArchiveViewSet, BulkArchiveIssuesEndpoint
|
||||
|
||||
from .issue.attachment import (
|
||||
IssueAttachmentEndpoint,
|
||||
# V2
|
||||
IssueAttachmentV2Endpoint,
|
||||
)
|
||||
|
||||
from .issue.comment import (
|
||||
@@ -151,6 +132,8 @@ from .issue.comment import (
|
||||
CommentReactionViewSet,
|
||||
)
|
||||
|
||||
from .issue.draft import IssueDraftViewSet
|
||||
|
||||
from .issue.label import (
|
||||
LabelViewSet,
|
||||
BulkCreateIssueLabelsEndpoint,
|
||||
@@ -220,7 +203,7 @@ from .estimate.base import (
|
||||
EstimatePointEndpoint,
|
||||
)
|
||||
|
||||
from .intake.base import IntakeViewSet, IntakeIssueViewSet
|
||||
from .inbox.base import InboxViewSet, InboxIssueViewSet
|
||||
|
||||
from .analytic.base import (
|
||||
AnalyticsEndpoint,
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
# Django imports
|
||||
from django.db.models import Count, F, Sum, Q
|
||||
from django.db.models import Count, F, Sum
|
||||
from django.db.models.functions import ExtractMonth
|
||||
from django.utils import timezone
|
||||
from django.db.models.functions import Concat
|
||||
from django.db.models import Case, When, Value
|
||||
from django.db import models
|
||||
|
||||
# Third party imports
|
||||
from rest_framework import status
|
||||
@@ -24,11 +21,7 @@ from plane.app.permissions import allow_permission, ROLE
|
||||
class AnalyticsEndpoint(BaseAPIView):
|
||||
|
||||
@allow_permission(
|
||||
[
|
||||
ROLE.ADMIN,
|
||||
ROLE.MEMBER,
|
||||
],
|
||||
level="WORKSPACE",
|
||||
[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER], level="WORKSPACE"
|
||||
)
|
||||
def get(self, request, slug):
|
||||
x_axis = request.GET.get("x_axis", False)
|
||||
@@ -110,10 +103,7 @@ class AnalyticsEndpoint(BaseAPIView):
|
||||
if x_axis in ["labels__id"] or segment in ["labels__id"]:
|
||||
label_details = (
|
||||
Issue.objects.filter(
|
||||
workspace__slug=slug,
|
||||
**filters,
|
||||
labels__id__isnull=False,
|
||||
label_issue__deleted_at__isnull=True,
|
||||
workspace__slug=slug, **filters, labels__id__isnull=False
|
||||
)
|
||||
.distinct("labels__id")
|
||||
.order_by("labels__id")
|
||||
@@ -124,37 +114,14 @@ class AnalyticsEndpoint(BaseAPIView):
|
||||
if x_axis in ["assignees__id"] or segment in ["assignees__id"]:
|
||||
assignee_details = (
|
||||
Issue.issue_objects.filter(
|
||||
Q(
|
||||
Q(assignees__avatar__isnull=False)
|
||||
| Q(assignees__avatar_asset__isnull=False)
|
||||
),
|
||||
workspace__slug=slug,
|
||||
**filters,
|
||||
)
|
||||
.annotate(
|
||||
assignees__avatar_url=Case(
|
||||
# If `avatar_asset` exists, use it to generate the asset URL
|
||||
When(
|
||||
assignees__avatar_asset__isnull=False,
|
||||
then=Concat(
|
||||
Value("/api/assets/v2/static/"),
|
||||
"assignees__avatar_asset", # Assuming avatar_asset has an id or relevant field
|
||||
Value("/"),
|
||||
),
|
||||
),
|
||||
# If `avatar_asset` is None, fall back to using `avatar` field directly
|
||||
When(
|
||||
assignees__avatar_asset__isnull=True,
|
||||
then="assignees__avatar",
|
||||
),
|
||||
default=Value(None),
|
||||
output_field=models.CharField(),
|
||||
)
|
||||
assignees__avatar__isnull=False,
|
||||
)
|
||||
.order_by("assignees__id")
|
||||
.distinct("assignees__id")
|
||||
.values(
|
||||
"assignees__avatar_url",
|
||||
"assignees__avatar",
|
||||
"assignees__display_name",
|
||||
"assignees__first_name",
|
||||
"assignees__last_name",
|
||||
@@ -171,7 +138,6 @@ class AnalyticsEndpoint(BaseAPIView):
|
||||
workspace__slug=slug,
|
||||
**filters,
|
||||
issue_cycle__cycle_id__isnull=False,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
)
|
||||
.distinct("issue_cycle__cycle_id")
|
||||
.order_by("issue_cycle__cycle_id")
|
||||
@@ -190,7 +156,6 @@ class AnalyticsEndpoint(BaseAPIView):
|
||||
workspace__slug=slug,
|
||||
**filters,
|
||||
issue_module__module_id__isnull=False,
|
||||
issue_module__deleted_at__isnull=True,
|
||||
)
|
||||
.distinct("issue_module__module_id")
|
||||
.order_by("issue_module__module_id")
|
||||
@@ -238,11 +203,7 @@ class AnalyticViewViewset(BaseViewSet):
|
||||
class SavedAnalyticEndpoint(BaseAPIView):
|
||||
|
||||
@allow_permission(
|
||||
[
|
||||
ROLE.ADMIN,
|
||||
ROLE.MEMBER,
|
||||
],
|
||||
level="WORKSPACE",
|
||||
[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER], level="WORKSPACE"
|
||||
)
|
||||
def get(self, request, slug, analytic_id):
|
||||
analytic_view = AnalyticView.objects.get(
|
||||
@@ -275,11 +236,7 @@ class SavedAnalyticEndpoint(BaseAPIView):
|
||||
class ExportAnalyticsEndpoint(BaseAPIView):
|
||||
|
||||
@allow_permission(
|
||||
[
|
||||
ROLE.ADMIN,
|
||||
ROLE.MEMBER,
|
||||
],
|
||||
level="WORKSPACE",
|
||||
[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER], level="WORKSPACE"
|
||||
)
|
||||
def post(self, request, slug):
|
||||
x_axis = request.data.get("x_axis", False)
|
||||
@@ -345,7 +302,9 @@ class ExportAnalyticsEndpoint(BaseAPIView):
|
||||
|
||||
class DefaultAnalyticsEndpoint(BaseAPIView):
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
|
||||
@allow_permission(
|
||||
[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST], level="WORKSPACE"
|
||||
)
|
||||
def get(self, request, slug):
|
||||
filters = issue_filters(request.GET, "GET")
|
||||
base_issues = Issue.issue_objects.filter(
|
||||
@@ -386,6 +345,7 @@ class DefaultAnalyticsEndpoint(BaseAPIView):
|
||||
user_details = [
|
||||
"created_by__first_name",
|
||||
"created_by__last_name",
|
||||
"created_by__avatar",
|
||||
"created_by__display_name",
|
||||
"created_by__id",
|
||||
]
|
||||
@@ -394,32 +354,13 @@ class DefaultAnalyticsEndpoint(BaseAPIView):
|
||||
base_issues.exclude(created_by=None)
|
||||
.values(*user_details)
|
||||
.annotate(count=Count("id"))
|
||||
.annotate(
|
||||
created_by__avatar_url=Case(
|
||||
# If `avatar_asset` exists, use it to generate the asset URL
|
||||
When(
|
||||
created_by__avatar_asset__isnull=False,
|
||||
then=Concat(
|
||||
Value("/api/assets/v2/static/"),
|
||||
"created_by__avatar_asset", # Assuming avatar_asset has an id or relevant field
|
||||
Value("/"),
|
||||
),
|
||||
),
|
||||
# If `avatar_asset` is None, fall back to using `avatar` field directly
|
||||
When(
|
||||
created_by__avatar_asset__isnull=True,
|
||||
then="created_by__avatar",
|
||||
),
|
||||
default=Value(None),
|
||||
output_field=models.CharField(),
|
||||
)
|
||||
)
|
||||
.order_by("-count")[:5]
|
||||
)
|
||||
|
||||
user_assignee_details = [
|
||||
"assignees__first_name",
|
||||
"assignees__last_name",
|
||||
"assignees__avatar",
|
||||
"assignees__display_name",
|
||||
"assignees__id",
|
||||
]
|
||||
@@ -428,26 +369,6 @@ class DefaultAnalyticsEndpoint(BaseAPIView):
|
||||
base_issues.filter(completed_at__isnull=False)
|
||||
.exclude(assignees=None)
|
||||
.values(*user_assignee_details)
|
||||
.annotate(
|
||||
assignees__avatar_url=Case(
|
||||
# If `avatar_asset` exists, use it to generate the asset URL
|
||||
When(
|
||||
assignees__avatar_asset__isnull=False,
|
||||
then=Concat(
|
||||
Value("/api/assets/v2/static/"),
|
||||
"assignees__avatar_asset", # Assuming avatar_asset has an id or relevant field
|
||||
Value("/"),
|
||||
),
|
||||
),
|
||||
# If `avatar_asset` is None, fall back to using `avatar` field directly
|
||||
When(
|
||||
assignees__avatar_asset__isnull=True,
|
||||
then="assignees__avatar",
|
||||
),
|
||||
default=Value(None),
|
||||
output_field=models.CharField(),
|
||||
)
|
||||
)
|
||||
.annotate(count=Count("id"))
|
||||
.order_by("-count")[:5]
|
||||
)
|
||||
@@ -456,26 +377,6 @@ class DefaultAnalyticsEndpoint(BaseAPIView):
|
||||
base_issues.filter(completed_at__isnull=True)
|
||||
.values(*user_assignee_details)
|
||||
.annotate(count=Count("id"))
|
||||
.annotate(
|
||||
assignees__avatar_url=Case(
|
||||
# If `avatar_asset` exists, use it to generate the asset URL
|
||||
When(
|
||||
assignees__avatar_asset__isnull=False,
|
||||
then=Concat(
|
||||
Value("/api/assets/v2/static/"),
|
||||
"assignees__avatar_asset", # Assuming avatar_asset has an id or relevant field
|
||||
Value("/"),
|
||||
),
|
||||
),
|
||||
# If `avatar_asset` is None, fall back to using `avatar` field directly
|
||||
When(
|
||||
assignees__avatar_asset__isnull=True,
|
||||
then="assignees__avatar",
|
||||
),
|
||||
default=Value(None),
|
||||
output_field=models.CharField(),
|
||||
)
|
||||
)
|
||||
.order_by("-count")
|
||||
)
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ class FileAssetEndpoint(BaseAPIView):
|
||||
asset_key = str(workspace_id) + "/" + asset_key
|
||||
file_asset = FileAsset.objects.get(asset=asset_key)
|
||||
file_asset.is_deleted = True
|
||||
file_asset.save(update_fields=["is_deleted"])
|
||||
file_asset.save()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ class FileAssetViewSet(BaseViewSet):
|
||||
asset_key = str(workspace_id) + "/" + asset_key
|
||||
file_asset = FileAsset.objects.get(asset=asset_key)
|
||||
file_asset.is_deleted = False
|
||||
file_asset.save(update_fields=["is_deleted"])
|
||||
file_asset.save()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
@@ -96,5 +96,5 @@ class UserAssetsEndpoint(BaseAPIView):
|
||||
asset=asset_key, created_by=request.user
|
||||
)
|
||||
file_asset.is_deleted = True
|
||||
file_asset.save(update_fields=["is_deleted"])
|
||||
file_asset.save()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@@ -1,826 +0,0 @@
|
||||
# Python imports
|
||||
import uuid
|
||||
|
||||
# Django imports
|
||||
from django.conf import settings
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.utils import timezone
|
||||
|
||||
# Third party imports
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.permissions import AllowAny
|
||||
|
||||
# Module imports
|
||||
from ..base import BaseAPIView
|
||||
from plane.db.models import (
|
||||
FileAsset,
|
||||
Workspace,
|
||||
Project,
|
||||
User,
|
||||
)
|
||||
from plane.settings.storage import S3Storage
|
||||
from plane.app.permissions import allow_permission, ROLE
|
||||
from plane.utils.cache import invalidate_cache_directly
|
||||
from plane.bgtasks.storage_metadata_task import get_asset_object_metadata
|
||||
|
||||
|
||||
class UserAssetsV2Endpoint(BaseAPIView):
|
||||
"""This endpoint is used to upload user profile images."""
|
||||
|
||||
def asset_delete(self, asset_id):
|
||||
asset = FileAsset.objects.filter(id=asset_id).first()
|
||||
if asset is None:
|
||||
return
|
||||
asset.is_deleted = True
|
||||
asset.deleted_at = timezone.now()
|
||||
asset.save(update_fields=["is_deleted", "deleted_at"])
|
||||
return
|
||||
|
||||
def entity_asset_save(self, asset_id, entity_type, asset, request):
|
||||
# User Avatar
|
||||
if entity_type == FileAsset.EntityTypeContext.USER_AVATAR:
|
||||
user = User.objects.get(id=asset.user_id)
|
||||
user.avatar = ""
|
||||
# Delete the previous avatar
|
||||
if user.avatar_asset_id:
|
||||
self.asset_delete(user.avatar_asset_id)
|
||||
# Save the new avatar
|
||||
user.avatar_asset_id = asset_id
|
||||
user.save()
|
||||
invalidate_cache_directly(
|
||||
path="/api/users/me/",
|
||||
url_params=False,
|
||||
user=True,
|
||||
request=request,
|
||||
)
|
||||
invalidate_cache_directly(
|
||||
path="/api/users/me/settings/",
|
||||
url_params=False,
|
||||
user=True,
|
||||
request=request,
|
||||
)
|
||||
return
|
||||
# User Cover
|
||||
if entity_type == FileAsset.EntityTypeContext.USER_COVER:
|
||||
user = User.objects.get(id=asset.user_id)
|
||||
user.cover_image = None
|
||||
# Delete the previous cover image
|
||||
if user.cover_image_asset_id:
|
||||
self.asset_delete(user.cover_image_asset_id)
|
||||
# Save the new cover image
|
||||
user.cover_image_asset_id = asset_id
|
||||
user.save()
|
||||
invalidate_cache_directly(
|
||||
path="/api/users/me/",
|
||||
url_params=False,
|
||||
user=True,
|
||||
request=request,
|
||||
)
|
||||
invalidate_cache_directly(
|
||||
path="/api/users/me/settings/",
|
||||
url_params=False,
|
||||
user=True,
|
||||
request=request,
|
||||
)
|
||||
return
|
||||
return
|
||||
|
||||
def entity_asset_delete(self, entity_type, asset, request):
|
||||
# User Avatar
|
||||
if entity_type == FileAsset.EntityTypeContext.USER_AVATAR:
|
||||
user = User.objects.get(id=asset.user_id)
|
||||
user.avatar_asset_id = None
|
||||
user.save()
|
||||
invalidate_cache_directly(
|
||||
path="/api/users/me/",
|
||||
url_params=False,
|
||||
user=True,
|
||||
request=request,
|
||||
)
|
||||
invalidate_cache_directly(
|
||||
path="/api/users/me/settings/",
|
||||
url_params=False,
|
||||
user=True,
|
||||
request=request,
|
||||
)
|
||||
return
|
||||
# User Cover
|
||||
if entity_type == FileAsset.EntityTypeContext.USER_COVER:
|
||||
user = User.objects.get(id=asset.user_id)
|
||||
user.cover_image_asset_id = None
|
||||
user.save()
|
||||
invalidate_cache_directly(
|
||||
path="/api/users/me/",
|
||||
url_params=False,
|
||||
user=True,
|
||||
request=request,
|
||||
)
|
||||
invalidate_cache_directly(
|
||||
path="/api/users/me/settings/",
|
||||
url_params=False,
|
||||
user=True,
|
||||
request=request,
|
||||
)
|
||||
return
|
||||
return
|
||||
|
||||
def post(self, request):
|
||||
# get the asset key
|
||||
name = request.data.get("name")
|
||||
type = request.data.get("type", "image/jpeg")
|
||||
size = int(request.data.get("size", settings.FILE_SIZE_LIMIT))
|
||||
entity_type = request.data.get("entity_type", False)
|
||||
|
||||
# Check if the file size is within the limit
|
||||
size_limit = min(size, settings.FILE_SIZE_LIMIT)
|
||||
|
||||
# Check if the entity type is allowed
|
||||
if not entity_type or entity_type not in ["USER_AVATAR", "USER_COVER"]:
|
||||
return Response(
|
||||
{
|
||||
"error": "Invalid entity type.",
|
||||
"status": False,
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Check if the file type is allowed
|
||||
allowed_types = [
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/webp",
|
||||
"image/jpg",
|
||||
]
|
||||
if type not in allowed_types:
|
||||
return Response(
|
||||
{
|
||||
"error": "Invalid file type. Only JPEG and PNG files are allowed.",
|
||||
"status": False,
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# asset key
|
||||
asset_key = f"{uuid.uuid4().hex}-{name}"
|
||||
|
||||
# Create a File Asset
|
||||
asset = FileAsset.objects.create(
|
||||
attributes={
|
||||
"name": name,
|
||||
"type": type,
|
||||
"size": size_limit,
|
||||
},
|
||||
asset=asset_key,
|
||||
size=size_limit,
|
||||
user=request.user,
|
||||
created_by=request.user,
|
||||
entity_type=entity_type,
|
||||
)
|
||||
|
||||
# Get the presigned URL
|
||||
storage = S3Storage(request=request)
|
||||
# Generate a presigned URL to share an S3 object
|
||||
presigned_url = storage.generate_presigned_post(
|
||||
object_name=asset_key,
|
||||
file_type=type,
|
||||
file_size=size_limit,
|
||||
)
|
||||
# Return the presigned URL
|
||||
return Response(
|
||||
{
|
||||
"upload_data": presigned_url,
|
||||
"asset_id": str(asset.id),
|
||||
"asset_url": asset.asset_url,
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
def patch(self, request, asset_id):
|
||||
# get the asset id
|
||||
asset = FileAsset.objects.get(id=asset_id, user_id=request.user.id)
|
||||
# get the storage metadata
|
||||
asset.is_uploaded = True
|
||||
# get the storage metadata
|
||||
if not asset.storage_metadata:
|
||||
get_asset_object_metadata.delay(asset_id=str(asset_id))
|
||||
# get the entity and save the asset id for the request field
|
||||
self.entity_asset_save(
|
||||
asset_id=asset_id,
|
||||
entity_type=asset.entity_type,
|
||||
asset=asset,
|
||||
request=request,
|
||||
)
|
||||
# update the attributes
|
||||
asset.attributes = request.data.get("attributes", asset.attributes)
|
||||
# save the asset
|
||||
asset.save(update_fields=["is_uploaded", "attributes"])
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
def delete(self, request, asset_id):
|
||||
asset = FileAsset.objects.get(id=asset_id, user_id=request.user.id)
|
||||
asset.is_deleted = True
|
||||
asset.deleted_at = timezone.now()
|
||||
# get the entity and save the asset id for the request field
|
||||
self.entity_asset_delete(
|
||||
entity_type=asset.entity_type, asset=asset, request=request
|
||||
)
|
||||
asset.save(update_fields=["is_deleted", "deleted_at"])
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
class WorkspaceFileAssetEndpoint(BaseAPIView):
|
||||
"""This endpoint is used to upload cover images/logos etc for workspace, projects and users."""
|
||||
|
||||
def get_entity_id_field(self, entity_type, entity_id):
|
||||
# Workspace Logo
|
||||
if entity_type == FileAsset.EntityTypeContext.WORKSPACE_LOGO:
|
||||
return {
|
||||
"workspace_id": entity_id,
|
||||
}
|
||||
|
||||
# Project Cover
|
||||
if entity_type == FileAsset.EntityTypeContext.PROJECT_COVER:
|
||||
return {
|
||||
"project_id": entity_id,
|
||||
}
|
||||
|
||||
# User Avatar and Cover
|
||||
if entity_type in [
|
||||
FileAsset.EntityTypeContext.USER_AVATAR,
|
||||
FileAsset.EntityTypeContext.USER_COVER,
|
||||
]:
|
||||
return {
|
||||
"user_id": entity_id,
|
||||
}
|
||||
|
||||
# Issue Attachment and Description
|
||||
if entity_type in [
|
||||
FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
|
||||
FileAsset.EntityTypeContext.ISSUE_DESCRIPTION,
|
||||
]:
|
||||
return {
|
||||
"issue_id": entity_id,
|
||||
}
|
||||
|
||||
# Page Description
|
||||
if entity_type == FileAsset.EntityTypeContext.PAGE_DESCRIPTION:
|
||||
return {
|
||||
"page_id": entity_id,
|
||||
}
|
||||
|
||||
# Comment Description
|
||||
if entity_type == FileAsset.EntityTypeContext.COMMENT_DESCRIPTION:
|
||||
return {
|
||||
"comment_id": entity_id,
|
||||
}
|
||||
return {}
|
||||
|
||||
def asset_delete(self, asset_id):
|
||||
asset = FileAsset.objects.filter(id=asset_id).first()
|
||||
# Check if the asset exists
|
||||
if asset is None:
|
||||
return
|
||||
# Mark the asset as deleted
|
||||
asset.is_deleted = True
|
||||
asset.deleted_at = timezone.now()
|
||||
asset.save(update_fields=["is_deleted", "deleted_at"])
|
||||
return
|
||||
|
||||
def entity_asset_save(self, asset_id, entity_type, asset, request):
|
||||
# Workspace Logo
|
||||
if entity_type == FileAsset.EntityTypeContext.WORKSPACE_LOGO:
|
||||
workspace = Workspace.objects.filter(id=asset.workspace_id).first()
|
||||
if workspace is None:
|
||||
return
|
||||
# Delete the previous logo
|
||||
if workspace.logo_asset_id:
|
||||
self.asset_delete(workspace.logo_asset_id)
|
||||
# Save the new logo
|
||||
workspace.logo = ""
|
||||
workspace.logo_asset_id = asset_id
|
||||
workspace.save()
|
||||
invalidate_cache_directly(
|
||||
path="/api/workspaces/",
|
||||
url_params=False,
|
||||
user=False,
|
||||
request=request,
|
||||
)
|
||||
invalidate_cache_directly(
|
||||
path="/api/users/me/workspaces/",
|
||||
url_params=False,
|
||||
user=True,
|
||||
request=request,
|
||||
)
|
||||
invalidate_cache_directly(
|
||||
path="/api/instances/",
|
||||
url_params=False,
|
||||
user=False,
|
||||
request=request,
|
||||
)
|
||||
return
|
||||
|
||||
# Project Cover
|
||||
elif entity_type == FileAsset.EntityTypeContext.PROJECT_COVER:
|
||||
project = Project.objects.filter(id=asset.project_id).first()
|
||||
if project is None:
|
||||
return
|
||||
# Delete the previous cover image
|
||||
if project.cover_image_asset_id:
|
||||
self.asset_delete(project.cover_image_asset_id)
|
||||
# Save the new cover image
|
||||
project.cover_image = ""
|
||||
project.cover_image_asset_id = asset_id
|
||||
project.save()
|
||||
return
|
||||
else:
|
||||
return
|
||||
|
||||
def entity_asset_delete(self, entity_type, asset, request):
|
||||
# Workspace Logo
|
||||
if entity_type == FileAsset.EntityTypeContext.WORKSPACE_LOGO:
|
||||
workspace = Workspace.objects.get(id=asset.workspace_id)
|
||||
if workspace is None:
|
||||
return
|
||||
workspace.logo_asset_id = None
|
||||
workspace.save()
|
||||
invalidate_cache_directly(
|
||||
path="/api/workspaces/",
|
||||
url_params=False,
|
||||
user=False,
|
||||
request=request,
|
||||
)
|
||||
invalidate_cache_directly(
|
||||
path="/api/users/me/workspaces/",
|
||||
url_params=False,
|
||||
user=True,
|
||||
request=request,
|
||||
)
|
||||
invalidate_cache_directly(
|
||||
path="/api/instances/",
|
||||
url_params=False,
|
||||
user=False,
|
||||
request=request,
|
||||
)
|
||||
return
|
||||
# Project Cover
|
||||
elif entity_type == FileAsset.EntityTypeContext.PROJECT_COVER:
|
||||
project = Project.objects.filter(id=asset.project_id).first()
|
||||
if project is None:
|
||||
return
|
||||
project.cover_image_asset_id = None
|
||||
project.save()
|
||||
return
|
||||
else:
|
||||
return
|
||||
|
||||
def post(self, request, slug):
|
||||
name = request.data.get("name")
|
||||
type = request.data.get("type", "image/jpeg")
|
||||
size = int(request.data.get("size", settings.FILE_SIZE_LIMIT))
|
||||
entity_type = request.data.get("entity_type")
|
||||
entity_identifier = request.data.get("entity_identifier", False)
|
||||
|
||||
# Check if the entity type is allowed
|
||||
if entity_type not in FileAsset.EntityTypeContext.values:
|
||||
return Response(
|
||||
{
|
||||
"error": "Invalid entity type.",
|
||||
"status": False,
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Check if the file type is allowed
|
||||
allowed_types = [
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/webp",
|
||||
"image/jpg",
|
||||
"image/gif",
|
||||
]
|
||||
if type not in allowed_types:
|
||||
return Response(
|
||||
{
|
||||
"error": "Invalid file type. Only JPEG and PNG files are allowed.",
|
||||
"status": False,
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Get the size limit
|
||||
size_limit = min(settings.FILE_SIZE_LIMIT, size)
|
||||
|
||||
# Get the workspace
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
|
||||
# asset key
|
||||
asset_key = f"{workspace.id}/{uuid.uuid4().hex}-{name}"
|
||||
|
||||
# Create a File Asset
|
||||
asset = FileAsset.objects.create(
|
||||
attributes={
|
||||
"name": name,
|
||||
"type": type,
|
||||
"size": size_limit,
|
||||
},
|
||||
asset=asset_key,
|
||||
size=size_limit,
|
||||
workspace=workspace,
|
||||
created_by=request.user,
|
||||
entity_type=entity_type,
|
||||
**self.get_entity_id_field(
|
||||
entity_type=entity_type, entity_id=entity_identifier
|
||||
),
|
||||
)
|
||||
|
||||
# Get the presigned URL
|
||||
storage = S3Storage(request=request)
|
||||
# Generate a presigned URL to share an S3 object
|
||||
presigned_url = storage.generate_presigned_post(
|
||||
object_name=asset_key,
|
||||
file_type=type,
|
||||
file_size=size_limit,
|
||||
)
|
||||
# Return the presigned URL
|
||||
return Response(
|
||||
{
|
||||
"upload_data": presigned_url,
|
||||
"asset_id": str(asset.id),
|
||||
"asset_url": asset.asset_url,
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
def patch(self, request, slug, asset_id):
|
||||
# get the asset id
|
||||
asset = FileAsset.objects.get(id=asset_id, workspace__slug=slug)
|
||||
# get the storage metadata
|
||||
asset.is_uploaded = True
|
||||
# get the storage metadata
|
||||
if not asset.storage_metadata:
|
||||
get_asset_object_metadata.delay(asset_id=str(asset_id))
|
||||
# get the entity and save the asset id for the request field
|
||||
self.entity_asset_save(
|
||||
asset_id=asset_id,
|
||||
entity_type=asset.entity_type,
|
||||
asset=asset,
|
||||
request=request,
|
||||
)
|
||||
# update the attributes
|
||||
asset.attributes = request.data.get("attributes", asset.attributes)
|
||||
# save the asset
|
||||
asset.save(update_fields=["is_uploaded", "attributes"])
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
def delete(self, request, slug, asset_id):
|
||||
asset = FileAsset.objects.get(id=asset_id, workspace__slug=slug)
|
||||
asset.is_deleted = True
|
||||
asset.deleted_at = timezone.now()
|
||||
# get the entity and save the asset id for the request field
|
||||
self.entity_asset_delete(
|
||||
entity_type=asset.entity_type, asset=asset, request=request
|
||||
)
|
||||
asset.save(update_fields=["is_deleted", "deleted_at"])
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
def get(self, request, slug, asset_id):
|
||||
# get the asset id
|
||||
asset = FileAsset.objects.get(id=asset_id, workspace__slug=slug)
|
||||
|
||||
# Check if the asset is uploaded
|
||||
if not asset.is_uploaded:
|
||||
return Response(
|
||||
{
|
||||
"error": "The requested asset could not be found.",
|
||||
},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
|
||||
# Get the presigned URL
|
||||
storage = S3Storage(request=request)
|
||||
# Generate a presigned URL to share an S3 object
|
||||
signed_url = storage.generate_presigned_url(
|
||||
object_name=asset.asset.name,
|
||||
)
|
||||
# Redirect to the signed URL
|
||||
return HttpResponseRedirect(signed_url)
|
||||
|
||||
|
||||
class StaticFileAssetEndpoint(BaseAPIView):
|
||||
"""This endpoint is used to get the signed URL for a static asset."""
|
||||
|
||||
permission_classes = [
|
||||
AllowAny,
|
||||
]
|
||||
|
||||
def get(self, request, asset_id):
|
||||
# get the asset id
|
||||
asset = FileAsset.objects.get(id=asset_id)
|
||||
|
||||
# Check if the asset is uploaded
|
||||
if not asset.is_uploaded:
|
||||
return Response(
|
||||
{
|
||||
"error": "The requested asset could not be found.",
|
||||
},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
|
||||
# Check if the entity type is allowed
|
||||
if asset.entity_type not in [
|
||||
FileAsset.EntityTypeContext.USER_AVATAR,
|
||||
FileAsset.EntityTypeContext.USER_COVER,
|
||||
FileAsset.EntityTypeContext.WORKSPACE_LOGO,
|
||||
FileAsset.EntityTypeContext.PROJECT_COVER,
|
||||
]:
|
||||
return Response(
|
||||
{
|
||||
"error": "Invalid entity type.",
|
||||
"status": False,
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Get the presigned URL
|
||||
storage = S3Storage(request=request)
|
||||
# Generate a presigned URL to share an S3 object
|
||||
signed_url = storage.generate_presigned_url(
|
||||
object_name=asset.asset.name,
|
||||
)
|
||||
# Redirect to the signed URL
|
||||
return HttpResponseRedirect(signed_url)
|
||||
|
||||
|
||||
class AssetRestoreEndpoint(BaseAPIView):
|
||||
"""Endpoint to restore a deleted assets."""
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
|
||||
def post(self, request, slug, asset_id):
|
||||
asset = FileAsset.all_objects.get(id=asset_id, workspace__slug=slug)
|
||||
asset.is_deleted = False
|
||||
asset.deleted_at = None
|
||||
asset.save(update_fields=["is_deleted", "deleted_at"])
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
class ProjectAssetEndpoint(BaseAPIView):
|
||||
"""This endpoint is used to upload cover images/logos etc for workspace, projects and users."""
|
||||
|
||||
def get_entity_id_field(self, entity_type, entity_id):
|
||||
if entity_type == FileAsset.EntityTypeContext.WORKSPACE_LOGO:
|
||||
return {
|
||||
"workspace_id": entity_id,
|
||||
}
|
||||
|
||||
if entity_type == FileAsset.EntityTypeContext.PROJECT_COVER:
|
||||
return {
|
||||
"project_id": entity_id,
|
||||
}
|
||||
|
||||
if entity_type in [
|
||||
FileAsset.EntityTypeContext.USER_AVATAR,
|
||||
FileAsset.EntityTypeContext.USER_COVER,
|
||||
]:
|
||||
return {
|
||||
"user_id": entity_id,
|
||||
}
|
||||
|
||||
if entity_type in [
|
||||
FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
|
||||
FileAsset.EntityTypeContext.ISSUE_DESCRIPTION,
|
||||
]:
|
||||
return {
|
||||
"issue_id": entity_id,
|
||||
}
|
||||
|
||||
if entity_type == FileAsset.EntityTypeContext.PAGE_DESCRIPTION:
|
||||
return {
|
||||
"page_id": entity_id,
|
||||
}
|
||||
|
||||
if entity_type == FileAsset.EntityTypeContext.COMMENT_DESCRIPTION:
|
||||
return {
|
||||
"comment_id": entity_id,
|
||||
}
|
||||
|
||||
if entity_type == FileAsset.EntityTypeContext.DRAFT_ISSUE_DESCRIPTION:
|
||||
return {
|
||||
"draft_issue_id": entity_id,
|
||||
}
|
||||
return {}
|
||||
|
||||
@allow_permission(
|
||||
[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST],
|
||||
)
|
||||
def post(self, request, slug, project_id):
|
||||
name = request.data.get("name")
|
||||
type = request.data.get("type", "image/jpeg")
|
||||
size = int(request.data.get("size", settings.FILE_SIZE_LIMIT))
|
||||
entity_type = request.data.get("entity_type", "")
|
||||
entity_identifier = request.data.get("entity_identifier")
|
||||
|
||||
# Check if the entity type is allowed
|
||||
if entity_type not in FileAsset.EntityTypeContext.values:
|
||||
return Response(
|
||||
{
|
||||
"error": "Invalid entity type.",
|
||||
"status": False,
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Check if the file type is allowed
|
||||
allowed_types = [
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/webp",
|
||||
"image/jpg",
|
||||
"image/gif",
|
||||
]
|
||||
if type not in allowed_types:
|
||||
return Response(
|
||||
{
|
||||
"error": "Invalid file type. Only JPEG and PNG files are allowed.",
|
||||
"status": False,
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Get the size limit
|
||||
size_limit = min(settings.FILE_SIZE_LIMIT, size)
|
||||
|
||||
# Get the workspace
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
|
||||
# asset key
|
||||
asset_key = f"{workspace.id}/{uuid.uuid4().hex}-{name}"
|
||||
|
||||
# Create a File Asset
|
||||
asset = FileAsset.objects.create(
|
||||
attributes={
|
||||
"name": name,
|
||||
"type": type,
|
||||
"size": size_limit,
|
||||
},
|
||||
asset=asset_key,
|
||||
size=size_limit,
|
||||
workspace=workspace,
|
||||
created_by=request.user,
|
||||
entity_type=entity_type,
|
||||
project_id=project_id,
|
||||
**self.get_entity_id_field(entity_type, entity_identifier),
|
||||
)
|
||||
|
||||
# Get the presigned URL
|
||||
storage = S3Storage(request=request)
|
||||
# Generate a presigned URL to share an S3 object
|
||||
presigned_url = storage.generate_presigned_post(
|
||||
object_name=asset_key,
|
||||
file_type=type,
|
||||
file_size=size_limit,
|
||||
)
|
||||
# Return the presigned URL
|
||||
return Response(
|
||||
{
|
||||
"upload_data": presigned_url,
|
||||
"asset_id": str(asset.id),
|
||||
"asset_url": asset.asset_url,
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
@allow_permission(
|
||||
[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST],
|
||||
)
|
||||
def patch(self, request, slug, project_id, pk):
|
||||
# get the asset id
|
||||
asset = FileAsset.objects.get(
|
||||
id=pk,
|
||||
)
|
||||
# get the storage metadata
|
||||
asset.is_uploaded = True
|
||||
# get the storage metadata
|
||||
if not asset.storage_metadata:
|
||||
get_asset_object_metadata.delay(asset_id=str(pk))
|
||||
|
||||
# update the attributes
|
||||
asset.attributes = request.data.get("attributes", asset.attributes)
|
||||
# save the asset
|
||||
asset.save(update_fields=["is_uploaded", "attributes"])
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
def delete(self, request, slug, project_id, pk):
|
||||
# Get the asset
|
||||
asset = FileAsset.objects.get(
|
||||
id=pk,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
)
|
||||
# Check deleted assets
|
||||
asset.is_deleted = True
|
||||
asset.deleted_at = timezone.now()
|
||||
# Save the asset
|
||||
asset.save(update_fields=["is_deleted", "deleted_at"])
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
def get(self, request, slug, project_id, pk):
|
||||
# get the asset id
|
||||
asset = FileAsset.objects.get(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
pk=pk,
|
||||
)
|
||||
|
||||
# Check if the asset is uploaded
|
||||
if not asset.is_uploaded:
|
||||
return Response(
|
||||
{
|
||||
"error": "The requested asset could not be found.",
|
||||
},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
|
||||
# Get the presigned URL
|
||||
storage = S3Storage(request=request)
|
||||
# Generate a presigned URL to share an S3 object
|
||||
signed_url = storage.generate_presigned_url(
|
||||
object_name=asset.asset.name,
|
||||
)
|
||||
# Redirect to the signed URL
|
||||
return HttpResponseRedirect(signed_url)
|
||||
|
||||
|
||||
class ProjectBulkAssetEndpoint(BaseAPIView):
|
||||
|
||||
def save_project_cover(self, asset, project_id):
|
||||
project = Project.objects.get(id=project_id)
|
||||
project.cover_image_asset_id = asset.id
|
||||
project.save()
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
def post(self, request, slug, project_id, entity_id):
|
||||
asset_ids = request.data.get("asset_ids", [])
|
||||
|
||||
# Check if the asset ids are provided
|
||||
if not asset_ids:
|
||||
return Response(
|
||||
{
|
||||
"error": "No asset ids provided.",
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# get the asset id
|
||||
assets = FileAsset.objects.filter(
|
||||
id__in=asset_ids,
|
||||
workspace__slug=slug,
|
||||
)
|
||||
|
||||
# Get the first asset
|
||||
asset = assets.first()
|
||||
|
||||
if not asset:
|
||||
return Response(
|
||||
{
|
||||
"error": "The requested asset could not be found.",
|
||||
},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
|
||||
# Check if the asset is uploaded
|
||||
if asset.entity_type == FileAsset.EntityTypeContext.PROJECT_COVER:
|
||||
assets.update(
|
||||
project_id=project_id,
|
||||
)
|
||||
[self.save_project_cover(asset, project_id) for asset in assets]
|
||||
|
||||
if asset.entity_type == FileAsset.EntityTypeContext.ISSUE_DESCRIPTION:
|
||||
assets.update(
|
||||
issue_id=entity_id,
|
||||
)
|
||||
|
||||
if (
|
||||
asset.entity_type
|
||||
== FileAsset.EntityTypeContext.COMMENT_DESCRIPTION
|
||||
):
|
||||
assets.update(
|
||||
comment_id=entity_id,
|
||||
)
|
||||
|
||||
if asset.entity_type == FileAsset.EntityTypeContext.PAGE_DESCRIPTION:
|
||||
assets.update(
|
||||
page_id=entity_id,
|
||||
)
|
||||
|
||||
if (
|
||||
asset.entity_type
|
||||
== FileAsset.EntityTypeContext.DRAFT_ISSUE_DESCRIPTION
|
||||
):
|
||||
assets.update(
|
||||
draft_issue_id=entity_id,
|
||||
)
|
||||
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
@@ -1,7 +1,6 @@
|
||||
# Django imports
|
||||
from django.contrib.postgres.aggregates import ArrayAgg
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.db import models
|
||||
from django.db.models import (
|
||||
Case,
|
||||
CharField,
|
||||
@@ -19,7 +18,7 @@ from django.db.models import (
|
||||
Sum,
|
||||
FloatField,
|
||||
)
|
||||
from django.db.models.functions import Coalesce, Cast, Concat
|
||||
from django.db.models.functions import Coalesce, Cast
|
||||
from django.utils import timezone
|
||||
|
||||
# Third party imports
|
||||
@@ -48,7 +47,6 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
estimate_point__estimate__type="points",
|
||||
state__group="backlog",
|
||||
issue_cycle__cycle_id=OuterRef("pk"),
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
)
|
||||
.values("issue_cycle__cycle_id")
|
||||
.annotate(
|
||||
@@ -63,7 +61,6 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
estimate_point__estimate__type="points",
|
||||
state__group="unstarted",
|
||||
issue_cycle__cycle_id=OuterRef("pk"),
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
)
|
||||
.values("issue_cycle__cycle_id")
|
||||
.annotate(
|
||||
@@ -78,7 +75,6 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
estimate_point__estimate__type="points",
|
||||
state__group="started",
|
||||
issue_cycle__cycle_id=OuterRef("pk"),
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
)
|
||||
.values("issue_cycle__cycle_id")
|
||||
.annotate(
|
||||
@@ -93,7 +89,6 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
estimate_point__estimate__type="points",
|
||||
state__group="cancelled",
|
||||
issue_cycle__cycle_id=OuterRef("pk"),
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
)
|
||||
.values("issue_cycle__cycle_id")
|
||||
.annotate(
|
||||
@@ -108,7 +103,6 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
estimate_point__estimate__type="points",
|
||||
state__group="completed",
|
||||
issue_cycle__cycle_id=OuterRef("pk"),
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
)
|
||||
.values("issue_cycle__cycle_id")
|
||||
.annotate(
|
||||
@@ -122,7 +116,6 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
Issue.issue_objects.filter(
|
||||
estimate_point__estimate__type="points",
|
||||
issue_cycle__cycle_id=OuterRef("pk"),
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
)
|
||||
.values("issue_cycle__cycle_id")
|
||||
.annotate(
|
||||
@@ -146,7 +139,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
Prefetch(
|
||||
"issue_cycle__issue__assignees",
|
||||
queryset=User.objects.only(
|
||||
"avatar_asset", "first_name", "id"
|
||||
"avatar", "first_name", "id"
|
||||
).distinct(),
|
||||
)
|
||||
)
|
||||
@@ -166,7 +159,6 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
filter=Q(
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
issue_cycle__issue__deleted_at__isnull=True,
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -178,7 +170,6 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
issue_cycle__issue__state__group="completed",
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
issue_cycle__issue__deleted_at__isnull=True,
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -190,7 +181,6 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
issue_cycle__issue__state__group="cancelled",
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
issue_cycle__issue__deleted_at__isnull=True,
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -202,7 +192,6 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
issue_cycle__issue__state__group="started",
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
issue_cycle__issue__deleted_at__isnull=True,
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -214,7 +203,6 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
issue_cycle__issue__state__group="unstarted",
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
issue_cycle__issue__deleted_at__isnull=True,
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -226,7 +214,6 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
issue_cycle__issue__state__group="backlog",
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
issue_cycle__issue__deleted_at__isnull=True,
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -301,12 +288,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
.distinct()
|
||||
)
|
||||
|
||||
@allow_permission(
|
||||
[
|
||||
ROLE.ADMIN,
|
||||
ROLE.MEMBER,
|
||||
]
|
||||
)
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER])
|
||||
def get(self, request, slug, project_id, pk=None):
|
||||
if pk is None:
|
||||
queryset = (
|
||||
@@ -354,7 +336,6 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
parent__isnull=False,
|
||||
issue_cycle__cycle_id=pk,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
)
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
@@ -409,33 +390,13 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
assignee_distribution = (
|
||||
Issue.issue_objects.filter(
|
||||
issue_cycle__cycle_id=pk,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
)
|
||||
.annotate(display_name=F("assignees__display_name"))
|
||||
.annotate(assignee_id=F("assignees__id"))
|
||||
.annotate(
|
||||
avatar_url=Case(
|
||||
# If `avatar_asset` exists, use it to generate the asset URL
|
||||
When(
|
||||
assignees__avatar_asset__isnull=False,
|
||||
then=Concat(
|
||||
Value("/api/assets/v2/static/"),
|
||||
"assignees__avatar_asset", # Assuming avatar_asset has an id or relevant field
|
||||
Value("/"),
|
||||
),
|
||||
),
|
||||
# If `avatar_asset` is None, fall back to using `avatar` field directly
|
||||
When(
|
||||
assignees__avatar_asset__isnull=True,
|
||||
then="assignees__avatar",
|
||||
),
|
||||
default=Value(None),
|
||||
output_field=models.CharField(),
|
||||
)
|
||||
)
|
||||
.values("display_name", "assignee_id", "avatar_url")
|
||||
.annotate(avatar=F("assignees__avatar"))
|
||||
.values("display_name", "assignee_id", "avatar")
|
||||
.annotate(
|
||||
total_estimates=Sum(
|
||||
Cast("estimate_point__value", FloatField())
|
||||
@@ -467,7 +428,6 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
label_distribution = (
|
||||
Issue.issue_objects.filter(
|
||||
issue_cycle__cycle_id=pk,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
)
|
||||
@@ -523,39 +483,19 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
assignee_distribution = (
|
||||
Issue.issue_objects.filter(
|
||||
issue_cycle__cycle_id=pk,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
)
|
||||
.annotate(first_name=F("assignees__first_name"))
|
||||
.annotate(last_name=F("assignees__last_name"))
|
||||
.annotate(assignee_id=F("assignees__id"))
|
||||
.annotate(
|
||||
avatar_url=Case(
|
||||
# If `avatar_asset` exists, use it to generate the asset URL
|
||||
When(
|
||||
assignees__avatar_asset__isnull=False,
|
||||
then=Concat(
|
||||
Value("/api/assets/v2/static/"),
|
||||
"assignees__avatar_asset", # Assuming avatar_asset has an id or relevant field
|
||||
Value("/"),
|
||||
),
|
||||
),
|
||||
# If `avatar_asset` is None, fall back to using `avatar` field directly
|
||||
When(
|
||||
assignees__avatar_asset__isnull=True,
|
||||
then="assignees__avatar",
|
||||
),
|
||||
default=Value(None),
|
||||
output_field=models.CharField(),
|
||||
)
|
||||
)
|
||||
.annotate(avatar=F("assignees__avatar"))
|
||||
.annotate(display_name=F("assignees__display_name"))
|
||||
.values(
|
||||
"first_name",
|
||||
"last_name",
|
||||
"assignee_id",
|
||||
"avatar_url",
|
||||
"avatar",
|
||||
"display_name",
|
||||
)
|
||||
.annotate(
|
||||
@@ -594,7 +534,6 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
label_distribution = (
|
||||
Issue.issue_objects.filter(
|
||||
issue_cycle__cycle_id=pk,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
)
|
||||
@@ -660,7 +599,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
pk=cycle_id, project_id=project_id, workspace__slug=slug
|
||||
)
|
||||
|
||||
if cycle.end_date >= timezone.now():
|
||||
if cycle.end_date >= timezone.now().date():
|
||||
return Response(
|
||||
{"error": "Only completed cycles can be archived"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
|
||||
@@ -20,8 +20,7 @@ from django.db.models import (
|
||||
Sum,
|
||||
FloatField,
|
||||
)
|
||||
from django.db import models
|
||||
from django.db.models.functions import Coalesce, Cast, Concat
|
||||
from django.db.models.functions import Coalesce, Cast
|
||||
from django.utils import timezone
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
|
||||
@@ -82,7 +81,7 @@ class CycleViewSet(BaseViewSet):
|
||||
Prefetch(
|
||||
"issue_cycle__issue__assignees",
|
||||
queryset=User.objects.only(
|
||||
"avatar_asset", "first_name", "id"
|
||||
"avatar", "first_name", "id"
|
||||
).distinct(),
|
||||
)
|
||||
)
|
||||
@@ -102,7 +101,6 @@ class CycleViewSet(BaseViewSet):
|
||||
filter=Q(
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -114,7 +112,6 @@ class CycleViewSet(BaseViewSet):
|
||||
issue_cycle__issue__state__group="completed",
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -144,11 +141,6 @@ class CycleViewSet(BaseViewSet):
|
||||
distinct=True,
|
||||
filter=~Q(
|
||||
issue_cycle__issue__assignees__id__isnull=True
|
||||
)
|
||||
& (
|
||||
Q(
|
||||
issue_cycle__issue__issue_assignee__deleted_at__isnull=True
|
||||
)
|
||||
),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
@@ -158,7 +150,7 @@ class CycleViewSet(BaseViewSet):
|
||||
.distinct()
|
||||
)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST])
|
||||
def list(self, request, slug, project_id):
|
||||
queryset = self.get_queryset().filter(archived_at__isnull=True)
|
||||
cycle_view = request.GET.get("cycle_view", "all")
|
||||
@@ -195,7 +187,6 @@ class CycleViewSet(BaseViewSet):
|
||||
"completed_issues",
|
||||
"assignee_ids",
|
||||
"status",
|
||||
"version",
|
||||
"created_by",
|
||||
)
|
||||
|
||||
@@ -225,7 +216,6 @@ class CycleViewSet(BaseViewSet):
|
||||
"completed_issues",
|
||||
"assignee_ids",
|
||||
"status",
|
||||
"version",
|
||||
"created_by",
|
||||
)
|
||||
return Response(data, status=status.HTTP_200_OK)
|
||||
@@ -265,7 +255,6 @@ class CycleViewSet(BaseViewSet):
|
||||
"external_id",
|
||||
"progress_snapshot",
|
||||
"logo_props",
|
||||
"version",
|
||||
# meta fields
|
||||
"is_favorite",
|
||||
"total_issues",
|
||||
@@ -317,7 +306,10 @@ class CycleViewSet(BaseViewSet):
|
||||
|
||||
request_data = request.data
|
||||
|
||||
if cycle.end_date is not None and cycle.end_date < timezone.now():
|
||||
if (
|
||||
cycle.end_date is not None
|
||||
and cycle.end_date < timezone.now().date()
|
||||
):
|
||||
if "sort_order" in request_data:
|
||||
# Can only change sort order for a completed cycle``
|
||||
request_data = {
|
||||
@@ -355,7 +347,6 @@ class CycleViewSet(BaseViewSet):
|
||||
"external_id",
|
||||
"progress_snapshot",
|
||||
"logo_props",
|
||||
"version",
|
||||
# meta fields
|
||||
"is_favorite",
|
||||
"total_issues",
|
||||
@@ -379,12 +370,7 @@ class CycleViewSet(BaseViewSet):
|
||||
return Response(cycle, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@allow_permission(
|
||||
[
|
||||
ROLE.ADMIN,
|
||||
ROLE.MEMBER,
|
||||
]
|
||||
)
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER])
|
||||
def retrieve(self, request, slug, project_id, pk):
|
||||
queryset = (
|
||||
self.get_queryset().filter(archived_at__isnull=True).filter(pk=pk)
|
||||
@@ -398,7 +384,6 @@ class CycleViewSet(BaseViewSet):
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
parent__isnull=False,
|
||||
issue_cycle__cycle_id=pk,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
)
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
@@ -422,7 +407,6 @@ class CycleViewSet(BaseViewSet):
|
||||
"progress_snapshot",
|
||||
"sub_issues",
|
||||
"logo_props",
|
||||
"version",
|
||||
# meta fields
|
||||
"is_favorite",
|
||||
"total_issues",
|
||||
@@ -496,9 +480,12 @@ class CycleViewSet(BaseViewSet):
|
||||
notification=True,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
# TODO: Soft delete the cycle break the onetoone relationship with cycle issue
|
||||
# Delete the cycle
|
||||
cycle.delete()
|
||||
|
||||
# Delete the cycle issues
|
||||
CycleIssue.objects.filter(
|
||||
cycle_id=self.kwargs.get("pk"),
|
||||
).delete()
|
||||
# Delete the user favorite cycle
|
||||
UserFavorite.objects.filter(
|
||||
user=request.user,
|
||||
@@ -510,6 +497,7 @@ class CycleViewSet(BaseViewSet):
|
||||
|
||||
|
||||
class CycleDateCheckEndpoint(BaseAPIView):
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def post(self, request, slug, project_id):
|
||||
start_date = request.data.get("start_date", False)
|
||||
@@ -578,6 +566,7 @@ class CycleFavoriteViewSet(BaseViewSet):
|
||||
|
||||
|
||||
class TransferCycleIssueEndpoint(BaseAPIView):
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def post(self, request, slug, project_id, cycle_id):
|
||||
new_cycle_id = request.data.get("new_cycle_id", False)
|
||||
@@ -602,7 +591,6 @@ class TransferCycleIssueEndpoint(BaseAPIView):
|
||||
filter=Q(
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -613,8 +601,6 @@ class TransferCycleIssueEndpoint(BaseAPIView):
|
||||
issue_cycle__issue__state__group="completed",
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
issue_cycle__issue__deleted_at__isnull=True,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -625,8 +611,6 @@ class TransferCycleIssueEndpoint(BaseAPIView):
|
||||
issue_cycle__issue__state__group="cancelled",
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
issue_cycle__issue__deleted_at__isnull=True,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -637,8 +621,6 @@ class TransferCycleIssueEndpoint(BaseAPIView):
|
||||
issue_cycle__issue__state__group="started",
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
issue_cycle__issue__deleted_at__isnull=True,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -649,8 +631,6 @@ class TransferCycleIssueEndpoint(BaseAPIView):
|
||||
issue_cycle__issue__state__group="unstarted",
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
issue_cycle__issue__deleted_at__isnull=True,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -661,8 +641,6 @@ class TransferCycleIssueEndpoint(BaseAPIView):
|
||||
issue_cycle__issue__state__group="backlog",
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
issue_cycle__issue__deleted_at__isnull=True,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -679,33 +657,13 @@ class TransferCycleIssueEndpoint(BaseAPIView):
|
||||
assignee_estimate_data = (
|
||||
Issue.issue_objects.filter(
|
||||
issue_cycle__cycle_id=cycle_id,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
)
|
||||
.annotate(display_name=F("assignees__display_name"))
|
||||
.annotate(assignee_id=F("assignees__id"))
|
||||
.annotate(
|
||||
avatar_url=Case(
|
||||
# If `avatar_asset` exists, use it to generate the asset URL
|
||||
When(
|
||||
assignees__avatar_asset__isnull=False,
|
||||
then=Concat(
|
||||
Value("/api/assets/v2/static/"),
|
||||
"assignees__avatar_asset", # Assuming avatar_asset has an id or relevant field
|
||||
Value("/"),
|
||||
),
|
||||
),
|
||||
# If `avatar_asset` is None, fall back to using `avatar` field directly
|
||||
When(
|
||||
assignees__avatar_asset__isnull=True,
|
||||
then="assignees__avatar",
|
||||
),
|
||||
default=Value(None),
|
||||
output_field=models.CharField(),
|
||||
)
|
||||
)
|
||||
.values("display_name", "assignee_id", "avatar_url")
|
||||
.annotate(avatar=F("assignees__avatar"))
|
||||
.values("display_name", "assignee_id", "avatar")
|
||||
.annotate(
|
||||
total_estimates=Sum(
|
||||
Cast("estimate_point__value", FloatField())
|
||||
@@ -742,8 +700,7 @@ class TransferCycleIssueEndpoint(BaseAPIView):
|
||||
if item["assignee_id"]
|
||||
else None
|
||||
),
|
||||
"avatar": item.get("avatar"),
|
||||
"avatar_url": item.get("avatar_url"),
|
||||
"avatar": item["avatar"],
|
||||
"total_estimates": item["total_estimates"],
|
||||
"completed_estimates": item["completed_estimates"],
|
||||
"pending_estimates": item["pending_estimates"],
|
||||
@@ -754,7 +711,6 @@ class TransferCycleIssueEndpoint(BaseAPIView):
|
||||
label_distribution_data = (
|
||||
Issue.issue_objects.filter(
|
||||
issue_cycle__cycle_id=cycle_id,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
)
|
||||
@@ -816,33 +772,13 @@ class TransferCycleIssueEndpoint(BaseAPIView):
|
||||
assignee_distribution = (
|
||||
Issue.issue_objects.filter(
|
||||
issue_cycle__cycle_id=cycle_id,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
)
|
||||
.annotate(display_name=F("assignees__display_name"))
|
||||
.annotate(assignee_id=F("assignees__id"))
|
||||
.annotate(
|
||||
avatar_url=Case(
|
||||
# If `avatar_asset` exists, use it to generate the asset URL
|
||||
When(
|
||||
assignees__avatar_asset__isnull=False,
|
||||
then=Concat(
|
||||
Value("/api/assets/v2/static/"),
|
||||
"assignees__avatar_asset", # Assuming avatar_asset has an id or relevant field
|
||||
Value("/"),
|
||||
),
|
||||
),
|
||||
# If `avatar_asset` is None, fall back to using `avatar` field directly
|
||||
When(
|
||||
assignees__avatar_asset__isnull=True,
|
||||
then="assignees__avatar",
|
||||
),
|
||||
default=Value(None),
|
||||
output_field=models.CharField(),
|
||||
)
|
||||
)
|
||||
.values("display_name", "assignee_id", "avatar_url")
|
||||
.annotate(avatar=F("assignees__avatar"))
|
||||
.values("display_name", "assignee_id", "avatar")
|
||||
.annotate(
|
||||
total_issues=Count(
|
||||
"id",
|
||||
@@ -881,8 +817,7 @@ class TransferCycleIssueEndpoint(BaseAPIView):
|
||||
"assignee_id": (
|
||||
str(item["assignee_id"]) if item["assignee_id"] else None
|
||||
),
|
||||
"avatar": item.get("avatar"),
|
||||
"avatar_url": item.get("avatar_url"),
|
||||
"avatar": item["avatar"],
|
||||
"total_issues": item["total_issues"],
|
||||
"completed_issues": item["completed_issues"],
|
||||
"pending_issues": item["pending_issues"],
|
||||
@@ -894,7 +829,6 @@ class TransferCycleIssueEndpoint(BaseAPIView):
|
||||
label_distribution = (
|
||||
Issue.issue_objects.filter(
|
||||
issue_cycle__cycle_id=cycle_id,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
)
|
||||
@@ -988,7 +922,7 @@ class TransferCycleIssueEndpoint(BaseAPIView):
|
||||
|
||||
if (
|
||||
new_cycle.end_date is not None
|
||||
and new_cycle.end_date < timezone.now()
|
||||
and new_cycle.end_date < timezone.now().date()
|
||||
):
|
||||
return Response(
|
||||
{
|
||||
@@ -1043,7 +977,8 @@ class TransferCycleIssueEndpoint(BaseAPIView):
|
||||
|
||||
|
||||
class CycleUserPropertiesEndpoint(BaseAPIView):
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST])
|
||||
def patch(self, request, slug, project_id, cycle_id):
|
||||
cycle_properties = CycleUserProperties.objects.get(
|
||||
user=request.user,
|
||||
@@ -1066,7 +1001,7 @@ class CycleUserPropertiesEndpoint(BaseAPIView):
|
||||
serializer = CycleUserPropertiesSerializer(cycle_properties)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST])
|
||||
def get(self, request, slug, project_id, cycle_id):
|
||||
cycle_properties, _ = CycleUserProperties.objects.get_or_create(
|
||||
user=request.user,
|
||||
@@ -1079,13 +1014,14 @@ class CycleUserPropertiesEndpoint(BaseAPIView):
|
||||
|
||||
|
||||
class CycleProgressEndpoint(BaseAPIView):
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST])
|
||||
def get(self, request, slug, project_id, cycle_id):
|
||||
|
||||
aggregate_estimates = (
|
||||
Issue.issue_objects.filter(
|
||||
estimate_point__estimate__type="points",
|
||||
issue_cycle__cycle_id=cycle_id,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
)
|
||||
@@ -1138,7 +1074,6 @@ class CycleProgressEndpoint(BaseAPIView):
|
||||
|
||||
backlog_issues = Issue.issue_objects.filter(
|
||||
issue_cycle__cycle_id=cycle_id,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
state__group="backlog",
|
||||
@@ -1146,7 +1081,6 @@ class CycleProgressEndpoint(BaseAPIView):
|
||||
|
||||
unstarted_issues = Issue.issue_objects.filter(
|
||||
issue_cycle__cycle_id=cycle_id,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
state__group="unstarted",
|
||||
@@ -1154,7 +1088,6 @@ class CycleProgressEndpoint(BaseAPIView):
|
||||
|
||||
started_issues = Issue.issue_objects.filter(
|
||||
issue_cycle__cycle_id=cycle_id,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
state__group="started",
|
||||
@@ -1162,7 +1095,6 @@ class CycleProgressEndpoint(BaseAPIView):
|
||||
|
||||
cancelled_issues = Issue.issue_objects.filter(
|
||||
issue_cycle__cycle_id=cycle_id,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
state__group="cancelled",
|
||||
@@ -1170,7 +1102,6 @@ class CycleProgressEndpoint(BaseAPIView):
|
||||
|
||||
completed_issues = Issue.issue_objects.filter(
|
||||
issue_cycle__cycle_id=cycle_id,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
state__group="completed",
|
||||
@@ -1178,7 +1109,6 @@ class CycleProgressEndpoint(BaseAPIView):
|
||||
|
||||
total_issues = Issue.issue_objects.filter(
|
||||
issue_cycle__cycle_id=cycle_id,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
).count()
|
||||
@@ -1221,7 +1151,7 @@ class CycleProgressEndpoint(BaseAPIView):
|
||||
|
||||
class CycleAnalyticsEndpoint(BaseAPIView):
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST])
|
||||
def get(self, request, slug, project_id, cycle_id):
|
||||
analytic_type = request.GET.get("type", "issues")
|
||||
cycle = (
|
||||
@@ -1237,8 +1167,6 @@ class CycleAnalyticsEndpoint(BaseAPIView):
|
||||
filter=Q(
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
issue_cycle__issue__deleted_at__isnull=True,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -1266,33 +1194,13 @@ class CycleAnalyticsEndpoint(BaseAPIView):
|
||||
assignee_distribution = (
|
||||
Issue.issue_objects.filter(
|
||||
issue_cycle__cycle_id=cycle_id,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
)
|
||||
.annotate(display_name=F("assignees__display_name"))
|
||||
.annotate(assignee_id=F("assignees__id"))
|
||||
.annotate(
|
||||
avatar_url=Case(
|
||||
# If `avatar_asset` exists, use it to generate the asset URL
|
||||
When(
|
||||
assignees__avatar_asset__isnull=False,
|
||||
then=Concat(
|
||||
Value("/api/assets/v2/static/"),
|
||||
"assignees__avatar_asset", # Assuming avatar_asset has an id or relevant field
|
||||
Value("/"),
|
||||
),
|
||||
),
|
||||
# If `avatar_asset` is None, fall back to using `avatar` field directly
|
||||
When(
|
||||
assignees__avatar_asset__isnull=True,
|
||||
then="assignees__avatar",
|
||||
),
|
||||
default=Value(None),
|
||||
output_field=models.CharField(),
|
||||
)
|
||||
)
|
||||
.values("display_name", "assignee_id", "avatar_url")
|
||||
.annotate(avatar=F("assignees__avatar"))
|
||||
.values("display_name", "assignee_id", "avatar")
|
||||
.annotate(
|
||||
total_estimates=Sum(
|
||||
Cast("estimate_point__value", FloatField())
|
||||
@@ -1324,7 +1232,6 @@ class CycleAnalyticsEndpoint(BaseAPIView):
|
||||
label_distribution = (
|
||||
Issue.issue_objects.filter(
|
||||
issue_cycle__cycle_id=cycle_id,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
)
|
||||
@@ -1371,33 +1278,13 @@ class CycleAnalyticsEndpoint(BaseAPIView):
|
||||
assignee_distribution = (
|
||||
Issue.issue_objects.filter(
|
||||
issue_cycle__cycle_id=cycle_id,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
project_id=project_id,
|
||||
workspace__slug=slug,
|
||||
)
|
||||
.annotate(display_name=F("assignees__display_name"))
|
||||
.annotate(assignee_id=F("assignees__id"))
|
||||
.annotate(
|
||||
avatar_url=Case(
|
||||
# If `avatar_asset` exists, use it to generate the asset URL
|
||||
When(
|
||||
assignees__avatar_asset__isnull=False,
|
||||
then=Concat(
|
||||
Value("/api/assets/v2/static/"),
|
||||
"assignees__avatar_asset", # Assuming avatar_asset has an id or relevant field
|
||||
Value("/"),
|
||||
),
|
||||
),
|
||||
# If `avatar_asset` is None, fall back to using `avatar` field directly
|
||||
When(
|
||||
assignees__avatar_asset__isnull=True,
|
||||
then="assignees__avatar",
|
||||
),
|
||||
default=Value(None),
|
||||
output_field=models.CharField(),
|
||||
)
|
||||
)
|
||||
.values("display_name", "assignee_id", "avatar_url")
|
||||
.annotate(avatar=F("assignees__avatar"))
|
||||
.values("display_name", "assignee_id", "avatar")
|
||||
.annotate(
|
||||
total_issues=Count(
|
||||
"assignee_id",
|
||||
@@ -1430,7 +1317,6 @@ class CycleAnalyticsEndpoint(BaseAPIView):
|
||||
label_distribution = (
|
||||
Issue.issue_objects.filter(
|
||||
issue_cycle__cycle_id=cycle_id,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
project_id=project_id,
|
||||
workspace__slug=slug,
|
||||
)
|
||||
|
||||
@@ -3,7 +3,7 @@ import json
|
||||
|
||||
# Django imports
|
||||
from django.core import serializers
|
||||
from django.db.models import F, Func, OuterRef, Q, Subquery
|
||||
from django.db.models import F, Func, OuterRef, Q
|
||||
from django.utils import timezone
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.gzip import gzip_page
|
||||
@@ -12,7 +12,6 @@ from django.views.decorators.gzip import gzip_page
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
|
||||
|
||||
# Module imports
|
||||
from .. import BaseViewSet
|
||||
from plane.app.serializers import (
|
||||
@@ -23,7 +22,7 @@ from plane.db.models import (
|
||||
Cycle,
|
||||
CycleIssue,
|
||||
Issue,
|
||||
FileAsset,
|
||||
IssueAttachment,
|
||||
IssueLink,
|
||||
)
|
||||
from plane.utils.grouper import (
|
||||
@@ -40,7 +39,6 @@ from plane.utils.paginator import (
|
||||
from plane.app.permissions import allow_permission, ROLE
|
||||
|
||||
|
||||
|
||||
class CycleIssueViewSet(BaseViewSet):
|
||||
serializer_class = CycleIssueSerializer
|
||||
model = CycleIssue
|
||||
@@ -82,20 +80,12 @@ class CycleIssueViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
@method_decorator(gzip_page)
|
||||
@allow_permission(
|
||||
[
|
||||
ROLE.ADMIN,
|
||||
ROLE.MEMBER,
|
||||
]
|
||||
)
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER])
|
||||
def list(self, request, slug, project_id, cycle_id):
|
||||
order_by_param = request.GET.get("order_by", "created_at")
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
issue_queryset = (
|
||||
Issue.issue_objects.filter(
|
||||
issue_cycle__cycle_id=cycle_id,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
)
|
||||
Issue.issue_objects.filter(issue_cycle__cycle_id=cycle_id)
|
||||
.filter(project_id=project_id)
|
||||
.filter(workspace__slug=slug)
|
||||
.filter(**filters)
|
||||
@@ -107,13 +97,7 @@ class CycleIssueViewSet(BaseViewSet):
|
||||
"issue_cycle__cycle",
|
||||
)
|
||||
.filter(**filters)
|
||||
.annotate(
|
||||
cycle_id=Subquery(
|
||||
CycleIssue.objects.filter(
|
||||
issue=OuterRef("id"), deleted_at__isnull=True
|
||||
).values("cycle_id")[:1]
|
||||
)
|
||||
)
|
||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||
.annotate(
|
||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||
.order_by()
|
||||
@@ -121,9 +105,8 @@ class CycleIssueViewSet(BaseViewSet):
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
attachment_count=FileAsset.objects.filter(
|
||||
issue_id=OuterRef("id"),
|
||||
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
|
||||
attachment_count=IssueAttachment.objects.filter(
|
||||
issue=OuterRef("id")
|
||||
)
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
@@ -196,10 +179,10 @@ class CycleIssueViewSet(BaseViewSet):
|
||||
group_by_field_name=group_by,
|
||||
sub_group_by_field_name=sub_group_by,
|
||||
count_filter=Q(
|
||||
Q(issue_intake__status=1)
|
||||
| Q(issue_intake__status=-1)
|
||||
| Q(issue_intake__status=2)
|
||||
| Q(issue_intake__isnull=True),
|
||||
Q(issue_inbox__status=1)
|
||||
| Q(issue_inbox__status=-1)
|
||||
| Q(issue_inbox__status=2)
|
||||
| Q(issue_inbox__isnull=True),
|
||||
archived_at__isnull=True,
|
||||
is_draft=False,
|
||||
),
|
||||
@@ -225,10 +208,10 @@ class CycleIssueViewSet(BaseViewSet):
|
||||
),
|
||||
group_by_field_name=group_by,
|
||||
count_filter=Q(
|
||||
Q(issue_intake__status=1)
|
||||
| Q(issue_intake__status=-1)
|
||||
| Q(issue_intake__status=2)
|
||||
| Q(issue_intake__isnull=True),
|
||||
Q(issue_inbox__status=1)
|
||||
| Q(issue_inbox__status=-1)
|
||||
| Q(issue_inbox__status=2)
|
||||
| Q(issue_inbox__isnull=True),
|
||||
archived_at__isnull=True,
|
||||
is_draft=False,
|
||||
),
|
||||
@@ -258,7 +241,10 @@ class CycleIssueViewSet(BaseViewSet):
|
||||
workspace__slug=slug, project_id=project_id, pk=cycle_id
|
||||
)
|
||||
|
||||
if cycle.end_date is not None and cycle.end_date < timezone.now():
|
||||
if (
|
||||
cycle.end_date is not None
|
||||
and cycle.end_date < timezone.now().date()
|
||||
):
|
||||
return Response(
|
||||
{
|
||||
"error": "The Cycle has already been completed so no new issues can be added"
|
||||
|
||||
@@ -36,13 +36,14 @@ from plane.db.models import (
|
||||
DashboardWidget,
|
||||
Issue,
|
||||
IssueActivity,
|
||||
FileAsset,
|
||||
IssueAttachment,
|
||||
IssueLink,
|
||||
IssueRelation,
|
||||
Project,
|
||||
ProjectMember,
|
||||
User,
|
||||
Widget,
|
||||
WorkspaceMember,
|
||||
CycleIssue,
|
||||
)
|
||||
from plane.utils.issue_filters import issue_filters
|
||||
|
||||
@@ -51,6 +52,15 @@ from .. import BaseAPIView
|
||||
|
||||
|
||||
def dashboard_overview_stats(self, request, slug):
|
||||
extra_filters = {}
|
||||
if WorkspaceMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
member=request.user,
|
||||
role=5,
|
||||
is_active=True,
|
||||
).exists():
|
||||
extra_filters = {"created_by": request.user}
|
||||
|
||||
assigned_issues = (
|
||||
Issue.issue_objects.filter(
|
||||
project__project_projectmember__is_active=True,
|
||||
@@ -58,22 +68,7 @@ def dashboard_overview_stats(self, request, slug):
|
||||
workspace__slug=slug,
|
||||
assignees__in=[request.user],
|
||||
)
|
||||
.filter(
|
||||
Q(
|
||||
project__project_projectmember__role=5,
|
||||
project__guest_view_all_features=True,
|
||||
)
|
||||
| Q(
|
||||
project__project_projectmember__role=5,
|
||||
project__guest_view_all_features=False,
|
||||
created_by=self.request.user,
|
||||
)
|
||||
|
|
||||
# For other roles (role < 5), show all issues
|
||||
Q(project__project_projectmember__role__gt=5),
|
||||
project__project_projectmember__member=self.request.user,
|
||||
project__project_projectmember__is_active=True,
|
||||
)
|
||||
.filter(**extra_filters)
|
||||
.count()
|
||||
)
|
||||
|
||||
@@ -86,22 +81,7 @@ def dashboard_overview_stats(self, request, slug):
|
||||
workspace__slug=slug,
|
||||
assignees__in=[request.user],
|
||||
)
|
||||
.filter(
|
||||
Q(
|
||||
project__project_projectmember__role=5,
|
||||
project__guest_view_all_features=True,
|
||||
)
|
||||
| Q(
|
||||
project__project_projectmember__role=5,
|
||||
project__guest_view_all_features=False,
|
||||
created_by=self.request.user,
|
||||
)
|
||||
|
|
||||
# For other roles (role < 5), show all issues
|
||||
Q(project__project_projectmember__role__gt=5),
|
||||
project__project_projectmember__member=self.request.user,
|
||||
project__project_projectmember__is_active=True,
|
||||
)
|
||||
.filter(**extra_filters)
|
||||
.count()
|
||||
)
|
||||
|
||||
@@ -112,22 +92,7 @@ def dashboard_overview_stats(self, request, slug):
|
||||
project__project_projectmember__member=request.user,
|
||||
created_by_id=request.user.id,
|
||||
)
|
||||
.filter(
|
||||
Q(
|
||||
project__project_projectmember__role=5,
|
||||
project__guest_view_all_features=True,
|
||||
)
|
||||
| Q(
|
||||
project__project_projectmember__role=5,
|
||||
project__guest_view_all_features=False,
|
||||
created_by=self.request.user,
|
||||
)
|
||||
|
|
||||
# For other roles (role < 5), show all issues
|
||||
Q(project__project_projectmember__role__gt=5),
|
||||
project__project_projectmember__member=self.request.user,
|
||||
project__project_projectmember__is_active=True,
|
||||
)
|
||||
.filter(**extra_filters)
|
||||
.count()
|
||||
)
|
||||
|
||||
@@ -139,22 +104,7 @@ def dashboard_overview_stats(self, request, slug):
|
||||
assignees__in=[request.user],
|
||||
state__group="completed",
|
||||
)
|
||||
.filter(
|
||||
Q(
|
||||
project__project_projectmember__role=5,
|
||||
project__guest_view_all_features=True,
|
||||
)
|
||||
| Q(
|
||||
project__project_projectmember__role=5,
|
||||
project__guest_view_all_features=False,
|
||||
created_by=self.request.user,
|
||||
)
|
||||
|
|
||||
# For other roles (role < 5), show all issues
|
||||
Q(project__project_projectmember__role__gt=5),
|
||||
project__project_projectmember__member=self.request.user,
|
||||
project__project_projectmember__is_active=True,
|
||||
)
|
||||
.filter(**extra_filters)
|
||||
.count()
|
||||
)
|
||||
|
||||
@@ -192,13 +142,7 @@ def dashboard_assigned_issues(self, request, slug):
|
||||
).select_related("issue"),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
cycle_id=Subquery(
|
||||
CycleIssue.objects.filter(
|
||||
issue=OuterRef("id"), deleted_at__isnull=True
|
||||
).values("cycle_id")[:1]
|
||||
)
|
||||
)
|
||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||
.annotate(
|
||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||
.order_by()
|
||||
@@ -206,9 +150,8 @@ def dashboard_assigned_issues(self, request, slug):
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
attachment_count=FileAsset.objects.filter(
|
||||
issue_id=OuterRef("id"),
|
||||
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
|
||||
attachment_count=IssueAttachment.objects.filter(
|
||||
issue=OuterRef("id")
|
||||
)
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
@@ -225,10 +168,7 @@ def dashboard_assigned_issues(self, request, slug):
|
||||
ArrayAgg(
|
||||
"labels__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(labels__id__isnull=True)
|
||||
& Q(label_issue__deleted_at__isnull=True),
|
||||
),
|
||||
filter=~Q(labels__id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
@@ -236,11 +176,8 @@ def dashboard_assigned_issues(self, request, slug):
|
||||
ArrayAgg(
|
||||
"assignees__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True)
|
||||
& Q(issue_assignee__deleted_at__isnull=True)
|
||||
),
|
||||
filter=~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
@@ -248,11 +185,7 @@ def dashboard_assigned_issues(self, request, slug):
|
||||
ArrayAgg(
|
||||
"issue_module__module_id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(issue_module__module_id__isnull=True)
|
||||
& Q(issue_module__module__archived_at__isnull=True)
|
||||
& Q(issue_module__deleted_at__isnull=True)
|
||||
),
|
||||
filter=~Q(issue_module__module_id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
@@ -372,13 +305,7 @@ def dashboard_created_issues(self, request, slug):
|
||||
.filter(**filters)
|
||||
.select_related("workspace", "project", "state", "parent")
|
||||
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||
.annotate(
|
||||
cycle_id=Subquery(
|
||||
CycleIssue.objects.filter(
|
||||
issue=OuterRef("id"), deleted_at__isnull=True
|
||||
).values("cycle_id")[:1]
|
||||
)
|
||||
)
|
||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||
.annotate(
|
||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||
.order_by()
|
||||
@@ -386,9 +313,8 @@ def dashboard_created_issues(self, request, slug):
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
attachment_count=FileAsset.objects.filter(
|
||||
issue_id=OuterRef("id"),
|
||||
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
|
||||
attachment_count=IssueAttachment.objects.filter(
|
||||
issue=OuterRef("id")
|
||||
)
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
@@ -405,10 +331,7 @@ def dashboard_created_issues(self, request, slug):
|
||||
ArrayAgg(
|
||||
"labels__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(labels__id__isnull=True)
|
||||
& Q(label_issue__deleted_at__isnull=True),
|
||||
),
|
||||
filter=~Q(labels__id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
@@ -416,11 +339,8 @@ def dashboard_created_issues(self, request, slug):
|
||||
ArrayAgg(
|
||||
"assignees__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True)
|
||||
& Q(issue_assignee__deleted_at__isnull=True)
|
||||
),
|
||||
filter=~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
@@ -428,11 +348,7 @@ def dashboard_created_issues(self, request, slug):
|
||||
ArrayAgg(
|
||||
"issue_module__module_id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(issue_module__module_id__isnull=True)
|
||||
& Q(issue_module__module__archived_at__isnull=True)
|
||||
& Q(issue_module__deleted_at__isnull=True)
|
||||
),
|
||||
filter=~Q(issue_module__module_id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import random
|
||||
import string
|
||||
import json
|
||||
|
||||
# Django imports
|
||||
from django.utils import timezone
|
||||
|
||||
# Third party imports
|
||||
from rest_framework.response import Response
|
||||
@@ -23,7 +19,6 @@ from plane.app.serializers import (
|
||||
EstimateReadSerializer,
|
||||
)
|
||||
from plane.utils.cache import invalidate_cache
|
||||
from plane.bgtasks.issue_activities_task import issue_activity
|
||||
|
||||
|
||||
def generate_random_name(length=10):
|
||||
@@ -33,12 +28,7 @@ def generate_random_name(length=10):
|
||||
|
||||
class ProjectEstimatePointEndpoint(BaseAPIView):
|
||||
|
||||
@allow_permission(
|
||||
[
|
||||
ROLE.ADMIN,
|
||||
ROLE.MEMBER,
|
||||
]
|
||||
)
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER])
|
||||
def get(self, request, slug, project_id):
|
||||
project = Project.objects.get(workspace__slug=slug, pk=project_id)
|
||||
if project.estimate_id is not None:
|
||||
@@ -254,66 +244,11 @@ class EstimatePointEndpoint(BaseViewSet):
|
||||
)
|
||||
# update all the issues with the new estimate
|
||||
if new_estimate_id:
|
||||
issues = Issue.objects.filter(
|
||||
_ = Issue.objects.filter(
|
||||
project_id=project_id,
|
||||
workspace__slug=slug,
|
||||
estimate_point_id=estimate_point_id,
|
||||
)
|
||||
for issue in issues:
|
||||
issue_activity.delay(
|
||||
type="issue.activity.updated",
|
||||
requested_data=json.dumps(
|
||||
{
|
||||
"estimate_point": (
|
||||
str(new_estimate_id)
|
||||
if new_estimate_id
|
||||
else None
|
||||
),
|
||||
}
|
||||
),
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=issue.id,
|
||||
project_id=str(project_id),
|
||||
current_instance=json.dumps(
|
||||
{
|
||||
"estimate_point": (
|
||||
str(issue.estimate_point_id)
|
||||
if issue.estimate_point_id
|
||||
else None
|
||||
),
|
||||
}
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
)
|
||||
issues.update(estimate_point_id=new_estimate_id)
|
||||
else:
|
||||
issues = Issue.objects.filter(
|
||||
project_id=project_id,
|
||||
workspace__slug=slug,
|
||||
estimate_point_id=estimate_point_id,
|
||||
)
|
||||
for issue in issues:
|
||||
issue_activity.delay(
|
||||
type="issue.activity.updated",
|
||||
requested_data=json.dumps(
|
||||
{
|
||||
"estimate_point": None,
|
||||
}
|
||||
),
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=issue.id,
|
||||
project_id=str(project_id),
|
||||
current_instance=json.dumps(
|
||||
{
|
||||
"estimate_point": (
|
||||
str(issue.estimate_point_id)
|
||||
if issue.estimate_point_id
|
||||
else None
|
||||
),
|
||||
}
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
)
|
||||
).update(estimate_point_id=new_estimate_id)
|
||||
|
||||
# delete the estimate point
|
||||
old_estimate_point = EstimatePoint.objects.filter(
|
||||
|
||||
+120
-183
@@ -3,7 +3,7 @@ import json
|
||||
|
||||
# Django import
|
||||
from django.utils import timezone
|
||||
from django.db.models import Q, Count, OuterRef, Func, F, Prefetch, Subquery
|
||||
from django.db.models import Q, Count, OuterRef, Func, F, Prefetch
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.contrib.postgres.aggregates import ArrayAgg
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
@@ -18,31 +18,30 @@ from rest_framework.response import Response
|
||||
from ..base import BaseViewSet
|
||||
from plane.app.permissions import allow_permission, ROLE
|
||||
from plane.db.models import (
|
||||
Intake,
|
||||
IntakeIssue,
|
||||
Inbox,
|
||||
InboxIssue,
|
||||
Issue,
|
||||
State,
|
||||
IssueLink,
|
||||
FileAsset,
|
||||
IssueAttachment,
|
||||
Project,
|
||||
ProjectMember,
|
||||
CycleIssue,
|
||||
)
|
||||
from plane.app.serializers import (
|
||||
IssueCreateSerializer,
|
||||
IssueSerializer,
|
||||
IntakeSerializer,
|
||||
IntakeIssueSerializer,
|
||||
IntakeIssueDetailSerializer,
|
||||
InboxSerializer,
|
||||
InboxIssueSerializer,
|
||||
InboxIssueDetailSerializer,
|
||||
)
|
||||
from plane.utils.issue_filters import issue_filters
|
||||
from plane.bgtasks.issue_activities_task import issue_activity
|
||||
|
||||
|
||||
class IntakeViewSet(BaseViewSet):
|
||||
class InboxViewSet(BaseViewSet):
|
||||
|
||||
serializer_class = IntakeSerializer
|
||||
model = Intake
|
||||
serializer_class = InboxSerializer
|
||||
model = Inbox
|
||||
|
||||
def get_queryset(self):
|
||||
return (
|
||||
@@ -54,8 +53,8 @@ class IntakeViewSet(BaseViewSet):
|
||||
)
|
||||
.annotate(
|
||||
pending_issue_count=Count(
|
||||
"issue_intake",
|
||||
filter=Q(issue_intake__status=-2),
|
||||
"issue_inbox",
|
||||
filter=Q(issue_inbox__status=-2),
|
||||
)
|
||||
)
|
||||
.select_related("workspace", "project")
|
||||
@@ -63,9 +62,9 @@ class IntakeViewSet(BaseViewSet):
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def list(self, request, slug, project_id):
|
||||
intake = self.get_queryset().first()
|
||||
inbox = self.get_queryset().first()
|
||||
return Response(
|
||||
IntakeSerializer(intake).data,
|
||||
InboxSerializer(inbox).data,
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
@@ -75,26 +74,26 @@ class IntakeViewSet(BaseViewSet):
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def destroy(self, request, slug, project_id, pk):
|
||||
intake = Intake.objects.filter(
|
||||
inbox = Inbox.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id, pk=pk
|
||||
).first()
|
||||
# Handle default intake delete
|
||||
if intake.is_default:
|
||||
# Handle default inbox delete
|
||||
if inbox.is_default:
|
||||
return Response(
|
||||
{"error": "You cannot delete the default intake"},
|
||||
{"error": "You cannot delete the default inbox"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
intake.delete()
|
||||
inbox.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
class IntakeIssueViewSet(BaseViewSet):
|
||||
class InboxIssueViewSet(BaseViewSet):
|
||||
|
||||
serializer_class = IntakeIssueSerializer
|
||||
model = IntakeIssue
|
||||
serializer_class = InboxIssueSerializer
|
||||
model = InboxIssue
|
||||
|
||||
filterset_fields = [
|
||||
"statulls",
|
||||
"status",
|
||||
]
|
||||
|
||||
def get_queryset(self):
|
||||
@@ -107,19 +106,13 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"issue_intake",
|
||||
queryset=IntakeIssue.objects.only(
|
||||
"issue_inbox",
|
||||
queryset=InboxIssue.objects.only(
|
||||
"status", "duplicate_to", "snoozed_till", "source"
|
||||
),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
cycle_id=Subquery(
|
||||
CycleIssue.objects.filter(
|
||||
issue=OuterRef("id"), deleted_at__isnull=True
|
||||
).values("cycle_id")[:1]
|
||||
)
|
||||
)
|
||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||
.annotate(
|
||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||
.order_by()
|
||||
@@ -127,9 +120,8 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
attachment_count=FileAsset.objects.filter(
|
||||
issue_id=OuterRef("id"),
|
||||
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
|
||||
attachment_count=IssueAttachment.objects.filter(
|
||||
issue=OuterRef("id")
|
||||
)
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
@@ -148,10 +140,7 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
ArrayAgg(
|
||||
"labels__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(labels__id__isnull=True)
|
||||
& Q(label_issue__deleted_at__isnull=True),
|
||||
),
|
||||
filter=~Q(labels__id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
@@ -159,11 +148,8 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
ArrayAgg(
|
||||
"assignees__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True)
|
||||
& Q(issue_assignee__deleted_at__isnull=True)
|
||||
),
|
||||
filter=~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
@@ -171,27 +157,23 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
ArrayAgg(
|
||||
"issue_module__module_id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(issue_module__module_id__isnull=True)
|
||||
& Q(issue_module__module__archived_at__isnull=True)
|
||||
& Q(issue_module__deleted_at__isnull=True)
|
||||
),
|
||||
filter=~Q(issue_module__module_id__isnull=True)
|
||||
& Q(issue_module__module__archived_at__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
)
|
||||
).distinct()
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST])
|
||||
def list(self, request, slug, project_id):
|
||||
intake_id = Intake.objects.filter(
|
||||
inbox_id = Inbox.objects.get(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
).first()
|
||||
project = Project.objects.get(pk=project_id)
|
||||
)
|
||||
filters = issue_filters(request.GET, "GET", "issue__")
|
||||
intake_issue = (
|
||||
IntakeIssue.objects.filter(
|
||||
intake_id=intake_id.id, project_id=project_id, **filters
|
||||
inbox_issue = (
|
||||
InboxIssue.objects.filter(
|
||||
inbox_id=inbox_id.id, project_id=project_id, **filters
|
||||
)
|
||||
.select_related("issue")
|
||||
.prefetch_related(
|
||||
@@ -202,40 +184,34 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
ArrayAgg(
|
||||
"issue__labels__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(issue__labels__id__isnull=True)
|
||||
& Q(issue__label_issue__deleted_at__isnull=True)
|
||||
),
|
||||
filter=~Q(issue__labels__id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
)
|
||||
)
|
||||
).order_by(request.GET.get("order_by", "-issue__created_at"))
|
||||
# Intake status filter
|
||||
intake_status = [
|
||||
# inbox status filter
|
||||
inbox_status = [
|
||||
item
|
||||
for item in request.GET.get("status", "-2").split(",")
|
||||
if item != "null"
|
||||
]
|
||||
if intake_status:
|
||||
intake_issue = intake_issue.filter(status__in=intake_status)
|
||||
if inbox_status:
|
||||
inbox_issue = inbox_issue.filter(status__in=inbox_status)
|
||||
|
||||
if (
|
||||
ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
member=request.user,
|
||||
role=5,
|
||||
is_active=True,
|
||||
).exists()
|
||||
and not project.guest_view_all_features
|
||||
):
|
||||
intake_issue = intake_issue.filter(created_by=request.user)
|
||||
if ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
member=request.user,
|
||||
role=5,
|
||||
is_active=True,
|
||||
).exists():
|
||||
inbox_issue = inbox_issue.filter(created_by=request.user)
|
||||
return self.paginate(
|
||||
request=request,
|
||||
queryset=(intake_issue),
|
||||
on_results=lambda intake_issues: IntakeIssueSerializer(
|
||||
intake_issues,
|
||||
queryset=(inbox_issue),
|
||||
on_results=lambda inbox_issues: InboxIssueSerializer(
|
||||
inbox_issues,
|
||||
many=True,
|
||||
).data,
|
||||
)
|
||||
@@ -261,6 +237,16 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Create or get state
|
||||
state, _ = State.objects.get_or_create(
|
||||
name="Triage",
|
||||
group="triage",
|
||||
description="Default state for managing all Inbox Issues",
|
||||
project_id=project_id,
|
||||
color="#ff7700",
|
||||
is_triage=True,
|
||||
)
|
||||
|
||||
# create an issue
|
||||
project = Project.objects.get(pk=project_id)
|
||||
serializer = IssueCreateSerializer(
|
||||
@@ -273,15 +259,15 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
intake_id = Intake.objects.filter(
|
||||
inbox_id = Inbox.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
).first()
|
||||
# create an intake issue
|
||||
intake_issue = IntakeIssue.objects.create(
|
||||
intake_id=intake_id.id,
|
||||
# create an inbox issue
|
||||
inbox_issue = InboxIssue.objects.create(
|
||||
inbox_id=inbox_id.id,
|
||||
project_id=project_id,
|
||||
issue_id=serializer.data["id"],
|
||||
source=request.data.get("source", "IN-APP"),
|
||||
source=request.data.get("source", "in-app"),
|
||||
)
|
||||
# Create an Issue Activity
|
||||
issue_activity.delay(
|
||||
@@ -294,10 +280,10 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
intake=str(intake_issue.id),
|
||||
inbox=str(inbox_issue.id),
|
||||
)
|
||||
intake_issue = (
|
||||
IntakeIssue.objects.select_related("issue")
|
||||
inbox_issue = (
|
||||
InboxIssue.objects.select_related("issue")
|
||||
.prefetch_related(
|
||||
"issue__labels",
|
||||
"issue__assignees",
|
||||
@@ -307,12 +293,7 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
ArrayAgg(
|
||||
"issue__labels__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(issue__labels__id__isnull=True)
|
||||
& Q(
|
||||
issue__label_issue__deleted_at__isnull=True
|
||||
)
|
||||
),
|
||||
filter=~Q(issue__labels__id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
@@ -320,37 +301,34 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
ArrayAgg(
|
||||
"issue__assignees__id",
|
||||
distinct=True,
|
||||
filter=~Q(issue__assignees__id__isnull=True)
|
||||
& Q(
|
||||
issue__assignees__member_project__is_active=True
|
||||
),
|
||||
filter=~Q(issue__assignees__id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
)
|
||||
.get(
|
||||
intake_id=intake_id.id,
|
||||
inbox_id=inbox_id.id,
|
||||
issue_id=serializer.data["id"],
|
||||
project_id=project_id,
|
||||
)
|
||||
)
|
||||
serializer = IntakeIssueDetailSerializer(intake_issue)
|
||||
serializer = InboxIssueDetailSerializer(inbox_issue)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
else:
|
||||
return Response(
|
||||
serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN], creator=True, model=Issue)
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
def partial_update(self, request, slug, project_id, pk):
|
||||
intake_id = Intake.objects.filter(
|
||||
inbox_id = Inbox.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
).first()
|
||||
intake_issue = IntakeIssue.objects.get(
|
||||
inbox_issue = InboxIssue.objects.get(
|
||||
issue_id=pk,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
intake_id=intake_id,
|
||||
inbox_id=inbox_id,
|
||||
)
|
||||
# Get the project member
|
||||
project_member = ProjectMember.objects.get(
|
||||
@@ -360,11 +338,11 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
is_active=True,
|
||||
)
|
||||
# Only project members admins and created_by users can access this endpoint
|
||||
if project_member.role <= 5 and str(intake_issue.created_by_id) != str(
|
||||
if project_member.role <= 10 and str(inbox_issue.created_by_id) != str(
|
||||
request.user.id
|
||||
):
|
||||
return Response(
|
||||
{"error": "You cannot edit intake issues"},
|
||||
{"error": "You cannot edit inbox issues"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@@ -376,10 +354,7 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
ArrayAgg(
|
||||
"labels__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(labels__id__isnull=True)
|
||||
& Q(label_issue__deleted_at__isnull=True)
|
||||
),
|
||||
filter=~Q(labels__id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
@@ -387,20 +362,18 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
ArrayAgg(
|
||||
"assignees__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(assignees__id__isnull=True)
|
||||
& Q(issue_assignee__deleted_at__isnull=True)
|
||||
),
|
||||
filter=~Q(assignees__id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
).get(
|
||||
pk=intake_issue.issue_id,
|
||||
pk=inbox_issue.issue_id,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
)
|
||||
# Only allow guests to edit name and description
|
||||
if project_member.role <= 5:
|
||||
# Only allow guests and viewers to edit name and description
|
||||
if project_member.role <= 10:
|
||||
# viewers and guests since only viewers and guests
|
||||
issue_data = {
|
||||
"name": issue_data.get("name", issue.name),
|
||||
"description_html": issue_data.get(
|
||||
@@ -433,7 +406,7 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
intake=str(intake_issue.id),
|
||||
inbox=str(inbox_issue.id),
|
||||
)
|
||||
issue_serializer.save()
|
||||
else:
|
||||
@@ -441,20 +414,20 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
issue_serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
# Only project admins and members can edit intake issue attributes
|
||||
if project_member.role > 15:
|
||||
serializer = IntakeIssueSerializer(
|
||||
intake_issue, data=request.data, partial=True
|
||||
# Only project admins and members can edit inbox issue attributes
|
||||
if project_member.role > 10:
|
||||
serializer = InboxIssueSerializer(
|
||||
inbox_issue, data=request.data, partial=True
|
||||
)
|
||||
current_instance = json.dumps(
|
||||
IntakeIssueSerializer(intake_issue).data, cls=DjangoJSONEncoder
|
||||
InboxIssueSerializer(inbox_issue).data, cls=DjangoJSONEncoder
|
||||
)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
# Update the issue state if the issue is rejected or marked as duplicate
|
||||
if serializer.data["status"] in [-1, 2]:
|
||||
issue = Issue.objects.get(
|
||||
pk=intake_issue.issue_id,
|
||||
pk=inbox_issue.issue_id,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
)
|
||||
@@ -470,7 +443,7 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
# Update the issue state if it is accepted
|
||||
if serializer.data["status"] in [1]:
|
||||
issue = Issue.objects.get(
|
||||
pk=intake_issue.issue_id,
|
||||
pk=inbox_issue.issue_id,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
)
|
||||
@@ -488,7 +461,7 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
issue.save()
|
||||
# create a activity for status change
|
||||
issue_activity.delay(
|
||||
type="intake.activity.created",
|
||||
type="inbox.activity.created",
|
||||
requested_data=json.dumps(
|
||||
request.data, cls=DjangoJSONEncoder
|
||||
),
|
||||
@@ -499,11 +472,11 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=False,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
intake=(intake_issue.id),
|
||||
inbox=(inbox_issue.id),
|
||||
)
|
||||
|
||||
intake_issue = (
|
||||
IntakeIssue.objects.select_related("issue")
|
||||
inbox_issue = (
|
||||
InboxIssue.objects.select_related("issue")
|
||||
.prefetch_related(
|
||||
"issue__labels",
|
||||
"issue__assignees",
|
||||
@@ -513,12 +486,7 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
ArrayAgg(
|
||||
"issue__labels__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(issue__labels__id__isnull=True)
|
||||
& Q(
|
||||
issue__label_issue__deleted_at__isnull=True
|
||||
)
|
||||
),
|
||||
filter=~Q(issue__labels__id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
@@ -526,47 +494,37 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
ArrayAgg(
|
||||
"issue__assignees__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(issue__assignees__id__isnull=True)
|
||||
& Q(
|
||||
issue__issue_assignee__deleted_at__isnull=True
|
||||
)
|
||||
),
|
||||
filter=~Q(issue__assignees__id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
)
|
||||
.get(
|
||||
intake_id=intake_id.id,
|
||||
inbox_id=inbox_id.id,
|
||||
issue_id=pk,
|
||||
project_id=project_id,
|
||||
)
|
||||
)
|
||||
serializer = IntakeIssueDetailSerializer(intake_issue).data
|
||||
serializer = InboxIssueDetailSerializer(inbox_issue).data
|
||||
return Response(serializer, status=status.HTTP_200_OK)
|
||||
return Response(
|
||||
serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
else:
|
||||
serializer = IntakeIssueDetailSerializer(intake_issue).data
|
||||
serializer = InboxIssueDetailSerializer(inbox_issue).data
|
||||
return Response(serializer, status=status.HTTP_200_OK)
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[
|
||||
ROLE.ADMIN,
|
||||
ROLE.MEMBER,
|
||||
ROLE.GUEST,
|
||||
],
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER],
|
||||
creator=True,
|
||||
model=Issue,
|
||||
)
|
||||
def retrieve(self, request, slug, project_id, pk):
|
||||
intake_id = Intake.objects.filter(
|
||||
inbox_id = Inbox.objects.get(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
).first()
|
||||
project = Project.objects.get(pk=project_id)
|
||||
intake_issue = (
|
||||
IntakeIssue.objects.select_related("issue")
|
||||
)
|
||||
inbox_issue = (
|
||||
InboxIssue.objects.select_related("issue")
|
||||
.prefetch_related(
|
||||
"issue__labels",
|
||||
"issue__assignees",
|
||||
@@ -576,10 +534,7 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
ArrayAgg(
|
||||
"issue__labels__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(issue__labels__id__isnull=True)
|
||||
& Q(issue__label_issue__deleted_at__isnull=True)
|
||||
),
|
||||
filter=~Q(issue__labels__id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
@@ -587,32 +542,14 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
ArrayAgg(
|
||||
"issue__assignees__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(issue__assignees__id__isnull=True)
|
||||
& Q(issue__issue_assignee__deleted_at__isnull=True)
|
||||
),
|
||||
filter=~Q(issue__assignees__id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
)
|
||||
.get(intake_id=intake_id.id, issue_id=pk, project_id=project_id)
|
||||
.get(inbox_id=inbox_id.id, issue_id=pk, project_id=project_id)
|
||||
)
|
||||
if (
|
||||
ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
member=request.user,
|
||||
role=5,
|
||||
is_active=True,
|
||||
).exists()
|
||||
and not project.guest_view_all_features
|
||||
and not intake_issue.created_by == request.user
|
||||
):
|
||||
return Response(
|
||||
{"error": "You are not allowed to view this issue"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
issue = IntakeIssueDetailSerializer(intake_issue).data
|
||||
issue = InboxIssueDetailSerializer(inbox_issue).data
|
||||
return Response(
|
||||
issue,
|
||||
status=status.HTTP_200_OK,
|
||||
@@ -620,23 +557,23 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN], creator=True, model=Issue)
|
||||
def destroy(self, request, slug, project_id, pk):
|
||||
intake_id = Intake.objects.filter(
|
||||
inbox_id = Inbox.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
).first()
|
||||
intake_issue = IntakeIssue.objects.get(
|
||||
inbox_issue = InboxIssue.objects.get(
|
||||
issue_id=pk,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
intake_id=intake_id,
|
||||
inbox_id=inbox_id,
|
||||
)
|
||||
|
||||
# Check the issue status
|
||||
if intake_issue.status in [-2, -1, 0, 2]:
|
||||
if inbox_issue.status in [-2, -1, 0, 2]:
|
||||
# Delete the issue also
|
||||
issue = Issue.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id, pk=pk
|
||||
).first()
|
||||
issue.delete()
|
||||
|
||||
intake_issue.delete()
|
||||
inbox_issue.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
@@ -19,11 +19,7 @@ from plane.app.serializers import (
|
||||
IssueActivitySerializer,
|
||||
IssueCommentSerializer,
|
||||
)
|
||||
from plane.app.permissions import (
|
||||
ProjectEntityPermission,
|
||||
allow_permission,
|
||||
ROLE,
|
||||
)
|
||||
from plane.app.permissions import ProjectEntityPermission, allow_permission, ROLE
|
||||
from plane.db.models import (
|
||||
IssueActivity,
|
||||
IssueComment,
|
||||
@@ -37,13 +33,7 @@ class IssueActivityEndpoint(BaseAPIView):
|
||||
]
|
||||
|
||||
@method_decorator(gzip_page)
|
||||
@allow_permission(
|
||||
[
|
||||
ROLE.ADMIN,
|
||||
ROLE.MEMBER,
|
||||
ROLE.GUEST,
|
||||
]
|
||||
)
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST, ROLE.VIEWER])
|
||||
def get(self, request, slug, project_id, issue_id):
|
||||
filters = {}
|
||||
if request.GET.get("created_at__gt", None) is not None:
|
||||
|
||||
@@ -3,7 +3,14 @@ import json
|
||||
|
||||
# Django imports
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.db.models import F, Func, OuterRef, Q, Prefetch, Exists, Subquery
|
||||
from django.db.models import (
|
||||
F,
|
||||
Func,
|
||||
OuterRef,
|
||||
Q,
|
||||
Prefetch,
|
||||
Exists,
|
||||
)
|
||||
from django.utils import timezone
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.gzip import gzip_page
|
||||
@@ -23,11 +30,10 @@ from plane.app.serializers import (
|
||||
from plane.bgtasks.issue_activities_task import issue_activity
|
||||
from plane.db.models import (
|
||||
Issue,
|
||||
FileAsset,
|
||||
IssueAttachment,
|
||||
IssueLink,
|
||||
IssueSubscriber,
|
||||
IssueReaction,
|
||||
CycleIssue
|
||||
)
|
||||
from plane.utils.grouper import (
|
||||
issue_group_values,
|
||||
@@ -65,13 +71,7 @@ class IssueArchiveViewSet(BaseViewSet):
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.select_related("workspace", "project", "state", "parent")
|
||||
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||
.annotate(
|
||||
cycle_id=Subquery(
|
||||
CycleIssue.objects.filter(
|
||||
issue=OuterRef("id"), deleted_at__isnull=True
|
||||
).values("cycle_id")[:1]
|
||||
)
|
||||
)
|
||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||
.annotate(
|
||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||
.order_by()
|
||||
@@ -79,9 +79,8 @@ class IssueArchiveViewSet(BaseViewSet):
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
attachment_count=FileAsset.objects.filter(
|
||||
issue_id=OuterRef("id"),
|
||||
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
|
||||
attachment_count=IssueAttachment.objects.filter(
|
||||
issue=OuterRef("id")
|
||||
)
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
@@ -98,12 +97,7 @@ class IssueArchiveViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
@method_decorator(gzip_page)
|
||||
@allow_permission(
|
||||
[
|
||||
ROLE.ADMIN,
|
||||
ROLE.MEMBER,
|
||||
]
|
||||
)
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER])
|
||||
def list(self, request, slug, project_id):
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
show_sub_issues = request.GET.get("show_sub_issues", "true")
|
||||
@@ -171,10 +165,10 @@ class IssueArchiveViewSet(BaseViewSet):
|
||||
group_by_field_name=group_by,
|
||||
sub_group_by_field_name=sub_group_by,
|
||||
count_filter=Q(
|
||||
Q(issue_intake__status=1)
|
||||
| Q(issue_intake__status=-1)
|
||||
| Q(issue_intake__status=2)
|
||||
| Q(issue_intake__isnull=True),
|
||||
Q(issue_inbox__status=1)
|
||||
| Q(issue_inbox__status=-1)
|
||||
| Q(issue_inbox__status=2)
|
||||
| Q(issue_inbox__isnull=True),
|
||||
archived_at__isnull=True,
|
||||
is_draft=False,
|
||||
),
|
||||
@@ -200,10 +194,10 @@ class IssueArchiveViewSet(BaseViewSet):
|
||||
),
|
||||
group_by_field_name=group_by,
|
||||
count_filter=Q(
|
||||
Q(issue_intake__status=1)
|
||||
| Q(issue_intake__status=-1)
|
||||
| Q(issue_intake__status=2)
|
||||
| Q(issue_intake__isnull=True),
|
||||
Q(issue_inbox__status=1)
|
||||
| Q(issue_inbox__status=-1)
|
||||
| Q(issue_inbox__status=2)
|
||||
| Q(issue_inbox__isnull=True),
|
||||
archived_at__isnull=True,
|
||||
is_draft=False,
|
||||
),
|
||||
@@ -219,12 +213,7 @@ class IssueArchiveViewSet(BaseViewSet):
|
||||
),
|
||||
)
|
||||
|
||||
@allow_permission(
|
||||
[
|
||||
ROLE.ADMIN,
|
||||
ROLE.MEMBER,
|
||||
]
|
||||
)
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER])
|
||||
def retrieve(self, request, slug, project_id, pk=None):
|
||||
issue = (
|
||||
self.get_queryset()
|
||||
@@ -237,6 +226,12 @@ class IssueArchiveViewSet(BaseViewSet):
|
||||
),
|
||||
)
|
||||
)
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"issue_attachment",
|
||||
queryset=IssueAttachment.objects.select_related("issue"),
|
||||
)
|
||||
)
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"issue_link",
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
# Python imports
|
||||
import json
|
||||
import uuid
|
||||
|
||||
# Django imports
|
||||
from django.utils import timezone
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.conf import settings
|
||||
from django.http import HttpResponseRedirect
|
||||
|
||||
# Third Party imports
|
||||
from rest_framework.response import Response
|
||||
@@ -16,29 +13,21 @@ from rest_framework.parsers import MultiPartParser, FormParser
|
||||
# Module imports
|
||||
from .. import BaseAPIView
|
||||
from plane.app.serializers import IssueAttachmentSerializer
|
||||
from plane.db.models import FileAsset, Workspace
|
||||
from plane.db.models import IssueAttachment
|
||||
from plane.bgtasks.issue_activities_task import issue_activity
|
||||
from plane.app.permissions import allow_permission, ROLE
|
||||
from plane.settings.storage import S3Storage
|
||||
from plane.bgtasks.storage_metadata_task import get_asset_object_metadata
|
||||
|
||||
|
||||
class IssueAttachmentEndpoint(BaseAPIView):
|
||||
serializer_class = IssueAttachmentSerializer
|
||||
model = FileAsset
|
||||
model = IssueAttachment
|
||||
parser_classes = (MultiPartParser, FormParser)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
def post(self, request, slug, project_id, issue_id):
|
||||
serializer = IssueAttachmentSerializer(data=request.data)
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
if serializer.is_valid():
|
||||
serializer.save(
|
||||
project_id=project_id,
|
||||
issue_id=issue_id,
|
||||
workspace_id=workspace.id,
|
||||
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
|
||||
)
|
||||
serializer.save(project_id=project_id, issue_id=issue_id)
|
||||
issue_activity.delay(
|
||||
type="attachment.activity.created",
|
||||
requested_data=None,
|
||||
@@ -56,9 +45,9 @@ class IssueAttachmentEndpoint(BaseAPIView):
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@allow_permission([ROLE.ADMIN], creator=True, model=FileAsset)
|
||||
@allow_permission([ROLE.ADMIN], creator=True, model=IssueAttachment)
|
||||
def delete(self, request, slug, project_id, issue_id, pk):
|
||||
issue_attachment = FileAsset.objects.get(pk=pk)
|
||||
issue_attachment = IssueAttachment.objects.get(pk=pk)
|
||||
issue_attachment.asset.delete(save=False)
|
||||
issue_attachment.delete()
|
||||
issue_activity.delay(
|
||||
@@ -75,188 +64,10 @@ class IssueAttachmentEndpoint(BaseAPIView):
|
||||
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@allow_permission(
|
||||
[
|
||||
ROLE.ADMIN,
|
||||
ROLE.MEMBER,
|
||||
ROLE.GUEST,
|
||||
]
|
||||
)
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST, ROLE.VIEWER])
|
||||
def get(self, request, slug, project_id, issue_id):
|
||||
issue_attachments = FileAsset.objects.filter(
|
||||
issue_attachments = IssueAttachment.objects.filter(
|
||||
issue_id=issue_id, workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
serializer = IssueAttachmentSerializer(issue_attachments, many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class IssueAttachmentV2Endpoint(BaseAPIView):
|
||||
|
||||
serializer_class = IssueAttachmentSerializer
|
||||
model = FileAsset
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
def post(self, request, slug, project_id, issue_id):
|
||||
name = request.data.get("name")
|
||||
type = request.data.get("type", False)
|
||||
size = int(request.data.get("size", settings.FILE_SIZE_LIMIT))
|
||||
|
||||
if not type or type not in settings.ATTACHMENT_MIME_TYPES:
|
||||
return Response(
|
||||
{
|
||||
"error": "Invalid file type.",
|
||||
"status": False,
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Get the workspace
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
|
||||
# asset key
|
||||
asset_key = f"{workspace.id}/{uuid.uuid4().hex}-{name}"
|
||||
|
||||
# Get the size limit
|
||||
size_limit = min(size, settings.FILE_SIZE_LIMIT)
|
||||
|
||||
# Create a File Asset
|
||||
asset = FileAsset.objects.create(
|
||||
attributes={
|
||||
"name": name,
|
||||
"type": type,
|
||||
"size": size_limit,
|
||||
},
|
||||
asset=asset_key,
|
||||
size=size_limit,
|
||||
workspace_id=workspace.id,
|
||||
created_by=request.user,
|
||||
issue_id=issue_id,
|
||||
project_id=project_id,
|
||||
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
|
||||
)
|
||||
|
||||
# Get the presigned URL
|
||||
storage = S3Storage(request=request)
|
||||
# Generate a presigned URL to share an S3 object
|
||||
presigned_url = storage.generate_presigned_post(
|
||||
object_name=asset_key,
|
||||
file_type=type,
|
||||
file_size=size_limit,
|
||||
)
|
||||
# Return the presigned URL
|
||||
return Response(
|
||||
{
|
||||
"upload_data": presigned_url,
|
||||
"asset_id": str(asset.id),
|
||||
"attachment": IssueAttachmentSerializer(asset).data,
|
||||
"asset_url": asset.asset_url,
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
@allow_permission([ROLE.ADMIN], creator=True, model=FileAsset)
|
||||
def delete(self, request, slug, project_id, issue_id, pk):
|
||||
issue_attachment = FileAsset.objects.get(
|
||||
pk=pk, workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
issue_attachment.is_deleted = True
|
||||
issue_attachment.deleted_at = timezone.now()
|
||||
issue_attachment.save()
|
||||
|
||||
issue_activity.delay(
|
||||
type="attachment.activity.deleted",
|
||||
requested_data=None,
|
||||
actor_id=str(self.request.user.id),
|
||||
issue_id=str(issue_id),
|
||||
project_id=str(project_id),
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@allow_permission(
|
||||
[
|
||||
ROLE.ADMIN,
|
||||
ROLE.MEMBER,
|
||||
ROLE.GUEST,
|
||||
]
|
||||
)
|
||||
def get(self, request, slug, project_id, issue_id, pk=None):
|
||||
if pk:
|
||||
# Get the asset
|
||||
asset = FileAsset.objects.get(
|
||||
id=pk, workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
|
||||
# Check if the asset is uploaded
|
||||
if not asset.is_uploaded:
|
||||
return Response(
|
||||
{
|
||||
"error": "The asset is not uploaded.",
|
||||
"status": False,
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
storage = S3Storage(request=request)
|
||||
presigned_url = storage.generate_presigned_url(
|
||||
object_name=asset.asset.name,
|
||||
disposition="attachment",
|
||||
filename=asset.attributes.get("name"),
|
||||
)
|
||||
return HttpResponseRedirect(presigned_url)
|
||||
|
||||
# Get all the attachments
|
||||
issue_attachments = FileAsset.objects.filter(
|
||||
issue_id=issue_id,
|
||||
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
is_uploaded=True,
|
||||
)
|
||||
# Serialize the attachments
|
||||
serializer = IssueAttachmentSerializer(issue_attachments, many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@allow_permission(
|
||||
[
|
||||
ROLE.ADMIN,
|
||||
ROLE.MEMBER,
|
||||
ROLE.GUEST,
|
||||
]
|
||||
)
|
||||
def patch(self, request, slug, project_id, issue_id, pk):
|
||||
issue_attachment = FileAsset.objects.get(
|
||||
pk=pk, workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
serializer = IssueAttachmentSerializer(issue_attachment)
|
||||
|
||||
# Send this activity only if the attachment is not uploaded before
|
||||
if not issue_attachment.is_uploaded:
|
||||
issue_activity.delay(
|
||||
type="attachment.activity.created",
|
||||
requested_data=None,
|
||||
actor_id=str(self.request.user.id),
|
||||
issue_id=str(self.kwargs.get("issue_id", None)),
|
||||
project_id=str(self.kwargs.get("project_id", None)),
|
||||
current_instance=json.dumps(
|
||||
serializer.data,
|
||||
cls=DjangoJSONEncoder,
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
|
||||
# Update the attachment
|
||||
issue_attachment.is_uploaded = True
|
||||
issue_attachment.created_by = request.user
|
||||
|
||||
# Get the storage metadata
|
||||
if not issue_attachment.storage_metadata:
|
||||
get_asset_object_metadata.delay(str(issue_attachment.id))
|
||||
issue_attachment.save()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@@ -14,9 +14,6 @@ from django.db.models import (
|
||||
Q,
|
||||
UUIDField,
|
||||
Value,
|
||||
Subquery,
|
||||
Case,
|
||||
When,
|
||||
)
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.utils import timezone
|
||||
@@ -38,14 +35,13 @@ from plane.app.serializers import (
|
||||
from plane.bgtasks.issue_activities_task import issue_activity
|
||||
from plane.db.models import (
|
||||
Issue,
|
||||
FileAsset,
|
||||
IssueAttachment,
|
||||
IssueLink,
|
||||
IssueUserProperty,
|
||||
IssueReaction,
|
||||
IssueSubscriber,
|
||||
Project,
|
||||
ProjectMember,
|
||||
CycleIssue,
|
||||
)
|
||||
from plane.utils.grouper import (
|
||||
issue_group_values,
|
||||
@@ -66,7 +62,7 @@ from plane.bgtasks.webhook_task import model_activity
|
||||
|
||||
|
||||
class IssueListEndpoint(BaseAPIView):
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST])
|
||||
def get(self, request, slug, project_id):
|
||||
issue_ids = request.GET.get("issues", False)
|
||||
|
||||
@@ -87,13 +83,7 @@ class IssueListEndpoint(BaseAPIView):
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.select_related("workspace", "project", "state", "parent")
|
||||
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||
.annotate(
|
||||
cycle_id=Subquery(
|
||||
CycleIssue.objects.filter(
|
||||
issue=OuterRef("id"), deleted_at__isnull=True
|
||||
).values("cycle_id")[:1]
|
||||
)
|
||||
)
|
||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||
.annotate(
|
||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||
.order_by()
|
||||
@@ -101,9 +91,8 @@ class IssueListEndpoint(BaseAPIView):
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
attachment_count=FileAsset.objects.filter(
|
||||
issue_id=OuterRef("id"),
|
||||
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
|
||||
attachment_count=IssueAttachment.objects.filter(
|
||||
issue=OuterRef("id")
|
||||
)
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
@@ -217,13 +206,7 @@ class IssueViewSet(BaseViewSet):
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.select_related("workspace", "project", "state", "parent")
|
||||
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||
.annotate(
|
||||
cycle_id=Subquery(
|
||||
CycleIssue.objects.filter(
|
||||
issue=OuterRef("id"), deleted_at__isnull=True
|
||||
).values("cycle_id")[:1]
|
||||
)
|
||||
)
|
||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||
.annotate(
|
||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||
.order_by()
|
||||
@@ -231,9 +214,8 @@ class IssueViewSet(BaseViewSet):
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
attachment_count=FileAsset.objects.filter(
|
||||
issue_id=OuterRef("id"),
|
||||
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
|
||||
attachment_count=IssueAttachment.objects.filter(
|
||||
issue=OuterRef("id")
|
||||
)
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
@@ -250,19 +232,12 @@ class IssueViewSet(BaseViewSet):
|
||||
).distinct()
|
||||
|
||||
@method_decorator(gzip_page)
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST])
|
||||
def list(self, request, slug, project_id):
|
||||
extra_filters = {}
|
||||
if request.GET.get("updated_at__gt", None) is not None:
|
||||
extra_filters = {
|
||||
"updated_at__gt": request.GET.get("updated_at__gt")
|
||||
}
|
||||
|
||||
project = Project.objects.get(pk=project_id, workspace__slug=slug)
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
order_by_param = request.GET.get("order_by", "-created_at")
|
||||
|
||||
issue_queryset = self.get_queryset().filter(**filters, **extra_filters)
|
||||
issue_queryset = self.get_queryset().filter(**filters)
|
||||
# Custom ordering for priority and state
|
||||
|
||||
# Issue queryset
|
||||
@@ -289,16 +264,13 @@ class IssueViewSet(BaseViewSet):
|
||||
entity_identifier=project_id,
|
||||
user_id=request.user.id,
|
||||
)
|
||||
if (
|
||||
ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
member=request.user,
|
||||
role=5,
|
||||
is_active=True,
|
||||
).exists()
|
||||
and not project.guest_view_all_features
|
||||
):
|
||||
if ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
member=request.user,
|
||||
role=5,
|
||||
is_active=True,
|
||||
).exists():
|
||||
issue_queryset = issue_queryset.filter(created_by=request.user)
|
||||
|
||||
if group_by:
|
||||
@@ -336,10 +308,10 @@ class IssueViewSet(BaseViewSet):
|
||||
group_by_field_name=group_by,
|
||||
sub_group_by_field_name=sub_group_by,
|
||||
count_filter=Q(
|
||||
Q(issue_intake__status=1)
|
||||
| Q(issue_intake__status=-1)
|
||||
| Q(issue_intake__status=2)
|
||||
| Q(issue_intake__isnull=True),
|
||||
Q(issue_inbox__status=1)
|
||||
| Q(issue_inbox__status=-1)
|
||||
| Q(issue_inbox__status=2)
|
||||
| Q(issue_inbox__isnull=True),
|
||||
archived_at__isnull=True,
|
||||
is_draft=False,
|
||||
),
|
||||
@@ -364,10 +336,10 @@ class IssueViewSet(BaseViewSet):
|
||||
),
|
||||
group_by_field_name=group_by,
|
||||
count_filter=Q(
|
||||
Q(issue_intake__status=1)
|
||||
| Q(issue_intake__status=-1)
|
||||
| Q(issue_intake__status=2)
|
||||
| Q(issue_intake__isnull=True),
|
||||
Q(issue_inbox__status=1)
|
||||
| Q(issue_inbox__status=-1)
|
||||
| Q(issue_inbox__status=2)
|
||||
| Q(issue_inbox__isnull=True),
|
||||
archived_at__isnull=True,
|
||||
is_draft=False,
|
||||
),
|
||||
@@ -468,66 +440,18 @@ class IssueViewSet(BaseViewSet):
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[
|
||||
ROLE.ADMIN,
|
||||
ROLE.MEMBER,
|
||||
ROLE.GUEST,
|
||||
],
|
||||
creator=True,
|
||||
model=Issue,
|
||||
[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER], creator=True, model=Issue
|
||||
)
|
||||
def retrieve(self, request, slug, project_id, pk=None):
|
||||
project = Project.objects.get(pk=project_id, workspace__slug=slug)
|
||||
|
||||
issue = (
|
||||
Issue.objects.filter(
|
||||
project_id=self.kwargs.get("project_id")
|
||||
)
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.select_related("workspace", "project", "state", "parent")
|
||||
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||
.annotate(
|
||||
cycle_id=Case(
|
||||
When(
|
||||
issue_cycle__cycle__deleted_at__isnull=True,
|
||||
then=F("issue_cycle__cycle_id"),
|
||||
),
|
||||
default=None,
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
attachment_count=FileAsset.objects.filter(
|
||||
issue_id=OuterRef("id"),
|
||||
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
|
||||
)
|
||||
.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")
|
||||
)
|
||||
self.get_queryset()
|
||||
.filter(pk=pk)
|
||||
.annotate(
|
||||
label_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"labels__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(labels__id__isnull=True)
|
||||
& Q(label_issue__deleted_at__isnull=True),
|
||||
),
|
||||
filter=~Q(labels__id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
@@ -535,11 +459,8 @@ class IssueViewSet(BaseViewSet):
|
||||
ArrayAgg(
|
||||
"assignees__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True)
|
||||
& Q(issue_assignee__deleted_at__isnull=True)
|
||||
),
|
||||
filter=~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
@@ -547,11 +468,8 @@ class IssueViewSet(BaseViewSet):
|
||||
ArrayAgg(
|
||||
"issue_module__module_id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(issue_module__module_id__isnull=True)
|
||||
& Q(issue_module__module__archived_at__isnull=True)
|
||||
& Q(issue_module__deleted_at__isnull=True)
|
||||
),
|
||||
filter=~Q(issue_module__module_id__isnull=True)
|
||||
& Q(issue_module__module__archived_at__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
@@ -564,6 +482,12 @@ class IssueViewSet(BaseViewSet):
|
||||
),
|
||||
)
|
||||
)
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"issue_attachment",
|
||||
queryset=IssueAttachment.objects.select_related("issue"),
|
||||
)
|
||||
)
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"issue_link",
|
||||
@@ -587,27 +511,6 @@ class IssueViewSet(BaseViewSet):
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
|
||||
"""
|
||||
if the role is guest and guest_view_all_features is false and owned by is not
|
||||
the requesting user then dont show the issue
|
||||
"""
|
||||
|
||||
if (
|
||||
ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
member=request.user,
|
||||
role=5,
|
||||
is_active=True,
|
||||
).exists()
|
||||
and not project.guest_view_all_features
|
||||
and not issue.created_by == request.user
|
||||
):
|
||||
return Response(
|
||||
{"error": "You are not allowed to view this issue"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
recent_visited_task.delay(
|
||||
slug=slug,
|
||||
entity_name="issue",
|
||||
@@ -619,9 +522,7 @@ class IssueViewSet(BaseViewSet):
|
||||
serializer = IssueDetailSerializer(issue, expand=self.expand)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], creator=True, model=Issue
|
||||
)
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
def partial_update(self, request, slug, project_id, pk=None):
|
||||
issue = (
|
||||
self.get_queryset()
|
||||
@@ -630,10 +531,7 @@ class IssueViewSet(BaseViewSet):
|
||||
ArrayAgg(
|
||||
"labels__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(labels__id__isnull=True)
|
||||
& Q(label_issue__deleted_at__isnull=True),
|
||||
),
|
||||
filter=~Q(labels__id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
@@ -641,11 +539,8 @@ class IssueViewSet(BaseViewSet):
|
||||
ArrayAgg(
|
||||
"assignees__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True)
|
||||
& Q(issue_assignee__deleted_at__isnull=True)
|
||||
),
|
||||
filter=~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
@@ -653,11 +548,7 @@ class IssueViewSet(BaseViewSet):
|
||||
ArrayAgg(
|
||||
"issue_module__module_id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(issue_module__module_id__isnull=True)
|
||||
& Q(issue_module__module__archived_at__isnull=True)
|
||||
& Q(issue_module__deleted_at__isnull=True)
|
||||
),
|
||||
filter=~Q(issue_module__module_id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
@@ -727,7 +618,7 @@ class IssueViewSet(BaseViewSet):
|
||||
|
||||
|
||||
class IssueUserDisplayPropertyEndpoint(BaseAPIView):
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST, ROLE.VIEWER])
|
||||
def patch(self, request, slug, project_id):
|
||||
issue_property = IssueUserProperty.objects.get(
|
||||
user=request.user,
|
||||
@@ -747,13 +638,7 @@ class IssueUserDisplayPropertyEndpoint(BaseAPIView):
|
||||
serializer = IssueUserPropertySerializer(issue_property)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
@allow_permission(
|
||||
[
|
||||
ROLE.ADMIN,
|
||||
ROLE.MEMBER,
|
||||
ROLE.GUEST,
|
||||
]
|
||||
)
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST, ROLE.VIEWER])
|
||||
def get(self, request, slug, project_id):
|
||||
issue_property, _ = IssueUserProperty.objects.get_or_create(
|
||||
user=request.user, project_id=project_id
|
||||
@@ -787,46 +672,18 @@ class BulkDeleteIssuesEndpoint(BaseAPIView):
|
||||
)
|
||||
|
||||
|
||||
class DeletedIssuesListViewSet(BaseAPIView):
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
def get(self, request, slug, project_id):
|
||||
filters = {}
|
||||
if request.GET.get("updated_at__gt", None) is not None:
|
||||
filters = {"updated_at__gt": request.GET.get("updated_at__gt")}
|
||||
deleted_issues = (
|
||||
Issue.all_objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
)
|
||||
.filter(Q(archived_at__isnull=False) | Q(deleted_at__isnull=False))
|
||||
.filter(**filters)
|
||||
.values_list("id", flat=True)
|
||||
)
|
||||
|
||||
return Response(deleted_issues, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class IssuePaginatedViewSet(BaseViewSet):
|
||||
def get_queryset(self):
|
||||
workspace_slug = self.kwargs.get("slug")
|
||||
project_id = self.kwargs.get("project_id")
|
||||
|
||||
issue_queryset = Issue.issue_objects.filter(
|
||||
workspace__slug=workspace_slug, project_id=project_id
|
||||
)
|
||||
|
||||
return (
|
||||
issue_queryset.select_related(
|
||||
"workspace", "project", "state", "parent"
|
||||
Issue.issue_objects.filter(
|
||||
workspace__slug=workspace_slug, project_id=project_id
|
||||
)
|
||||
.select_related("workspace", "project", "state", "parent")
|
||||
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||
.annotate(
|
||||
cycle_id=Subquery(
|
||||
CycleIssue.objects.filter(
|
||||
issue=OuterRef("id"), deleted_at__isnull=True
|
||||
).values("cycle_id")[:1]
|
||||
)
|
||||
)
|
||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||
.annotate(
|
||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||
.order_by()
|
||||
@@ -834,9 +691,8 @@ class IssuePaginatedViewSet(BaseViewSet):
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
attachment_count=FileAsset.objects.filter(
|
||||
issue_id=OuterRef("id"),
|
||||
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
|
||||
attachment_count=IssueAttachment.objects.filter(
|
||||
issue=OuterRef("id")
|
||||
)
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
@@ -863,18 +719,17 @@ class IssuePaginatedViewSet(BaseViewSet):
|
||||
|
||||
return paginated_data
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST])
|
||||
def list(self, request, slug, project_id):
|
||||
cursor = request.GET.get("cursor", None)
|
||||
is_description_required = request.GET.get("description", "false")
|
||||
updated_at = request.GET.get("updated_at__gt", None)
|
||||
is_description_required = request.GET.get("description", False)
|
||||
updated_at = request.GET.get("updated_at__gte", None)
|
||||
|
||||
# required fields
|
||||
required_fields = [
|
||||
"id",
|
||||
"name",
|
||||
"state_id",
|
||||
"state__group",
|
||||
"sort_order",
|
||||
"completed_at",
|
||||
"estimate_point",
|
||||
@@ -891,6 +746,7 @@ class IssuePaginatedViewSet(BaseViewSet):
|
||||
"updated_by",
|
||||
"is_draft",
|
||||
"archived_at",
|
||||
"deleted_at",
|
||||
"module_ids",
|
||||
"label_ids",
|
||||
"assignee_ids",
|
||||
@@ -899,44 +755,26 @@ class IssuePaginatedViewSet(BaseViewSet):
|
||||
"sub_issues_count",
|
||||
]
|
||||
|
||||
if str(is_description_required).lower() == "true":
|
||||
if is_description_required:
|
||||
required_fields.append("description_html")
|
||||
|
||||
# querying issues
|
||||
base_queryset = Issue.issue_objects.filter(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
|
||||
base_queryset = base_queryset.order_by("updated_at")
|
||||
).order_by("updated_at")
|
||||
queryset = self.get_queryset().order_by("updated_at")
|
||||
|
||||
# validation for guest user
|
||||
project = Project.objects.get(pk=project_id, workspace__slug=slug)
|
||||
project_member = ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
member=request.user,
|
||||
role=5,
|
||||
is_active=True,
|
||||
)
|
||||
if project_member.exists() and not project.guest_view_all_features:
|
||||
base_queryset = base_queryset.filter(created_by=request.user)
|
||||
queryset = queryset.filter(created_by=request.user)
|
||||
|
||||
# filtering issues by greater then updated_at given by the user
|
||||
if updated_at:
|
||||
base_queryset = base_queryset.filter(updated_at__gt=updated_at)
|
||||
queryset = queryset.filter(updated_at__gt=updated_at)
|
||||
base_queryset = base_queryset.filter(updated_at__gte=updated_at)
|
||||
queryset = queryset.filter(updated_at__gte=updated_at)
|
||||
|
||||
queryset = queryset.annotate(
|
||||
label_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"labels__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(labels__id__isnull=True)
|
||||
& Q(label_issue__deleted_at__isnull=True),
|
||||
),
|
||||
filter=~Q(labels__id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
@@ -944,11 +782,8 @@ class IssuePaginatedViewSet(BaseViewSet):
|
||||
ArrayAgg(
|
||||
"assignees__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True)
|
||||
& Q(issue_assignee__deleted_at__isnull=True)
|
||||
),
|
||||
filter=~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
@@ -956,11 +791,8 @@ class IssuePaginatedViewSet(BaseViewSet):
|
||||
ArrayAgg(
|
||||
"issue_module__module_id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(issue_module__module_id__isnull=True)
|
||||
& Q(issue_module__module__archived_at__isnull=True)
|
||||
& Q(issue_module__deleted_at__isnull=True)
|
||||
),
|
||||
filter=~Q(issue_module__module_id__isnull=True)
|
||||
& Q(issue_module__module__archived_at__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
@@ -976,194 +808,3 @@ class IssuePaginatedViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
return Response(paginated_data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class IssueDetailEndpoint(BaseAPIView):
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
def get(self, request, slug, project_id):
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
issue = (
|
||||
Issue.issue_objects.filter(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
.select_related("workspace", "project", "state", "parent")
|
||||
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||
.annotate(
|
||||
cycle_id=Subquery(
|
||||
CycleIssue.objects.filter(
|
||||
issue=OuterRef("id"), deleted_at__isnull=True
|
||||
).values("cycle_id")[:1]
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
label_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"labels__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(labels__id__isnull=True)
|
||||
& Q(label_issue__deleted_at__isnull=True),
|
||||
),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
assignee_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"assignees__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True)
|
||||
& Q(issue_assignee__deleted_at__isnull=True)
|
||||
),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
module_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"issue_module__module_id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(issue_module__module_id__isnull=True)
|
||||
& Q(issue_module__module__archived_at__isnull=True)
|
||||
& Q(issue_module__deleted_at__isnull=True)
|
||||
),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
)
|
||||
.annotate(
|
||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
attachment_count=FileAsset.objects.filter(
|
||||
issue_id=OuterRef("id"),
|
||||
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
|
||||
)
|
||||
.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")
|
||||
)
|
||||
)
|
||||
issue = issue.filter(**filters)
|
||||
order_by_param = request.GET.get("order_by", "-created_at")
|
||||
# Issue queryset
|
||||
issue, order_by_param = order_issue_queryset(
|
||||
issue_queryset=issue,
|
||||
order_by_param=order_by_param,
|
||||
)
|
||||
return self.paginate(
|
||||
request=request,
|
||||
order_by=order_by_param,
|
||||
queryset=(issue),
|
||||
on_results=lambda issue: IssueSerializer(
|
||||
issue,
|
||||
many=True,
|
||||
fields=self.fields,
|
||||
expand=self.expand,
|
||||
).data,
|
||||
)
|
||||
|
||||
|
||||
class IssueBulkUpdateDateEndpoint(BaseAPIView):
|
||||
|
||||
def validate_dates(
|
||||
self, current_start, current_target, new_start, new_target
|
||||
):
|
||||
"""
|
||||
Validate that start date is before target date.
|
||||
"""
|
||||
start = new_start or current_start
|
||||
target = new_target or current_target
|
||||
|
||||
if start and target and start > target:
|
||||
return False
|
||||
return True
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def post(self, request, slug, project_id):
|
||||
|
||||
updates = request.data.get("updates", [])
|
||||
|
||||
issue_ids = [update["id"] for update in updates]
|
||||
epoch = int(timezone.now().timestamp())
|
||||
|
||||
# Fetch all relevant issues in a single query
|
||||
issues = list(Issue.objects.filter(id__in=issue_ids))
|
||||
issues_dict = {str(issue.id): issue for issue in issues}
|
||||
issues_to_update = []
|
||||
|
||||
for update in updates:
|
||||
issue_id = update["id"]
|
||||
issue = issues_dict.get(issue_id)
|
||||
|
||||
if not issue:
|
||||
continue
|
||||
|
||||
start_date = update.get("start_date")
|
||||
target_date = update.get("target_date")
|
||||
validate_dates = self.validate_dates(
|
||||
issue.start_date, issue.target_date, start_date, target_date
|
||||
)
|
||||
if not validate_dates:
|
||||
return Response(
|
||||
{
|
||||
"message": "Start date cannot exceed target date",
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
if start_date:
|
||||
issue_activity.delay(
|
||||
type="issue.activity.updated",
|
||||
requested_data=json.dumps(
|
||||
{"start_date": update.get("start_date")}
|
||||
),
|
||||
current_instance=json.dumps(
|
||||
{"start_date": str(issue.start_date)}
|
||||
),
|
||||
issue_id=str(issue_id),
|
||||
actor_id=str(request.user.id),
|
||||
project_id=str(project_id),
|
||||
epoch=epoch,
|
||||
)
|
||||
issue.start_date = start_date
|
||||
issues_to_update.append(issue)
|
||||
|
||||
if target_date:
|
||||
issue_activity.delay(
|
||||
type="issue.activity.updated",
|
||||
requested_data=json.dumps(
|
||||
{"target_date": update.get("target_date")}
|
||||
),
|
||||
current_instance=json.dumps(
|
||||
{"target_date": str(issue.target_date)}
|
||||
),
|
||||
issue_id=str(issue_id),
|
||||
actor_id=str(request.user.id),
|
||||
project_id=str(project_id),
|
||||
epoch=epoch,
|
||||
)
|
||||
issue.target_date = target_date
|
||||
issues_to_update.append(issue)
|
||||
|
||||
# Bulk update issues
|
||||
Issue.objects.bulk_update(
|
||||
issues_to_update, ["start_date", "target_date"]
|
||||
)
|
||||
|
||||
return Response(
|
||||
{"message": "Issues updated successfully"},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
@@ -16,13 +16,11 @@ from plane.app.serializers import (
|
||||
IssueCommentSerializer,
|
||||
CommentReactionSerializer,
|
||||
)
|
||||
from plane.app.permissions import allow_permission, ROLE
|
||||
from plane.app.permissions import ProjectLitePermission, allow_permission, ROLE
|
||||
from plane.db.models import (
|
||||
IssueComment,
|
||||
ProjectMember,
|
||||
CommentReaction,
|
||||
Project,
|
||||
Issue,
|
||||
)
|
||||
from plane.bgtasks.issue_activities_task import issue_activity
|
||||
|
||||
@@ -65,31 +63,8 @@ class IssueCommentViewSet(BaseViewSet):
|
||||
.distinct()
|
||||
)
|
||||
|
||||
@allow_permission(
|
||||
[
|
||||
ROLE.ADMIN,
|
||||
ROLE.MEMBER,
|
||||
ROLE.GUEST,
|
||||
]
|
||||
)
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST, ROLE.VIEWER])
|
||||
def create(self, request, slug, project_id, issue_id):
|
||||
project = Project.objects.get(pk=project_id)
|
||||
issue = Issue.objects.get(pk=issue_id)
|
||||
if (
|
||||
ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
member=request.user,
|
||||
role=5,
|
||||
is_active=True,
|
||||
).exists()
|
||||
and not project.guest_view_all_features
|
||||
and not issue.created_by == request.user
|
||||
):
|
||||
return Response(
|
||||
{"error": "You are not allowed to comment on the issue"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
serializer = IssueCommentSerializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
serializer.save(
|
||||
@@ -114,7 +89,7 @@ class IssueCommentViewSet(BaseViewSet):
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN],
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER],
|
||||
creator=True,
|
||||
model=IssueComment,
|
||||
)
|
||||
@@ -181,6 +156,9 @@ class IssueCommentViewSet(BaseViewSet):
|
||||
class CommentReactionViewSet(BaseViewSet):
|
||||
serializer_class = CommentReactionSerializer
|
||||
model = CommentReaction
|
||||
permission_classes = [
|
||||
ProjectLitePermission,
|
||||
]
|
||||
|
||||
def get_queryset(self):
|
||||
return (
|
||||
@@ -198,13 +176,6 @@ class CommentReactionViewSet(BaseViewSet):
|
||||
.distinct()
|
||||
)
|
||||
|
||||
@allow_permission(
|
||||
[
|
||||
ROLE.ADMIN,
|
||||
ROLE.MEMBER,
|
||||
ROLE.GUEST,
|
||||
]
|
||||
)
|
||||
def create(self, request, slug, project_id, comment_id):
|
||||
serializer = CommentReactionSerializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
@@ -227,13 +198,6 @@ class CommentReactionViewSet(BaseViewSet):
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@allow_permission(
|
||||
[
|
||||
ROLE.ADMIN,
|
||||
ROLE.MEMBER,
|
||||
ROLE.GUEST,
|
||||
]
|
||||
)
|
||||
def destroy(self, request, slug, project_id, comment_id, reaction_code):
|
||||
comment_reaction = CommentReaction.objects.get(
|
||||
workspace__slug=slug,
|
||||
|
||||
@@ -0,0 +1,410 @@
|
||||
# Python imports
|
||||
import json
|
||||
|
||||
# Django imports
|
||||
from django.contrib.postgres.aggregates import ArrayAgg
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.db.models import (
|
||||
Exists,
|
||||
F,
|
||||
Func,
|
||||
OuterRef,
|
||||
Prefetch,
|
||||
Q,
|
||||
UUIDField,
|
||||
Value,
|
||||
)
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.utils import timezone
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.gzip import gzip_page
|
||||
|
||||
# Third Party imports
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
|
||||
# Module imports
|
||||
from plane.app.permissions import ProjectEntityPermission
|
||||
from plane.app.serializers import (
|
||||
IssueCreateSerializer,
|
||||
IssueDetailSerializer,
|
||||
IssueFlatSerializer,
|
||||
IssueSerializer,
|
||||
)
|
||||
from plane.bgtasks.issue_activities_task import issue_activity
|
||||
from plane.db.models import (
|
||||
Issue,
|
||||
IssueAttachment,
|
||||
IssueLink,
|
||||
IssueReaction,
|
||||
IssueSubscriber,
|
||||
Project,
|
||||
ProjectMember,
|
||||
)
|
||||
from plane.utils.grouper import (
|
||||
issue_group_values,
|
||||
issue_on_results,
|
||||
issue_queryset_grouper,
|
||||
)
|
||||
from plane.utils.issue_filters import issue_filters
|
||||
from plane.utils.order_queryset import order_issue_queryset
|
||||
from plane.utils.paginator import (
|
||||
GroupedOffsetPaginator,
|
||||
SubGroupedOffsetPaginator,
|
||||
)
|
||||
from .. import BaseViewSet
|
||||
|
||||
|
||||
class IssueDraftViewSet(BaseViewSet):
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
serializer_class = IssueFlatSerializer
|
||||
model = Issue
|
||||
|
||||
def get_queryset(self):
|
||||
return (
|
||||
Issue.objects.filter(project_id=self.kwargs.get("project_id"))
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.filter(is_draft=True)
|
||||
.filter(deleted_at__isnull=True)
|
||||
.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")
|
||||
)
|
||||
).distinct()
|
||||
|
||||
@method_decorator(gzip_page)
|
||||
def list(self, request, slug, project_id):
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
|
||||
order_by_param = request.GET.get("order_by", "-created_at")
|
||||
|
||||
issue_queryset = self.get_queryset().filter(**filters)
|
||||
# Issue queryset
|
||||
issue_queryset, order_by_param = order_issue_queryset(
|
||||
issue_queryset=issue_queryset,
|
||||
order_by_param=order_by_param,
|
||||
)
|
||||
|
||||
# Group by
|
||||
group_by = request.GET.get("group_by", False)
|
||||
sub_group_by = request.GET.get("sub_group_by", False)
|
||||
|
||||
# issue queryset
|
||||
issue_queryset = issue_queryset_grouper(
|
||||
queryset=issue_queryset,
|
||||
group_by=group_by,
|
||||
sub_group_by=sub_group_by,
|
||||
)
|
||||
|
||||
if group_by:
|
||||
# Check group and sub group value paginate
|
||||
if sub_group_by:
|
||||
if group_by == sub_group_by:
|
||||
return Response(
|
||||
{
|
||||
"error": "Group by and sub group by cannot have same parameters"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
else:
|
||||
# group and sub group pagination
|
||||
return self.paginate(
|
||||
request=request,
|
||||
order_by=order_by_param,
|
||||
queryset=issue_queryset,
|
||||
on_results=lambda issues: issue_on_results(
|
||||
group_by=group_by,
|
||||
issues=issues,
|
||||
sub_group_by=sub_group_by,
|
||||
),
|
||||
paginator_cls=SubGroupedOffsetPaginator,
|
||||
group_by_fields=issue_group_values(
|
||||
field=group_by,
|
||||
slug=slug,
|
||||
project_id=project_id,
|
||||
filters=filters,
|
||||
),
|
||||
sub_group_by_fields=issue_group_values(
|
||||
field=sub_group_by,
|
||||
slug=slug,
|
||||
project_id=project_id,
|
||||
filters=filters,
|
||||
),
|
||||
group_by_field_name=group_by,
|
||||
sub_group_by_field_name=sub_group_by,
|
||||
count_filter=Q(
|
||||
Q(issue_inbox__status=1)
|
||||
| Q(issue_inbox__status=-1)
|
||||
| Q(issue_inbox__status=2)
|
||||
| Q(issue_inbox__isnull=True),
|
||||
archived_at__isnull=True,
|
||||
is_draft=False,
|
||||
),
|
||||
)
|
||||
# Group Paginate
|
||||
else:
|
||||
# Group paginate
|
||||
return self.paginate(
|
||||
request=request,
|
||||
order_by=order_by_param,
|
||||
queryset=issue_queryset,
|
||||
on_results=lambda issues: issue_on_results(
|
||||
group_by=group_by,
|
||||
issues=issues,
|
||||
sub_group_by=sub_group_by,
|
||||
),
|
||||
paginator_cls=GroupedOffsetPaginator,
|
||||
group_by_fields=issue_group_values(
|
||||
field=group_by,
|
||||
slug=slug,
|
||||
project_id=project_id,
|
||||
filters=filters,
|
||||
),
|
||||
group_by_field_name=group_by,
|
||||
count_filter=Q(
|
||||
Q(issue_inbox__status=1)
|
||||
| Q(issue_inbox__status=-1)
|
||||
| Q(issue_inbox__status=2)
|
||||
| Q(issue_inbox__isnull=True),
|
||||
archived_at__isnull=True,
|
||||
is_draft=False,
|
||||
),
|
||||
)
|
||||
else:
|
||||
# List Paginate
|
||||
return self.paginate(
|
||||
order_by=order_by_param,
|
||||
request=request,
|
||||
queryset=issue_queryset,
|
||||
on_results=lambda issues: issue_on_results(
|
||||
group_by=group_by, issues=issues, sub_group_by=sub_group_by
|
||||
),
|
||||
)
|
||||
|
||||
def create(self, request, slug, project_id):
|
||||
project = Project.objects.get(pk=project_id)
|
||||
|
||||
serializer = IssueCreateSerializer(
|
||||
data=request.data,
|
||||
context={
|
||||
"project_id": project_id,
|
||||
"workspace_id": project.workspace_id,
|
||||
"default_assignee_id": project.default_assignee_id,
|
||||
},
|
||||
)
|
||||
|
||||
if serializer.is_valid():
|
||||
serializer.save(is_draft=True)
|
||||
|
||||
# Track the issue
|
||||
issue_activity.delay(
|
||||
type="issue_draft.activity.created",
|
||||
requested_data=json.dumps(
|
||||
self.request.data, cls=DjangoJSONEncoder
|
||||
),
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=str(serializer.data.get("id", None)),
|
||||
project_id=str(project_id),
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
|
||||
issue = (
|
||||
issue_queryset_grouper(
|
||||
queryset=self.get_queryset().filter(
|
||||
pk=serializer.data["id"]
|
||||
),
|
||||
group_by=None,
|
||||
sub_group_by=None,
|
||||
)
|
||||
.values(
|
||||
"id",
|
||||
"name",
|
||||
"state_id",
|
||||
"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",
|
||||
"updated_by",
|
||||
"attachment_count",
|
||||
"link_count",
|
||||
"is_draft",
|
||||
"archived_at",
|
||||
)
|
||||
.first()
|
||||
)
|
||||
return Response(issue, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def partial_update(self, request, slug, project_id, pk):
|
||||
issue = self.get_queryset().filter(pk=pk).first()
|
||||
|
||||
if not issue:
|
||||
return Response(
|
||||
{"error": "Issue does not exist"},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
|
||||
serializer = IssueCreateSerializer(
|
||||
issue, data=request.data, partial=True
|
||||
)
|
||||
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
issue_activity.delay(
|
||||
type="issue_draft.activity.updated",
|
||||
requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
|
||||
actor_id=str(self.request.user.id),
|
||||
issue_id=str(self.kwargs.get("pk", None)),
|
||||
project_id=str(self.kwargs.get("project_id", None)),
|
||||
current_instance=json.dumps(
|
||||
IssueSerializer(issue).data,
|
||||
cls=DjangoJSONEncoder,
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def retrieve(self, request, slug, project_id, pk=None):
|
||||
issue = (
|
||||
self.get_queryset()
|
||||
.filter(pk=pk)
|
||||
.annotate(
|
||||
label_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"labels__id",
|
||||
distinct=True,
|
||||
filter=~Q(labels__id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
assignee_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"assignees__id",
|
||||
distinct=True,
|
||||
filter=~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
module_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"issue_module__module_id",
|
||||
distinct=True,
|
||||
filter=~Q(issue_module__module_id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
)
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"issue_reactions",
|
||||
queryset=IssueReaction.objects.select_related(
|
||||
"issue", "actor"
|
||||
),
|
||||
)
|
||||
)
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"issue_attachment",
|
||||
queryset=IssueAttachment.objects.select_related("issue"),
|
||||
)
|
||||
)
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"issue_link",
|
||||
queryset=IssueLink.objects.select_related("created_by"),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
is_subscribed=Exists(
|
||||
IssueSubscriber.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
issue_id=OuterRef("pk"),
|
||||
subscriber=request.user,
|
||||
)
|
||||
)
|
||||
)
|
||||
).first()
|
||||
|
||||
if not issue:
|
||||
return Response(
|
||||
{"error": "The required object does not exist."},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
serializer = IssueDetailSerializer(issue, expand=self.expand)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
def destroy(self, request, slug, project_id, pk=None):
|
||||
issue = Issue.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=pk
|
||||
)
|
||||
if issue.created_by_id != request.user.id and (
|
||||
not ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
member=request.user,
|
||||
role=20,
|
||||
project_id=project_id,
|
||||
is_active=True,
|
||||
).exists()
|
||||
):
|
||||
return Response(
|
||||
{"error": "Only admin or creator can delete the issue"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
issue.delete()
|
||||
issue_activity.delay(
|
||||
type="issue_draft.activity.deleted",
|
||||
requested_data=json.dumps({"issue_id": str(pk)}),
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=str(pk),
|
||||
project_id=str(project_id),
|
||||
current_instance={},
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
@@ -43,7 +43,7 @@ class LabelViewSet(BaseViewSet):
|
||||
@invalidate_cache(
|
||||
path="/api/workspaces/:slug/labels/", url_params=True, user=False
|
||||
)
|
||||
@allow_permission([ROLE.ADMIN])
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def create(self, request, slug, project_id):
|
||||
try:
|
||||
serializer = LabelSerializer(data=request.data)
|
||||
@@ -66,14 +66,14 @@ class LabelViewSet(BaseViewSet):
|
||||
@invalidate_cache(
|
||||
path="/api/workspaces/:slug/labels/", url_params=True, user=False
|
||||
)
|
||||
@allow_permission([ROLE.ADMIN])
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def partial_update(self, request, *args, **kwargs):
|
||||
return super().partial_update(request, *args, **kwargs)
|
||||
|
||||
@invalidate_cache(
|
||||
path="/api/workspaces/:slug/labels/", url_params=True, user=False
|
||||
)
|
||||
@allow_permission([ROLE.ADMIN])
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
return super().destroy(request, *args, **kwargs)
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ from rest_framework import status
|
||||
# Module imports
|
||||
from .. import BaseViewSet
|
||||
from plane.app.serializers import IssueReactionSerializer
|
||||
from plane.app.permissions import allow_permission, ROLE
|
||||
from plane.app.permissions import ProjectLitePermission
|
||||
from plane.db.models import IssueReaction
|
||||
from plane.bgtasks.issue_activities_task import issue_activity
|
||||
|
||||
@@ -20,6 +20,9 @@ from plane.bgtasks.issue_activities_task import issue_activity
|
||||
class IssueReactionViewSet(BaseViewSet):
|
||||
serializer_class = IssueReactionSerializer
|
||||
model = IssueReaction
|
||||
permission_classes = [
|
||||
ProjectLitePermission,
|
||||
]
|
||||
|
||||
def get_queryset(self):
|
||||
return (
|
||||
@@ -37,7 +40,6 @@ class IssueReactionViewSet(BaseViewSet):
|
||||
.distinct()
|
||||
)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
def create(self, request, slug, project_id, issue_id):
|
||||
serializer = IssueReactionSerializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
@@ -60,7 +62,6 @@ class IssueReactionViewSet(BaseViewSet):
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
def destroy(self, request, slug, project_id, issue_id, reaction_code):
|
||||
issue_reaction = IssueReaction.objects.get(
|
||||
workspace__slug=slug,
|
||||
|
||||
@@ -3,16 +3,7 @@ import json
|
||||
|
||||
# Django imports
|
||||
from django.utils import timezone
|
||||
from django.db.models import (
|
||||
Q,
|
||||
OuterRef,
|
||||
F,
|
||||
Func,
|
||||
UUIDField,
|
||||
Value,
|
||||
CharField,
|
||||
Subquery,
|
||||
)
|
||||
from django.db.models import Q, OuterRef, F, Func, UUIDField, Value, CharField
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.contrib.postgres.aggregates import ArrayAgg
|
||||
@@ -33,12 +24,10 @@ from plane.db.models import (
|
||||
Project,
|
||||
IssueRelation,
|
||||
Issue,
|
||||
FileAsset,
|
||||
IssueAttachment,
|
||||
IssueLink,
|
||||
CycleIssue,
|
||||
)
|
||||
from plane.bgtasks.issue_activities_task import issue_activity
|
||||
from plane.utils.issue_relation_mapper import get_actual_relation
|
||||
|
||||
|
||||
class IssueRelationViewSet(BaseViewSet):
|
||||
@@ -90,37 +79,11 @@ class IssueRelationViewSet(BaseViewSet):
|
||||
related_issue_id=issue_id, relation_type="relates_to"
|
||||
).values_list("issue_id", flat=True)
|
||||
|
||||
# get all start after issues
|
||||
start_after_issues = issue_relations.filter(
|
||||
relation_type="start_before", related_issue_id=issue_id
|
||||
).values_list("issue_id", flat=True)
|
||||
|
||||
# get all start_before issues
|
||||
start_before_issues = issue_relations.filter(
|
||||
relation_type="start_before", issue_id=issue_id
|
||||
).values_list("related_issue_id", flat=True)
|
||||
|
||||
# get all finish after issues
|
||||
finish_after_issues = issue_relations.filter(
|
||||
relation_type="finish_before", related_issue_id=issue_id
|
||||
).values_list("issue_id", flat=True)
|
||||
|
||||
# get all finish before issues
|
||||
finish_before_issues = issue_relations.filter(
|
||||
relation_type="finish_before", issue_id=issue_id
|
||||
).values_list("related_issue_id", flat=True)
|
||||
|
||||
queryset = (
|
||||
Issue.issue_objects.filter(workspace__slug=slug)
|
||||
.select_related("workspace", "project", "state", "parent")
|
||||
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||
.annotate(
|
||||
cycle_id=Subquery(
|
||||
CycleIssue.objects.filter(
|
||||
issue=OuterRef("id"), deleted_at__isnull=True
|
||||
).values("cycle_id")[:1]
|
||||
)
|
||||
)
|
||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||
.annotate(
|
||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||
.order_by()
|
||||
@@ -128,9 +91,8 @@ class IssueRelationViewSet(BaseViewSet):
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
attachment_count=FileAsset.objects.filter(
|
||||
issue_id=OuterRef("id"),
|
||||
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
|
||||
attachment_count=IssueAttachment.objects.filter(
|
||||
issue=OuterRef("id")
|
||||
)
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
@@ -149,10 +111,7 @@ class IssueRelationViewSet(BaseViewSet):
|
||||
ArrayAgg(
|
||||
"labels__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(labels__id__isnull=True)
|
||||
& (Q(label_issue__deleted_at__isnull=True))
|
||||
),
|
||||
filter=~Q(labels__id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
@@ -160,11 +119,8 @@ class IssueRelationViewSet(BaseViewSet):
|
||||
ArrayAgg(
|
||||
"assignees__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True)
|
||||
& Q(issue_assignee__deleted_at__isnull=True)
|
||||
),
|
||||
filter=~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
@@ -232,50 +188,12 @@ class IssueRelationViewSet(BaseViewSet):
|
||||
)
|
||||
)
|
||||
.values(*fields),
|
||||
"start_after": queryset.filter(pk__in=start_after_issues)
|
||||
.annotate(
|
||||
relation_type=Value(
|
||||
"start_after",
|
||||
output_field=CharField(),
|
||||
)
|
||||
)
|
||||
.values(*fields),
|
||||
"start_before": queryset.filter(pk__in=start_before_issues)
|
||||
.annotate(
|
||||
relation_type=Value(
|
||||
"start_before",
|
||||
output_field=CharField(),
|
||||
)
|
||||
)
|
||||
.values(*fields),
|
||||
"finish_after": queryset.filter(pk__in=finish_after_issues)
|
||||
.annotate(
|
||||
relation_type=Value(
|
||||
"finish_after",
|
||||
output_field=CharField(),
|
||||
)
|
||||
)
|
||||
.values(*fields),
|
||||
"finish_before": queryset.filter(pk__in=finish_before_issues)
|
||||
.annotate(
|
||||
relation_type=Value(
|
||||
"finish_before",
|
||||
output_field=CharField(),
|
||||
)
|
||||
)
|
||||
.values(*fields),
|
||||
}
|
||||
|
||||
return Response(response_data, status=status.HTTP_200_OK)
|
||||
|
||||
def create(self, request, slug, project_id, issue_id):
|
||||
relation_type = request.data.get("relation_type", None)
|
||||
if relation_type is None:
|
||||
return Response(
|
||||
{"message": "Issue relation type is required"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
issues = request.data.get("issues", [])
|
||||
project = Project.objects.get(pk=project_id)
|
||||
|
||||
@@ -283,18 +201,16 @@ class IssueRelationViewSet(BaseViewSet):
|
||||
[
|
||||
IssueRelation(
|
||||
issue_id=(
|
||||
issue
|
||||
if relation_type
|
||||
in ["blocking", "start_after", "finish_after"]
|
||||
else issue_id
|
||||
issue if relation_type == "blocking" else issue_id
|
||||
),
|
||||
related_issue_id=(
|
||||
issue_id
|
||||
if relation_type
|
||||
in ["blocking", "start_after", "finish_after"]
|
||||
else issue
|
||||
issue_id if relation_type == "blocking" else issue
|
||||
),
|
||||
relation_type=(
|
||||
"blocked_by"
|
||||
if relation_type == "blocking"
|
||||
else relation_type
|
||||
),
|
||||
relation_type=(get_actual_relation(relation_type)),
|
||||
project_id=project_id,
|
||||
workspace_id=project.workspace_id,
|
||||
created_by=request.user,
|
||||
@@ -318,7 +234,7 @@ class IssueRelationViewSet(BaseViewSet):
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
|
||||
if relation_type in ["blocking", "start_after", "finish_after"]:
|
||||
if relation_type == "blocking":
|
||||
return Response(
|
||||
RelatedIssueSerializer(issue_relation, many=True).data,
|
||||
status=status.HTTP_201_CREATED,
|
||||
@@ -333,7 +249,7 @@ class IssueRelationViewSet(BaseViewSet):
|
||||
relation_type = request.data.get("relation_type", None)
|
||||
related_issue = request.data.get("related_issue", None)
|
||||
|
||||
if relation_type in ["blocking", "start_after", "finish_after"]:
|
||||
if relation_type == "blocking":
|
||||
issue_relation = IssueRelation.objects.get(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
|
||||
@@ -3,7 +3,14 @@ import json
|
||||
|
||||
# Django imports
|
||||
from django.utils import timezone
|
||||
from django.db.models import OuterRef, Func, F, Q, Value, UUIDField, Subquery
|
||||
from django.db.models import (
|
||||
OuterRef,
|
||||
Func,
|
||||
F,
|
||||
Q,
|
||||
Value,
|
||||
UUIDField,
|
||||
)
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.gzip import gzip_page
|
||||
from django.contrib.postgres.aggregates import ArrayAgg
|
||||
@@ -21,8 +28,7 @@ from plane.app.permissions import ProjectEntityPermission
|
||||
from plane.db.models import (
|
||||
Issue,
|
||||
IssueLink,
|
||||
FileAsset,
|
||||
CycleIssue,
|
||||
IssueAttachment,
|
||||
)
|
||||
from plane.bgtasks.issue_activities_task import issue_activity
|
||||
from plane.utils.user_timezone_converter import user_timezone_converter
|
||||
@@ -42,13 +48,7 @@ class SubIssuesEndpoint(BaseAPIView):
|
||||
)
|
||||
.select_related("workspace", "project", "state", "parent")
|
||||
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||
.annotate(
|
||||
cycle_id=Subquery(
|
||||
CycleIssue.objects.filter(
|
||||
issue=OuterRef("id"), deleted_at__isnull=True
|
||||
).values("cycle_id")[:1]
|
||||
)
|
||||
)
|
||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||
.annotate(
|
||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||
.order_by()
|
||||
@@ -56,9 +56,8 @@ class SubIssuesEndpoint(BaseAPIView):
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
attachment_count=FileAsset.objects.filter(
|
||||
issue_id=OuterRef("id"),
|
||||
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
|
||||
attachment_count=IssueAttachment.objects.filter(
|
||||
issue=OuterRef("id")
|
||||
)
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
@@ -77,10 +76,7 @@ class SubIssuesEndpoint(BaseAPIView):
|
||||
ArrayAgg(
|
||||
"labels__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(labels__id__isnull=True)
|
||||
& Q(label_issue__deleted_at__isnull=True),
|
||||
),
|
||||
filter=~Q(labels__id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
@@ -88,11 +84,8 @@ class SubIssuesEndpoint(BaseAPIView):
|
||||
ArrayAgg(
|
||||
"assignees__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True)
|
||||
& Q(issue_assignee__deleted_at__isnull=True)
|
||||
),
|
||||
filter=~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
@@ -100,11 +93,7 @@ class SubIssuesEndpoint(BaseAPIView):
|
||||
ArrayAgg(
|
||||
"issue_module__module_id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(issue_module__module_id__isnull=True)
|
||||
& Q(issue_module__module__archived_at__isnull=True)
|
||||
& Q(issue_module__deleted_at__isnull=True)
|
||||
),
|
||||
filter=~Q(issue_module__module_id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
|
||||
@@ -14,12 +14,9 @@ from django.db.models import (
|
||||
Value,
|
||||
Sum,
|
||||
FloatField,
|
||||
Case,
|
||||
When,
|
||||
)
|
||||
from django.db.models.functions import Coalesce, Cast, Concat
|
||||
from django.db.models.functions import Coalesce, Cast
|
||||
from django.utils import timezone
|
||||
from django.db import models
|
||||
|
||||
# Third party imports
|
||||
from rest_framework import status
|
||||
@@ -57,7 +54,6 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
Issue.issue_objects.filter(
|
||||
state__group="cancelled",
|
||||
issue_module__module_id=OuterRef("pk"),
|
||||
issue_module__deleted_at__isnull=True,
|
||||
)
|
||||
.values("issue_module__module_id")
|
||||
.annotate(cnt=Count("pk"))
|
||||
@@ -67,7 +63,6 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
Issue.issue_objects.filter(
|
||||
state__group="completed",
|
||||
issue_module__module_id=OuterRef("pk"),
|
||||
issue_module__deleted_at__isnull=True,
|
||||
)
|
||||
.values("issue_module__module_id")
|
||||
.annotate(cnt=Count("pk"))
|
||||
@@ -77,7 +72,6 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
Issue.issue_objects.filter(
|
||||
state__group="started",
|
||||
issue_module__module_id=OuterRef("pk"),
|
||||
issue_module__deleted_at__isnull=True,
|
||||
)
|
||||
.values("issue_module__module_id")
|
||||
.annotate(cnt=Count("pk"))
|
||||
@@ -87,7 +81,6 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
Issue.issue_objects.filter(
|
||||
state__group="unstarted",
|
||||
issue_module__module_id=OuterRef("pk"),
|
||||
issue_module__deleted_at__isnull=True,
|
||||
)
|
||||
.values("issue_module__module_id")
|
||||
.annotate(cnt=Count("pk"))
|
||||
@@ -97,7 +90,6 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
Issue.issue_objects.filter(
|
||||
state__group="backlog",
|
||||
issue_module__module_id=OuterRef("pk"),
|
||||
issue_module__deleted_at__isnull=True,
|
||||
)
|
||||
.values("issue_module__module_id")
|
||||
.annotate(cnt=Count("pk"))
|
||||
@@ -106,7 +98,6 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
total_issues = (
|
||||
Issue.issue_objects.filter(
|
||||
issue_module__module_id=OuterRef("pk"),
|
||||
issue_module__deleted_at__isnull=True,
|
||||
)
|
||||
.values("issue_module__module_id")
|
||||
.annotate(cnt=Count("pk"))
|
||||
@@ -117,7 +108,6 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
estimate_point__estimate__type="points",
|
||||
state__group="completed",
|
||||
issue_module__module_id=OuterRef("pk"),
|
||||
issue_module__deleted_at__isnull=True,
|
||||
)
|
||||
.values("issue_module__module_id")
|
||||
.annotate(
|
||||
@@ -132,7 +122,6 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
Issue.issue_objects.filter(
|
||||
estimate_point__estimate__type="points",
|
||||
issue_module__module_id=OuterRef("pk"),
|
||||
issue_module__deleted_at__isnull=True,
|
||||
)
|
||||
.values("issue_module__module_id")
|
||||
.annotate(
|
||||
@@ -147,7 +136,6 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
estimate_point__estimate__type="points",
|
||||
state__group="backlog",
|
||||
issue_module__module_id=OuterRef("pk"),
|
||||
issue_module__deleted_at__isnull=True,
|
||||
)
|
||||
.values("issue_module__module_id")
|
||||
.annotate(
|
||||
@@ -162,7 +150,6 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
estimate_point__estimate__type="points",
|
||||
state__group="unstarted",
|
||||
issue_module__module_id=OuterRef("pk"),
|
||||
issue_module__deleted_at__isnull=True,
|
||||
)
|
||||
.values("issue_module__module_id")
|
||||
.annotate(
|
||||
@@ -177,7 +164,6 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
estimate_point__estimate__type="points",
|
||||
state__group="started",
|
||||
issue_module__module_id=OuterRef("pk"),
|
||||
issue_module__deleted_at__isnull=True,
|
||||
)
|
||||
.values("issue_module__module_id")
|
||||
.annotate(
|
||||
@@ -192,7 +178,6 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
estimate_point__estimate__type="points",
|
||||
state__group="cancelled",
|
||||
issue_module__module_id=OuterRef("pk"),
|
||||
issue_module__deleted_at__isnull=True,
|
||||
)
|
||||
.values("issue_module__module_id")
|
||||
.annotate(
|
||||
@@ -349,7 +334,6 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
parent__isnull=False,
|
||||
issue_module__module_id=pk,
|
||||
issue_module__deleted_at__isnull=True,
|
||||
)
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
@@ -373,7 +357,6 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
assignee_distribution = (
|
||||
Issue.issue_objects.filter(
|
||||
issue_module__module_id=pk,
|
||||
issue_module__deleted_at__isnull=True,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
)
|
||||
@@ -381,31 +364,12 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
.annotate(last_name=F("assignees__last_name"))
|
||||
.annotate(assignee_id=F("assignees__id"))
|
||||
.annotate(display_name=F("assignees__display_name"))
|
||||
.annotate(
|
||||
avatar_url=Case(
|
||||
# If `avatar_asset` exists, use it to generate the asset URL
|
||||
When(
|
||||
assignees__avatar_asset__isnull=False,
|
||||
then=Concat(
|
||||
Value("/api/assets/v2/static/"),
|
||||
"assignees__avatar_asset", # Assuming avatar_asset has an id or relevant field
|
||||
Value("/"),
|
||||
),
|
||||
),
|
||||
# If `avatar_asset` is None, fall back to using `avatar` field directly
|
||||
When(
|
||||
assignees__avatar_asset__isnull=True,
|
||||
then="assignees__avatar",
|
||||
),
|
||||
default=Value(None),
|
||||
output_field=models.CharField(),
|
||||
)
|
||||
)
|
||||
.annotate(avatar=F("assignees__avatar"))
|
||||
.values(
|
||||
"first_name",
|
||||
"last_name",
|
||||
"assignee_id",
|
||||
"avatar_url",
|
||||
"avatar",
|
||||
"display_name",
|
||||
)
|
||||
.annotate(
|
||||
@@ -473,9 +437,7 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
)
|
||||
.order_by("label_name")
|
||||
)
|
||||
data["estimate_distribution"][
|
||||
"assignees"
|
||||
] = assignee_distribution
|
||||
data["estimate_distribution"]["assignees"] = assignee_distribution
|
||||
data["estimate_distribution"]["labels"] = label_distribution
|
||||
|
||||
if modules and modules.start_date and modules.target_date:
|
||||
@@ -492,7 +454,6 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
assignee_distribution = (
|
||||
Issue.issue_objects.filter(
|
||||
issue_module__module_id=pk,
|
||||
issue_module__deleted_at__isnull=True,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
)
|
||||
@@ -500,31 +461,12 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
.annotate(last_name=F("assignees__last_name"))
|
||||
.annotate(assignee_id=F("assignees__id"))
|
||||
.annotate(display_name=F("assignees__display_name"))
|
||||
.annotate(
|
||||
avatar_url=Case(
|
||||
# If `avatar_asset` exists, use it to generate the asset URL
|
||||
When(
|
||||
assignees__avatar_asset__isnull=False,
|
||||
then=Concat(
|
||||
Value("/api/assets/v2/static/"),
|
||||
"assignees__avatar_asset", # Assuming avatar_asset has an id or relevant field
|
||||
Value("/"),
|
||||
),
|
||||
),
|
||||
# If `avatar_asset` is None, fall back to using `avatar` field directly
|
||||
When(
|
||||
assignees__avatar_asset__isnull=True,
|
||||
then="assignees__avatar",
|
||||
),
|
||||
default=Value(None),
|
||||
output_field=models.CharField(),
|
||||
)
|
||||
)
|
||||
.annotate(avatar=F("assignees__avatar"))
|
||||
.values(
|
||||
"first_name",
|
||||
"last_name",
|
||||
"assignee_id",
|
||||
"avatar_url",
|
||||
"avatar",
|
||||
"display_name",
|
||||
)
|
||||
.annotate(
|
||||
@@ -562,7 +504,6 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
label_distribution = (
|
||||
Issue.issue_objects.filter(
|
||||
issue_module__module_id=pk,
|
||||
issue_module__deleted_at__isnull=True,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
)
|
||||
|
||||
@@ -18,11 +18,8 @@ from django.db.models import (
|
||||
Value,
|
||||
Sum,
|
||||
FloatField,
|
||||
Case,
|
||||
When,
|
||||
)
|
||||
from django.db import models
|
||||
from django.db.models.functions import Coalesce, Cast, Concat
|
||||
from django.db.models.functions import Coalesce, Cast
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.utils import timezone
|
||||
|
||||
@@ -33,7 +30,6 @@ from rest_framework.response import Response
|
||||
# Module imports
|
||||
from plane.app.permissions import (
|
||||
ProjectEntityPermission,
|
||||
ProjectLitePermission,
|
||||
allow_permission,
|
||||
ROLE,
|
||||
)
|
||||
@@ -85,7 +81,6 @@ class ModuleViewSet(BaseViewSet):
|
||||
Issue.issue_objects.filter(
|
||||
state__group="cancelled",
|
||||
issue_module__module_id=OuterRef("pk"),
|
||||
issue_module__deleted_at__isnull=True,
|
||||
)
|
||||
.values("issue_module__module_id")
|
||||
.annotate(cnt=Count("pk"))
|
||||
@@ -95,7 +90,6 @@ class ModuleViewSet(BaseViewSet):
|
||||
Issue.issue_objects.filter(
|
||||
state__group="completed",
|
||||
issue_module__module_id=OuterRef("pk"),
|
||||
issue_module__deleted_at__isnull=True,
|
||||
)
|
||||
.values("issue_module__module_id")
|
||||
.annotate(cnt=Count("pk"))
|
||||
@@ -105,7 +99,6 @@ class ModuleViewSet(BaseViewSet):
|
||||
Issue.issue_objects.filter(
|
||||
state__group="started",
|
||||
issue_module__module_id=OuterRef("pk"),
|
||||
issue_module__deleted_at__isnull=True,
|
||||
)
|
||||
.values("issue_module__module_id")
|
||||
.annotate(cnt=Count("pk"))
|
||||
@@ -115,7 +108,6 @@ class ModuleViewSet(BaseViewSet):
|
||||
Issue.issue_objects.filter(
|
||||
state__group="unstarted",
|
||||
issue_module__module_id=OuterRef("pk"),
|
||||
issue_module__deleted_at__isnull=True,
|
||||
)
|
||||
.values("issue_module__module_id")
|
||||
.annotate(cnt=Count("pk"))
|
||||
@@ -125,7 +117,6 @@ class ModuleViewSet(BaseViewSet):
|
||||
Issue.issue_objects.filter(
|
||||
state__group="backlog",
|
||||
issue_module__module_id=OuterRef("pk"),
|
||||
issue_module__deleted_at__isnull=True,
|
||||
)
|
||||
.values("issue_module__module_id")
|
||||
.annotate(cnt=Count("pk"))
|
||||
@@ -134,7 +125,6 @@ class ModuleViewSet(BaseViewSet):
|
||||
total_issues = (
|
||||
Issue.issue_objects.filter(
|
||||
issue_module__module_id=OuterRef("pk"),
|
||||
issue_module__deleted_at__isnull=True,
|
||||
)
|
||||
.values("issue_module__module_id")
|
||||
.annotate(cnt=Count("pk"))
|
||||
@@ -145,7 +135,6 @@ class ModuleViewSet(BaseViewSet):
|
||||
estimate_point__estimate__type="points",
|
||||
state__group="completed",
|
||||
issue_module__module_id=OuterRef("pk"),
|
||||
issue_module__deleted_at__isnull=True,
|
||||
)
|
||||
.values("issue_module__module_id")
|
||||
.annotate(
|
||||
@@ -160,7 +149,6 @@ class ModuleViewSet(BaseViewSet):
|
||||
Issue.issue_objects.filter(
|
||||
estimate_point__estimate__type="points",
|
||||
issue_module__module_id=OuterRef("pk"),
|
||||
issue_module__deleted_at__isnull=True,
|
||||
)
|
||||
.values("issue_module__module_id")
|
||||
.annotate(
|
||||
@@ -175,7 +163,6 @@ class ModuleViewSet(BaseViewSet):
|
||||
estimate_point__estimate__type="points",
|
||||
state__group="backlog",
|
||||
issue_module__module_id=OuterRef("pk"),
|
||||
issue_module__deleted_at__isnull=True,
|
||||
)
|
||||
.values("issue_module__module_id")
|
||||
.annotate(
|
||||
@@ -190,7 +177,6 @@ class ModuleViewSet(BaseViewSet):
|
||||
estimate_point__estimate__type="points",
|
||||
state__group="unstarted",
|
||||
issue_module__module_id=OuterRef("pk"),
|
||||
issue_module__deleted_at__isnull=True,
|
||||
)
|
||||
.values("issue_module__module_id")
|
||||
.annotate(
|
||||
@@ -205,7 +191,6 @@ class ModuleViewSet(BaseViewSet):
|
||||
estimate_point__estimate__type="points",
|
||||
state__group="started",
|
||||
issue_module__module_id=OuterRef("pk"),
|
||||
issue_module__deleted_at__isnull=True,
|
||||
)
|
||||
.values("issue_module__module_id")
|
||||
.annotate(
|
||||
@@ -220,7 +205,6 @@ class ModuleViewSet(BaseViewSet):
|
||||
estimate_point__estimate__type="points",
|
||||
state__group="cancelled",
|
||||
issue_module__module_id=OuterRef("pk"),
|
||||
issue_module__deleted_at__isnull=True,
|
||||
)
|
||||
.values("issue_module__module_id")
|
||||
.annotate(
|
||||
@@ -333,12 +317,8 @@ class ModuleViewSet(BaseViewSet):
|
||||
.order_by("-is_favorite", "-created_at")
|
||||
)
|
||||
|
||||
@allow_permission(
|
||||
[
|
||||
ROLE.ADMIN,
|
||||
ROLE.MEMBER,
|
||||
]
|
||||
)
|
||||
allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER])
|
||||
|
||||
def create(self, request, slug, project_id):
|
||||
project = Project.objects.get(workspace__slug=slug, pk=project_id)
|
||||
serializer = ModuleWriteSerializer(
|
||||
@@ -401,7 +381,8 @@ class ModuleViewSet(BaseViewSet):
|
||||
return Response(module, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST])
|
||||
|
||||
def list(self, request, slug, project_id):
|
||||
queryset = self.get_queryset().filter(archived_at__isnull=True)
|
||||
if self.fields:
|
||||
@@ -449,7 +430,8 @@ class ModuleViewSet(BaseViewSet):
|
||||
)
|
||||
return Response(modules, status=status.HTTP_200_OK)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER])
|
||||
|
||||
def retrieve(self, request, slug, project_id, pk):
|
||||
queryset = (
|
||||
self.get_queryset()
|
||||
@@ -460,7 +442,6 @@ class ModuleViewSet(BaseViewSet):
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
parent__isnull=False,
|
||||
issue_module__module_id=pk,
|
||||
issue_module__deleted_at__isnull=True,
|
||||
)
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
@@ -490,7 +471,6 @@ class ModuleViewSet(BaseViewSet):
|
||||
assignee_distribution = (
|
||||
Issue.issue_objects.filter(
|
||||
issue_module__module_id=pk,
|
||||
issue_module__deleted_at__isnull=True,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
)
|
||||
@@ -498,31 +478,12 @@ class ModuleViewSet(BaseViewSet):
|
||||
.annotate(last_name=F("assignees__last_name"))
|
||||
.annotate(assignee_id=F("assignees__id"))
|
||||
.annotate(display_name=F("assignees__display_name"))
|
||||
.annotate(
|
||||
avatar_url=Case(
|
||||
# If `avatar_asset` exists, use it to generate the asset URL
|
||||
When(
|
||||
assignees__avatar_asset__isnull=False,
|
||||
then=Concat(
|
||||
Value("/api/assets/v2/static/"),
|
||||
"assignees__avatar_asset", # Assuming avatar_asset has an id or relevant field
|
||||
Value("/"),
|
||||
),
|
||||
),
|
||||
# If `avatar_asset` is None, fall back to using `avatar` field directly
|
||||
When(
|
||||
assignees__avatar_asset__isnull=True,
|
||||
then="assignees__avatar",
|
||||
),
|
||||
default=Value(None),
|
||||
output_field=models.CharField(),
|
||||
)
|
||||
)
|
||||
.annotate(avatar=F("assignees__avatar"))
|
||||
.values(
|
||||
"first_name",
|
||||
"last_name",
|
||||
"assignee_id",
|
||||
"avatar_url",
|
||||
"avatar",
|
||||
"display_name",
|
||||
)
|
||||
.annotate(
|
||||
@@ -556,7 +517,6 @@ class ModuleViewSet(BaseViewSet):
|
||||
label_distribution = (
|
||||
Issue.issue_objects.filter(
|
||||
issue_module__module_id=pk,
|
||||
issue_module__deleted_at__isnull=True,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
)
|
||||
@@ -608,7 +568,6 @@ class ModuleViewSet(BaseViewSet):
|
||||
assignee_distribution = (
|
||||
Issue.issue_objects.filter(
|
||||
issue_module__module_id=pk,
|
||||
issue_module__deleted_at__isnull=True,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
)
|
||||
@@ -616,31 +575,12 @@ class ModuleViewSet(BaseViewSet):
|
||||
.annotate(last_name=F("assignees__last_name"))
|
||||
.annotate(assignee_id=F("assignees__id"))
|
||||
.annotate(display_name=F("assignees__display_name"))
|
||||
.annotate(
|
||||
avatar_url=Case(
|
||||
# If `avatar_asset` exists, use it to generate the asset URL
|
||||
When(
|
||||
assignees__avatar_asset__isnull=False,
|
||||
then=Concat(
|
||||
Value("/api/assets/v2/static/"),
|
||||
"assignees__avatar_asset", # Assuming avatar_asset has an id or relevant field
|
||||
Value("/"),
|
||||
),
|
||||
),
|
||||
# If `avatar_asset` is None, fall back to using `avatar` field directly
|
||||
When(
|
||||
assignees__avatar_asset__isnull=True,
|
||||
then="assignees__avatar",
|
||||
),
|
||||
default=Value(None),
|
||||
output_field=models.CharField(),
|
||||
)
|
||||
)
|
||||
.annotate(avatar=F("assignees__avatar"))
|
||||
.values(
|
||||
"first_name",
|
||||
"last_name",
|
||||
"assignee_id",
|
||||
"avatar_url",
|
||||
"avatar",
|
||||
"display_name",
|
||||
)
|
||||
.annotate(
|
||||
@@ -678,7 +618,6 @@ class ModuleViewSet(BaseViewSet):
|
||||
label_distribution = (
|
||||
Issue.issue_objects.filter(
|
||||
issue_module__module_id=pk,
|
||||
issue_module__deleted_at__isnull=True,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
)
|
||||
@@ -723,13 +662,7 @@ class ModuleViewSet(BaseViewSet):
|
||||
"labels": label_distribution,
|
||||
"completion_chart": {},
|
||||
}
|
||||
|
||||
if (
|
||||
modules
|
||||
and modules.start_date
|
||||
and modules.target_date
|
||||
and modules.total_issues > 0
|
||||
):
|
||||
if modules and modules.start_date and modules.target_date:
|
||||
data["distribution"]["completion_chart"] = burndown_plot(
|
||||
queryset=modules,
|
||||
slug=slug,
|
||||
@@ -895,9 +828,6 @@ class ModuleLinkViewSet(BaseViewSet):
|
||||
|
||||
class ModuleFavoriteViewSet(BaseViewSet):
|
||||
model = UserFavorite
|
||||
permission_classes = [
|
||||
ProjectLitePermission,
|
||||
]
|
||||
|
||||
def get_queryset(self):
|
||||
return self.filter_queryset(
|
||||
@@ -931,7 +861,7 @@ class ModuleFavoriteViewSet(BaseViewSet):
|
||||
|
||||
class ModuleUserPropertiesEndpoint(BaseAPIView):
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST])
|
||||
def patch(self, request, slug, project_id, module_id):
|
||||
module_properties = ModuleUserProperties.objects.get(
|
||||
user=request.user,
|
||||
@@ -954,7 +884,7 @@ class ModuleUserPropertiesEndpoint(BaseAPIView):
|
||||
serializer = ModuleUserPropertiesSerializer(module_properties)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST])
|
||||
def get(self, request, slug, project_id, module_id):
|
||||
module_properties, _ = ModuleUserProperties.objects.get_or_create(
|
||||
user=request.user,
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
# Python imports
|
||||
import json
|
||||
|
||||
from django.db.models import F, Func, OuterRef, Q, Subquery
|
||||
from django.db.models import (
|
||||
F,
|
||||
Func,
|
||||
OuterRef,
|
||||
Q,
|
||||
)
|
||||
|
||||
# Django Imports
|
||||
from django.utils import timezone
|
||||
@@ -19,11 +24,10 @@ from plane.app.serializers import (
|
||||
from plane.bgtasks.issue_activities_task import issue_activity
|
||||
from plane.db.models import (
|
||||
Issue,
|
||||
FileAsset,
|
||||
IssueAttachment,
|
||||
IssueLink,
|
||||
ModuleIssue,
|
||||
Project,
|
||||
CycleIssue,
|
||||
)
|
||||
from plane.utils.grouper import (
|
||||
issue_group_values,
|
||||
@@ -58,17 +62,10 @@ class ModuleIssueViewSet(BaseViewSet):
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
issue_module__module_id=self.kwargs.get("module_id"),
|
||||
issue_module__deleted_at__isnull=True,
|
||||
)
|
||||
.select_related("workspace", "project", "state", "parent")
|
||||
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||
.annotate(
|
||||
cycle_id=Subquery(
|
||||
CycleIssue.objects.filter(
|
||||
issue=OuterRef("id"), deleted_at__isnull=True
|
||||
).values("cycle_id")[:1]
|
||||
)
|
||||
)
|
||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||
.annotate(
|
||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||
.order_by()
|
||||
@@ -76,9 +73,8 @@ class ModuleIssueViewSet(BaseViewSet):
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
attachment_count=FileAsset.objects.filter(
|
||||
issue_id=OuterRef("id"),
|
||||
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
|
||||
attachment_count=IssueAttachment.objects.filter(
|
||||
issue=OuterRef("id")
|
||||
)
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
@@ -95,12 +91,7 @@ class ModuleIssueViewSet(BaseViewSet):
|
||||
).distinct()
|
||||
|
||||
@method_decorator(gzip_page)
|
||||
@allow_permission(
|
||||
[
|
||||
ROLE.ADMIN,
|
||||
ROLE.MEMBER,
|
||||
]
|
||||
)
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER])
|
||||
def list(self, request, slug, project_id, module_id):
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
issue_queryset = self.get_queryset().filter(**filters)
|
||||
@@ -160,10 +151,10 @@ class ModuleIssueViewSet(BaseViewSet):
|
||||
group_by_field_name=group_by,
|
||||
sub_group_by_field_name=sub_group_by,
|
||||
count_filter=Q(
|
||||
Q(issue_intake__status=1)
|
||||
| Q(issue_intake__status=-1)
|
||||
| Q(issue_intake__status=2)
|
||||
| Q(issue_intake__isnull=True),
|
||||
Q(issue_inbox__status=1)
|
||||
| Q(issue_inbox__status=-1)
|
||||
| Q(issue_inbox__status=2)
|
||||
| Q(issue_inbox__isnull=True),
|
||||
archived_at__isnull=True,
|
||||
is_draft=False,
|
||||
),
|
||||
@@ -189,10 +180,10 @@ class ModuleIssueViewSet(BaseViewSet):
|
||||
),
|
||||
group_by_field_name=group_by,
|
||||
count_filter=Q(
|
||||
Q(issue_intake__status=1)
|
||||
| Q(issue_intake__status=-1)
|
||||
| Q(issue_intake__status=2)
|
||||
| Q(issue_intake__isnull=True),
|
||||
Q(issue_inbox__status=1)
|
||||
| Q(issue_inbox__status=-1)
|
||||
| Q(issue_inbox__status=2)
|
||||
| Q(issue_inbox__isnull=True),
|
||||
archived_at__isnull=True,
|
||||
is_draft=False,
|
||||
),
|
||||
|
||||
@@ -41,7 +41,7 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
|
||||
)
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST],
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST],
|
||||
level="WORKSPACE",
|
||||
)
|
||||
def list(self, request, slug):
|
||||
@@ -53,9 +53,9 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
|
||||
mentioned = request.GET.get("mentioned", False)
|
||||
q_filters = Q()
|
||||
|
||||
intake_issue = Issue.objects.filter(
|
||||
inbox_issue = Issue.objects.filter(
|
||||
pk=OuterRef("entity_identifier"),
|
||||
issue_intake__status__in=[0, 2, -2],
|
||||
issue_inbox__status__in=[0, 2, -2],
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
)
|
||||
|
||||
@@ -64,8 +64,7 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
|
||||
workspace__slug=slug, receiver_id=request.user.id
|
||||
)
|
||||
.filter(entity_name="issue")
|
||||
.annotate(is_inbox_issue=Exists(intake_issue))
|
||||
.annotate(is_intake_issue=Exists(intake_issue))
|
||||
.annotate(is_inbox_issue=Exists(inbox_issue))
|
||||
.annotate(
|
||||
is_mentioned_notification=Case(
|
||||
When(sender__icontains="mentioned", then=True),
|
||||
@@ -175,7 +174,7 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST],
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST],
|
||||
level="WORKSPACE",
|
||||
)
|
||||
def partial_update(self, request, slug, pk):
|
||||
@@ -196,7 +195,8 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST],
|
||||
level="WORKSPACE",
|
||||
)
|
||||
def mark_read(self, request, slug, pk):
|
||||
notification = Notification.objects.get(
|
||||
@@ -208,7 +208,7 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE"
|
||||
)
|
||||
def mark_unread(self, request, slug, pk):
|
||||
notification = Notification.objects.get(
|
||||
@@ -220,7 +220,7 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE"
|
||||
)
|
||||
def archive(self, request, slug, pk):
|
||||
notification = Notification.objects.get(
|
||||
@@ -232,7 +232,7 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE"
|
||||
)
|
||||
def unarchive(self, request, slug, pk):
|
||||
notification = Notification.objects.get(
|
||||
@@ -246,7 +246,7 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
|
||||
|
||||
class UnreadNotificationEndpoint(BaseAPIView):
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST],
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST],
|
||||
level="WORKSPACE",
|
||||
)
|
||||
def get(self, request, slug):
|
||||
@@ -287,7 +287,7 @@ class UnreadNotificationEndpoint(BaseAPIView):
|
||||
|
||||
class MarkAllReadNotificationViewSet(BaseViewSet):
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE"
|
||||
)
|
||||
def create(self, request, slug):
|
||||
snoozed = request.data.get("snoozed", False)
|
||||
|
||||
@@ -18,7 +18,7 @@ from django.db.models.functions import Coalesce
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
|
||||
# Module imports
|
||||
|
||||
from plane.app.permissions import allow_permission, ROLE
|
||||
from plane.app.serializers import (
|
||||
PageLogSerializer,
|
||||
@@ -32,10 +32,11 @@ from plane.db.models import (
|
||||
UserFavorite,
|
||||
ProjectMember,
|
||||
ProjectPage,
|
||||
Project,
|
||||
)
|
||||
from plane.utils.error_codes import ERROR_CODES
|
||||
# Module imports
|
||||
from ..base import BaseAPIView, BaseViewSet
|
||||
|
||||
from plane.bgtasks.page_transaction_task import page_transaction
|
||||
from plane.bgtasks.page_version_task import page_version
|
||||
from plane.bgtasks.recent_visited_task import recent_visited_task
|
||||
@@ -119,7 +120,7 @@ class PageViewSet(BaseViewSet):
|
||||
.distinct()
|
||||
)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def create(self, request, slug, project_id):
|
||||
serializer = PageSerializer(
|
||||
data=request.data,
|
||||
@@ -141,7 +142,7 @@ class PageViewSet(BaseViewSet):
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def partial_update(self, request, slug, project_id, pk):
|
||||
try:
|
||||
page = Page.objects.get(
|
||||
@@ -207,38 +208,9 @@ class PageViewSet(BaseViewSet):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@allow_permission(
|
||||
[
|
||||
ROLE.ADMIN,
|
||||
ROLE.MEMBER,
|
||||
ROLE.GUEST,
|
||||
]
|
||||
)
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER])
|
||||
def retrieve(self, request, slug, project_id, pk=None):
|
||||
page = self.get_queryset().filter(pk=pk).first()
|
||||
project = Project.objects.get(pk=project_id)
|
||||
|
||||
"""
|
||||
if the role is guest and guest_view_all_features is false and owned by is not
|
||||
the requesting user then dont show the page
|
||||
"""
|
||||
|
||||
if (
|
||||
ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
member=request.user,
|
||||
role=5,
|
||||
is_active=True,
|
||||
).exists()
|
||||
and not project.guest_view_all_features
|
||||
and not page.owned_by == request.user
|
||||
):
|
||||
return Response(
|
||||
{"error": "You are not allowed to view this page"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
if page is None:
|
||||
return Response(
|
||||
{"error": "Page not found"},
|
||||
@@ -262,7 +234,7 @@ class PageViewSet(BaseViewSet):
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def lock(self, request, slug, project_id, pk):
|
||||
page = Page.objects.filter(
|
||||
pk=pk, workspace__slug=slug, projects__id=project_id
|
||||
@@ -272,7 +244,7 @@ class PageViewSet(BaseViewSet):
|
||||
page.save()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def unlock(self, request, slug, project_id, pk):
|
||||
page = Page.objects.filter(
|
||||
pk=pk, workspace__slug=slug, projects__id=project_id
|
||||
@@ -283,7 +255,7 @@ class PageViewSet(BaseViewSet):
|
||||
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def access(self, request, slug, project_id, pk):
|
||||
access = request.data.get("access", 0)
|
||||
page = Page.objects.filter(
|
||||
@@ -306,31 +278,13 @@ class PageViewSet(BaseViewSet):
|
||||
page.save()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@allow_permission(
|
||||
[
|
||||
ROLE.ADMIN,
|
||||
ROLE.MEMBER,
|
||||
ROLE.GUEST,
|
||||
]
|
||||
)
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER])
|
||||
def list(self, request, slug, project_id):
|
||||
queryset = self.get_queryset()
|
||||
project = Project.objects.get(pk=project_id)
|
||||
if (
|
||||
ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
member=request.user,
|
||||
role=5,
|
||||
is_active=True,
|
||||
).exists()
|
||||
and not project.guest_view_all_features
|
||||
):
|
||||
queryset = queryset.filter(owned_by=request.user)
|
||||
pages = PageSerializer(queryset, many=True).data
|
||||
return Response(pages, status=status.HTTP_200_OK)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def archive(self, request, slug, project_id, pk):
|
||||
page = Page.objects.get(
|
||||
pk=pk, workspace__slug=slug, projects__id=project_id
|
||||
@@ -365,7 +319,7 @@ class PageViewSet(BaseViewSet):
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def unarchive(self, request, slug, project_id, pk):
|
||||
page = Page.objects.get(
|
||||
pk=pk, workspace__slug=slug, projects__id=project_id
|
||||
@@ -523,13 +477,7 @@ class SubPagesEndpoint(BaseAPIView):
|
||||
|
||||
class PagesDescriptionViewSet(BaseViewSet):
|
||||
|
||||
@allow_permission(
|
||||
[
|
||||
ROLE.ADMIN,
|
||||
ROLE.MEMBER,
|
||||
ROLE.GUEST,
|
||||
]
|
||||
)
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER])
|
||||
def retrieve(self, request, slug, project_id, pk):
|
||||
page = (
|
||||
Page.objects.filter(
|
||||
@@ -559,7 +507,7 @@ class PagesDescriptionViewSet(BaseViewSet):
|
||||
)
|
||||
return response
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST])
|
||||
def partial_update(self, request, slug, project_id, pk):
|
||||
page = (
|
||||
Page.objects.filter(
|
||||
|
||||
@@ -15,7 +15,7 @@ from plane.app.permissions import allow_permission, ROLE
|
||||
class PageVersionEndpoint(BaseAPIView):
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST]
|
||||
)
|
||||
def get(self, request, slug, project_id, page_id, pk=None):
|
||||
# Check if pk is provided
|
||||
|
||||
@@ -38,7 +38,7 @@ from plane.app.permissions import (
|
||||
from plane.db.models import (
|
||||
UserFavorite,
|
||||
Cycle,
|
||||
Intake,
|
||||
Inbox,
|
||||
DeployBoard,
|
||||
IssueUserProperty,
|
||||
Issue,
|
||||
@@ -53,7 +53,6 @@ from plane.db.models import (
|
||||
from plane.utils.cache import cache_response
|
||||
from plane.bgtasks.webhook_task import model_activity
|
||||
from plane.bgtasks.recent_visited_task import recent_visited_task
|
||||
from plane.utils.exception_logger import log_exception
|
||||
|
||||
|
||||
class ProjectViewSet(BaseViewSet):
|
||||
@@ -72,6 +71,13 @@ class ProjectViewSet(BaseViewSet):
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.filter(
|
||||
Q(
|
||||
project_projectmember__member=self.request.user,
|
||||
project_projectmember__is_active=True,
|
||||
)
|
||||
| Q(network=2)
|
||||
)
|
||||
.select_related(
|
||||
"workspace",
|
||||
"workspace__owner",
|
||||
@@ -149,7 +155,7 @@ class ProjectViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST],
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST],
|
||||
level="WORKSPACE",
|
||||
)
|
||||
def list(self, request, slug):
|
||||
@@ -159,31 +165,6 @@ class ProjectViewSet(BaseViewSet):
|
||||
if field
|
||||
]
|
||||
projects = self.get_queryset().order_by("sort_order", "name")
|
||||
if WorkspaceMember.objects.filter(
|
||||
member=request.user,
|
||||
workspace__slug=slug,
|
||||
is_active=True,
|
||||
role=5,
|
||||
).exists():
|
||||
projects = projects.filter(
|
||||
project_projectmember__member=self.request.user,
|
||||
project_projectmember__is_active=True,
|
||||
)
|
||||
|
||||
if WorkspaceMember.objects.filter(
|
||||
member=request.user,
|
||||
workspace__slug=slug,
|
||||
is_active=True,
|
||||
role=15,
|
||||
).exists():
|
||||
projects = projects.filter(
|
||||
Q(
|
||||
project_projectmember__member=self.request.user,
|
||||
project_projectmember__is_active=True,
|
||||
)
|
||||
| Q(network=2)
|
||||
)
|
||||
|
||||
if request.GET.get("per_page", False) and request.GET.get(
|
||||
"cursor", False
|
||||
):
|
||||
@@ -196,13 +177,24 @@ class ProjectViewSet(BaseViewSet):
|
||||
).data,
|
||||
)
|
||||
|
||||
if WorkspaceMember.objects.filter(
|
||||
member=request.user,
|
||||
workspace__slug=slug,
|
||||
is_active=True,
|
||||
role__in=[5, 10],
|
||||
).exists():
|
||||
projects = projects.filter(
|
||||
project_projectmember__member=self.request.user,
|
||||
project_projectmember__is_active=True,
|
||||
)
|
||||
|
||||
projects = ProjectListSerializer(
|
||||
projects, many=True, fields=fields if fields else None
|
||||
).data
|
||||
return Response(projects, status=status.HTTP_200_OK)
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST],
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST],
|
||||
level="WORKSPACE",
|
||||
)
|
||||
def retrieve(self, request, slug, pk):
|
||||
@@ -414,24 +406,10 @@ class ProjectViewSet(BaseViewSet):
|
||||
status=status.HTTP_410_GONE,
|
||||
)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
|
||||
def partial_update(self, request, slug, pk=None):
|
||||
try:
|
||||
if not ProjectMember.objects.filter(
|
||||
member=request.user,
|
||||
workspace__slug=slug,
|
||||
project_id=pk,
|
||||
role=20,
|
||||
is_active=True,
|
||||
).exists():
|
||||
return Response(
|
||||
{"error": "You don't have the required permissions."},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
intake_view = request.data.get(
|
||||
"inbox_view", request.data.get("intake_view", False)
|
||||
)
|
||||
|
||||
project = Project.objects.get(pk=pk)
|
||||
current_instance = json.dumps(
|
||||
@@ -445,33 +423,25 @@ class ProjectViewSet(BaseViewSet):
|
||||
|
||||
serializer = ProjectSerializer(
|
||||
project,
|
||||
data={
|
||||
**request.data,
|
||||
"intake_view": intake_view,
|
||||
},
|
||||
data={**request.data},
|
||||
context={"workspace_id": workspace.id},
|
||||
partial=True,
|
||||
)
|
||||
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
if intake_view:
|
||||
intake = Intake.objects.filter(
|
||||
if serializer.data["inbox_view"]:
|
||||
Inbox.objects.get_or_create(
|
||||
name=f"{project.name} Inbox",
|
||||
project=project,
|
||||
is_default=True,
|
||||
).first()
|
||||
if not intake:
|
||||
Intake.objects.create(
|
||||
name=f"{project.name} Intake",
|
||||
project=project,
|
||||
is_default=True,
|
||||
)
|
||||
)
|
||||
|
||||
# Create the triage state in Backlog group
|
||||
State.objects.get_or_create(
|
||||
name="Triage",
|
||||
group="triage",
|
||||
description="Default state for managing all Intake Issues",
|
||||
description="Default state for managing all Inbox Issues",
|
||||
project_id=pk,
|
||||
color="#ff7700",
|
||||
is_triage=True,
|
||||
@@ -515,44 +485,6 @@ class ProjectViewSet(BaseViewSet):
|
||||
status=status.HTTP_410_GONE,
|
||||
)
|
||||
|
||||
def destroy(self, request, slug, pk):
|
||||
if (
|
||||
WorkspaceMember.objects.filter(
|
||||
member=request.user,
|
||||
workspace__slug=slug,
|
||||
is_active=True,
|
||||
role=20,
|
||||
).exists()
|
||||
or ProjectMember.objects.filter(
|
||||
member=request.user,
|
||||
workspace__slug=slug,
|
||||
project_id=pk,
|
||||
role=20,
|
||||
is_active=True,
|
||||
).exists()
|
||||
):
|
||||
project = Project.objects.get(pk=pk)
|
||||
project.delete()
|
||||
|
||||
# Delete the project members
|
||||
DeployBoard.objects.filter(
|
||||
project_id=pk,
|
||||
workspace__slug=slug,
|
||||
).delete()
|
||||
|
||||
# Delete the user favorite
|
||||
UserFavorite.objects.filter(
|
||||
project_id=pk,
|
||||
workspace__slug=slug,
|
||||
).delete()
|
||||
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
else:
|
||||
return Response(
|
||||
{"error": "You don't have the required permissions."},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
|
||||
class ProjectArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
|
||||
@@ -727,22 +659,18 @@ class ProjectPublicCoverImagesEndpoint(BaseAPIView):
|
||||
"Prefix": "static/project-cover/",
|
||||
}
|
||||
|
||||
try:
|
||||
response = s3.list_objects_v2(**params)
|
||||
# Extracting file keys from the response
|
||||
if "Contents" in response:
|
||||
for content in response["Contents"]:
|
||||
if not content["Key"].endswith(
|
||||
"/"
|
||||
): # This line ensures we're only getting files, not "sub-folders"
|
||||
files.append(
|
||||
f"https://{settings.AWS_STORAGE_BUCKET_NAME}.s3.{settings.AWS_REGION}.amazonaws.com/{content['Key']}"
|
||||
)
|
||||
response = s3.list_objects_v2(**params)
|
||||
# Extracting file keys from the response
|
||||
if "Contents" in response:
|
||||
for content in response["Contents"]:
|
||||
if not content["Key"].endswith(
|
||||
"/"
|
||||
): # This line ensures we're only getting files, not "sub-folders"
|
||||
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)
|
||||
except Exception as e:
|
||||
log_exception(e)
|
||||
return Response([], status=status.HTTP_200_OK)
|
||||
return Response(files, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class DeployBoardViewSet(BaseViewSet):
|
||||
@@ -765,7 +693,7 @@ class DeployBoardViewSet(BaseViewSet):
|
||||
def create(self, request, slug, project_id):
|
||||
comments = request.data.get("is_comments_enabled", False)
|
||||
reactions = request.data.get("is_reactions_enabled", False)
|
||||
intake = request.data.get("intake", None)
|
||||
inbox = request.data.get("inbox", None)
|
||||
votes = request.data.get("is_votes_enabled", False)
|
||||
views = request.data.get(
|
||||
"views",
|
||||
@@ -783,7 +711,7 @@ class DeployBoardViewSet(BaseViewSet):
|
||||
entity_identifier=project_id,
|
||||
project_id=project_id,
|
||||
)
|
||||
project_deploy_board.intake = intake
|
||||
project_deploy_board.inbox = inbox
|
||||
project_deploy_board.view_props = views
|
||||
project_deploy_board.is_votes_enabled = votes
|
||||
project_deploy_board.is_comments_enabled = comments
|
||||
|
||||
@@ -17,7 +17,7 @@ from rest_framework.permissions import AllowAny
|
||||
from .base import BaseViewSet, BaseAPIView
|
||||
from plane.app.serializers import ProjectMemberInviteSerializer
|
||||
|
||||
from plane.app.permissions import allow_permission, ROLE
|
||||
from plane.app.permissions import ProjectBasePermission
|
||||
|
||||
from plane.db.models import (
|
||||
ProjectMember,
|
||||
@@ -35,6 +35,10 @@ class ProjectInvitationsViewset(BaseViewSet):
|
||||
|
||||
search_fields = []
|
||||
|
||||
permission_classes = [
|
||||
ProjectBasePermission,
|
||||
]
|
||||
|
||||
def get_queryset(self):
|
||||
return self.filter_queryset(
|
||||
super()
|
||||
@@ -45,7 +49,6 @@ class ProjectInvitationsViewset(BaseViewSet):
|
||||
.select_related("workspace", "workspace__owner")
|
||||
)
|
||||
|
||||
@allow_permission([ROLE.ADMIN])
|
||||
def create(self, request, slug, project_id):
|
||||
emails = request.data.get("emails", [])
|
||||
|
||||
@@ -56,21 +59,24 @@ class ProjectInvitationsViewset(BaseViewSet):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
for email in emails:
|
||||
workspace_role = WorkspaceMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
member__email=email.get("email"),
|
||||
is_active=True,
|
||||
).role
|
||||
requesting_user = ProjectMember.objects.get(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
member_id=request.user.id,
|
||||
)
|
||||
|
||||
if workspace_role in [5, 20] and workspace_role != email.get(
|
||||
"role", 5
|
||||
):
|
||||
return Response(
|
||||
{
|
||||
"error": "You cannot invite a user with different role than workspace role"
|
||||
},
|
||||
)
|
||||
# Check if any invited user has an higher role
|
||||
if len(
|
||||
[
|
||||
email
|
||||
for email in emails
|
||||
if int(email.get("role", 10)) > requesting_user.role
|
||||
]
|
||||
):
|
||||
return Response(
|
||||
{"error": "You cannot invite a user with higher role"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
|
||||
@@ -91,7 +97,7 @@ class ProjectInvitationsViewset(BaseViewSet):
|
||||
settings.SECRET_KEY,
|
||||
algorithm="HS256",
|
||||
),
|
||||
role=email.get("role", 5),
|
||||
role=email.get("role", 10),
|
||||
created_by=request.user,
|
||||
)
|
||||
)
|
||||
@@ -164,7 +170,7 @@ class UserProjectInvitationsViewset(BaseViewSet):
|
||||
ProjectMember(
|
||||
project_id=project_id,
|
||||
member=request.user,
|
||||
role=workspace_role,
|
||||
role=15 if workspace_role >= 15 else 10,
|
||||
workspace=workspace,
|
||||
created_by=request.user,
|
||||
)
|
||||
|
||||
@@ -95,21 +95,9 @@ class ProjectMemberViewSet(BaseViewSet):
|
||||
member=member,
|
||||
is_active=True,
|
||||
).role
|
||||
if workspace_member_role in [20] and member_roles.get(member) in [
|
||||
5,
|
||||
15,
|
||||
]:
|
||||
return Response(
|
||||
{
|
||||
"error": "You cannot add a user with role lower than the workspace role"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
if workspace_member_role in [5] and member_roles.get(member) in [
|
||||
15,
|
||||
20,
|
||||
]:
|
||||
if workspace_member_role in [5, 10] and member_roles.get(
|
||||
member
|
||||
) in [15, 20]:
|
||||
return Response(
|
||||
{
|
||||
"error": "You cannot add a user with role higher than the workspace role"
|
||||
@@ -155,7 +143,7 @@ class ProjectMemberViewSet(BaseViewSet):
|
||||
bulk_project_members.append(
|
||||
ProjectMember(
|
||||
member_id=member.get("member_id"),
|
||||
role=member.get("role", 5),
|
||||
role=member.get("role", 10),
|
||||
project_id=project_id,
|
||||
workspace_id=project.workspace_id,
|
||||
sort_order=(
|
||||
@@ -201,7 +189,7 @@ class ProjectMemberViewSet(BaseViewSet):
|
||||
# Return the serialized data
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST])
|
||||
def list(self, request, slug, project_id):
|
||||
# Get the list of project members for the project
|
||||
project_members = ProjectMember.objects.filter(
|
||||
@@ -242,7 +230,7 @@ class ProjectMemberViewSet(BaseViewSet):
|
||||
member=project_member.member,
|
||||
is_active=True,
|
||||
).role
|
||||
if workspace_role in [5] and int(
|
||||
if workspace_role in [5, 10] and int(
|
||||
request.data.get("role", project_member.role)
|
||||
) in [15, 20]:
|
||||
return Response(
|
||||
@@ -273,7 +261,7 @@ class ProjectMemberViewSet(BaseViewSet):
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@allow_permission([ROLE.ADMIN])
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def destroy(self, request, slug, project_id, pk):
|
||||
project_member = ProjectMember.objects.get(
|
||||
workspace__slug=slug,
|
||||
@@ -310,7 +298,7 @@ class ProjectMemberViewSet(BaseViewSet):
|
||||
project_member.save()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST])
|
||||
def leave(self, request, slug, project_id):
|
||||
project_member = ProjectMember.objects.get(
|
||||
workspace__slug=slug,
|
||||
@@ -414,7 +402,6 @@ class UserProjectRolesEndpoint(BaseAPIView):
|
||||
project_members = ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
member_id=request.user.id,
|
||||
is_active=True,
|
||||
).values("project_id", "role")
|
||||
|
||||
project_members = {
|
||||
|
||||
@@ -90,7 +90,7 @@ class GlobalSearchEndpoint(BaseAPIView):
|
||||
"project__identifier",
|
||||
"project_id",
|
||||
"workspace__slug",
|
||||
)[:100]
|
||||
)
|
||||
|
||||
def filter_cycles(self, query, slug, project_id, workspace_search):
|
||||
fields = ["name"]
|
||||
|
||||
@@ -42,56 +42,47 @@ class IssueSearchEndpoint(BaseAPIView):
|
||||
issues = search_issues(query, issues)
|
||||
|
||||
if parent == "true" and issue_id:
|
||||
issue = Issue.issue_objects.filter(pk=issue_id).first()
|
||||
if issue:
|
||||
issues = issues.filter(
|
||||
~Q(pk=issue_id),
|
||||
~Q(pk=issue.parent_id),
|
||||
~Q(parent_id=issue_id),
|
||||
)
|
||||
issue = Issue.issue_objects.get(pk=issue_id)
|
||||
issues = issues.filter(
|
||||
~Q(pk=issue_id), ~Q(pk=issue.parent_id), ~Q(parent_id=issue_id)
|
||||
)
|
||||
if issue_relation == "true" and issue_id:
|
||||
issue = Issue.issue_objects.filter(pk=issue_id).first()
|
||||
if issue:
|
||||
issues = issues.filter(
|
||||
~Q(pk=issue_id),
|
||||
~(
|
||||
Q(issue_related__issue=issue)
|
||||
& Q(issue_related__deleted_at__isnull=True)
|
||||
),
|
||||
~(
|
||||
Q(issue_relation__related_issue=issue)
|
||||
& Q(issue_relation__deleted_at__isnull=True)
|
||||
),
|
||||
)
|
||||
issue = Issue.issue_objects.get(pk=issue_id)
|
||||
issues = issues.filter(
|
||||
~Q(pk=issue_id),
|
||||
~Q(
|
||||
issue_related__issue=issue,
|
||||
issue_related__deleted_at__isnull=True,
|
||||
),
|
||||
~Q(
|
||||
issue_relation__related_issue=issue,
|
||||
issue_related__deleted_at__isnull=True,
|
||||
),
|
||||
)
|
||||
if sub_issue == "true" and issue_id:
|
||||
issue = Issue.issue_objects.filter(pk=issue_id).first()
|
||||
if issue:
|
||||
issues = issues.filter(~Q(pk=issue_id), parent__isnull=True)
|
||||
issue = Issue.issue_objects.get(pk=issue_id)
|
||||
issues = issues.filter(~Q(pk=issue_id), parent__isnull=True)
|
||||
if issue.parent:
|
||||
issues = issues.filter(~Q(pk=issue.parent_id))
|
||||
|
||||
if cycle == "true":
|
||||
issues = issues.exclude(
|
||||
Q(issue_cycle__isnull=False)
|
||||
& Q(issue_cycle__deleted_at__isnull=True)
|
||||
)
|
||||
issues = issues.exclude(issue_cycle__isnull=False)
|
||||
|
||||
if module:
|
||||
issues = issues.exclude(
|
||||
Q(issue_module__module=module)
|
||||
& Q(issue_module__deleted_at__isnull=True)
|
||||
)
|
||||
issues = issues.exclude(issue_module__module=module)
|
||||
|
||||
if target_date == "none":
|
||||
issues = issues.filter(target_date__isnull=True)
|
||||
|
||||
|
||||
if ProjectMember.objects.filter(
|
||||
project_id=project_id,
|
||||
member=self.request.user,
|
||||
is_active=True,
|
||||
role=5,
|
||||
role=5
|
||||
).exists():
|
||||
issues = issues.filter(created_by=self.request.user)
|
||||
issues = issues.filter(
|
||||
created_by=self.request.user
|
||||
)
|
||||
|
||||
return Response(
|
||||
issues.values(
|
||||
@@ -106,6 +97,6 @@ class IssueSearchEndpoint(BaseAPIView):
|
||||
"state__name",
|
||||
"state__group",
|
||||
"state__color",
|
||||
)[:100],
|
||||
),
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
@@ -9,8 +9,7 @@ from rest_framework import status
|
||||
from .. import BaseViewSet
|
||||
from plane.app.serializers import StateSerializer
|
||||
from plane.app.permissions import (
|
||||
ROLE,
|
||||
allow_permission
|
||||
ProjectEntityPermission,
|
||||
)
|
||||
from plane.db.models import State, Issue
|
||||
from plane.utils.cache import invalidate_cache
|
||||
@@ -19,6 +18,9 @@ from plane.utils.cache import invalidate_cache
|
||||
class StateViewSet(BaseViewSet):
|
||||
serializer_class = StateSerializer
|
||||
model = State
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
|
||||
def get_queryset(self):
|
||||
return self.filter_queryset(
|
||||
@@ -40,7 +42,6 @@ class StateViewSet(BaseViewSet):
|
||||
@invalidate_cache(
|
||||
path="workspaces/:slug/states/", url_params=True, user=False
|
||||
)
|
||||
@allow_permission([ROLE.ADMIN])
|
||||
def create(self, request, slug, project_id):
|
||||
serializer = StateSerializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
@@ -48,7 +49,6 @@ class StateViewSet(BaseViewSet):
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
def list(self, request, slug, project_id):
|
||||
states = StateSerializer(self.get_queryset(), many=True).data
|
||||
grouped = request.GET.get("grouped", False)
|
||||
@@ -65,7 +65,6 @@ class StateViewSet(BaseViewSet):
|
||||
@invalidate_cache(
|
||||
path="workspaces/:slug/states/", url_params=True, user=False
|
||||
)
|
||||
@allow_permission([ROLE.ADMIN])
|
||||
def mark_as_default(self, request, slug, project_id, pk):
|
||||
# Select all the states which are marked as default
|
||||
_ = State.objects.filter(
|
||||
@@ -79,7 +78,6 @@ class StateViewSet(BaseViewSet):
|
||||
@invalidate_cache(
|
||||
path="workspaces/:slug/states/", url_params=True, user=False
|
||||
)
|
||||
@allow_permission([ROLE.ADMIN])
|
||||
def destroy(self, request, slug, project_id, pk):
|
||||
state = State.objects.get(
|
||||
is_triage=False,
|
||||
|
||||
@@ -32,6 +32,7 @@ from plane.db.models import (
|
||||
Session,
|
||||
)
|
||||
from plane.license.models import Instance, InstanceAdmin
|
||||
from plane.utils.cache import cache_response, invalidate_cache
|
||||
from plane.utils.paginator import BasePaginator
|
||||
from plane.authentication.utils.host import user_ip
|
||||
from plane.bgtasks.user_deactivation_email_task import user_deactivation_email
|
||||
@@ -48,6 +49,7 @@ class UserEndpoint(BaseViewSet):
|
||||
def get_object(self):
|
||||
return self.request.user
|
||||
|
||||
@cache_response(60 * 60)
|
||||
@method_decorator(cache_control(private=True, max_age=12))
|
||||
@method_decorator(vary_on_cookie)
|
||||
def retrieve(self, request):
|
||||
@@ -57,12 +59,14 @@ class UserEndpoint(BaseViewSet):
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
@cache_response(60 * 60)
|
||||
@method_decorator(cache_control(private=True, max_age=12))
|
||||
@method_decorator(vary_on_cookie)
|
||||
def retrieve_user_settings(self, request):
|
||||
serialized_data = UserMeSettingsSerializer(request.user).data
|
||||
return Response(serialized_data, status=status.HTTP_200_OK)
|
||||
|
||||
@cache_response(60 * 60)
|
||||
def retrieve_instance_admin(self, request):
|
||||
instance = Instance.objects.first()
|
||||
is_admin = InstanceAdmin.objects.filter(
|
||||
@@ -72,9 +76,19 @@ class UserEndpoint(BaseViewSet):
|
||||
{"is_instance_admin": is_admin}, status=status.HTTP_200_OK
|
||||
)
|
||||
|
||||
@invalidate_cache(
|
||||
path="/api/users/me/",
|
||||
)
|
||||
@invalidate_cache(
|
||||
path="/api/users/me/settings/",
|
||||
)
|
||||
def partial_update(self, request, *args, **kwargs):
|
||||
return super().partial_update(request, *args, **kwargs)
|
||||
|
||||
@invalidate_cache(path="/api/users/me/")
|
||||
@invalidate_cache(
|
||||
path="/api/users/me/workspaces/", multiple=True, user=False
|
||||
)
|
||||
def deactivate(self, request):
|
||||
# Check all workspace user is active
|
||||
user = self.get_object()
|
||||
@@ -221,6 +235,7 @@ class UserSessionEndpoint(BaseAPIView):
|
||||
|
||||
class UpdateUserOnBoardedEndpoint(BaseAPIView):
|
||||
|
||||
@invalidate_cache(path="/api/users/me/")
|
||||
def patch(self, request):
|
||||
profile = Profile.objects.get(user_id=request.user.id)
|
||||
profile.is_onboarded = request.data.get("is_onboarded", False)
|
||||
@@ -232,6 +247,7 @@ class UpdateUserOnBoardedEndpoint(BaseAPIView):
|
||||
|
||||
class UpdateUserTourCompletedEndpoint(BaseAPIView):
|
||||
|
||||
@invalidate_cache(path="/api/users/me/")
|
||||
def patch(self, request):
|
||||
profile = Profile.objects.get(user_id=request.user.id)
|
||||
profile.is_tour_completed = request.data.get(
|
||||
@@ -289,6 +305,7 @@ class ProfileEndpoint(BaseAPIView):
|
||||
serializer = ProfileSerializer(profile)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@invalidate_cache("/api/users/me/settings/")
|
||||
def patch(self, request):
|
||||
profile = Profile.objects.get(user=request.user)
|
||||
serializer = ProfileSerializer(
|
||||
|
||||
@@ -9,7 +9,6 @@ from django.db.models import (
|
||||
Q,
|
||||
UUIDField,
|
||||
Value,
|
||||
Subquery,
|
||||
)
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.utils.decorators import method_decorator
|
||||
@@ -30,14 +29,12 @@ from plane.app.serializers import (
|
||||
)
|
||||
from plane.db.models import (
|
||||
Issue,
|
||||
FileAsset,
|
||||
IssueAttachment,
|
||||
IssueLink,
|
||||
IssueView,
|
||||
Workspace,
|
||||
WorkspaceMember,
|
||||
ProjectMember,
|
||||
Project,
|
||||
CycleIssue,
|
||||
)
|
||||
from plane.utils.grouper import (
|
||||
issue_group_values,
|
||||
@@ -78,7 +75,7 @@ class WorkspaceViewViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST],
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST],
|
||||
level="WORKSPACE",
|
||||
)
|
||||
def list(self, request, slug):
|
||||
@@ -207,13 +204,7 @@ class WorkspaceViewIssuesViewSet(BaseViewSet):
|
||||
)
|
||||
.select_related("workspace", "project", "state", "parent")
|
||||
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||
.annotate(
|
||||
cycle_id=Subquery(
|
||||
CycleIssue.objects.filter(
|
||||
issue=OuterRef("id"), deleted_at__isnull=True
|
||||
).values("cycle_id")[:1]
|
||||
)
|
||||
)
|
||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||
.annotate(
|
||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||
.order_by()
|
||||
@@ -221,9 +212,8 @@ class WorkspaceViewIssuesViewSet(BaseViewSet):
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
attachment_count=FileAsset.objects.filter(
|
||||
issue_id=OuterRef("id"),
|
||||
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
|
||||
attachment_count=IssueAttachment.objects.filter(
|
||||
issue=OuterRef("id")
|
||||
)
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
@@ -242,10 +232,7 @@ class WorkspaceViewIssuesViewSet(BaseViewSet):
|
||||
ArrayAgg(
|
||||
"labels__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(labels__id__isnull=True)
|
||||
& Q(label_issue__deleted_at__isnull=True),
|
||||
),
|
||||
filter=~Q(labels__id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
@@ -253,11 +240,8 @@ class WorkspaceViewIssuesViewSet(BaseViewSet):
|
||||
ArrayAgg(
|
||||
"assignees__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True)
|
||||
& Q(issue_assignee__deleted_at__isnull=True)
|
||||
),
|
||||
filter=~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
@@ -265,11 +249,8 @@ class WorkspaceViewIssuesViewSet(BaseViewSet):
|
||||
ArrayAgg(
|
||||
"issue_module__module_id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(issue_module__module_id__isnull=True)
|
||||
& Q(issue_module__module__archived_at__isnull=True)
|
||||
& Q(issue_module__deleted_at__isnull=True)
|
||||
),
|
||||
filter=~Q(issue_module__module_id__isnull=True)
|
||||
& Q(issue_module__module__archived_at__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
@@ -278,7 +259,7 @@ class WorkspaceViewIssuesViewSet(BaseViewSet):
|
||||
|
||||
@method_decorator(gzip_page)
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST],
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST],
|
||||
level="WORKSPACE",
|
||||
)
|
||||
def list(self, request, slug):
|
||||
@@ -288,33 +269,18 @@ class WorkspaceViewIssuesViewSet(BaseViewSet):
|
||||
issue_queryset = (
|
||||
self.get_queryset()
|
||||
.filter(**filters)
|
||||
.annotate(
|
||||
cycle_id=Subquery(
|
||||
CycleIssue.objects.filter(
|
||||
issue=OuterRef("id"), deleted_at__isnull=True
|
||||
).values("cycle_id")[:1]
|
||||
)
|
||||
)
|
||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||
)
|
||||
|
||||
# check for the project member role, if the role is 5 then check for the guest_view_all_features if it is true then show all the issues else show only the issues created by the user
|
||||
|
||||
issue_queryset = issue_queryset.filter(
|
||||
Q(
|
||||
project__project_projectmember__role=5,
|
||||
project__guest_view_all_features=True,
|
||||
if WorkspaceMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
member=request.user,
|
||||
role=5,
|
||||
is_active=True,
|
||||
).exists():
|
||||
issue_queryset = issue_queryset.filter(
|
||||
created_by=request.user,
|
||||
)
|
||||
| Q(
|
||||
project__project_projectmember__role=5,
|
||||
project__guest_view_all_features=False,
|
||||
created_by=self.request.user,
|
||||
)
|
||||
|
|
||||
# For other roles (role < 5), show all issues
|
||||
Q(project__project_projectmember__role__gt=5),
|
||||
project__project_projectmember__member=self.request.user,
|
||||
project__project_projectmember__is_active=True,
|
||||
)
|
||||
|
||||
# Issue queryset
|
||||
issue_queryset, order_by_param = order_issue_queryset(
|
||||
@@ -370,10 +336,10 @@ class WorkspaceViewIssuesViewSet(BaseViewSet):
|
||||
group_by_field_name=group_by,
|
||||
sub_group_by_field_name=sub_group_by,
|
||||
count_filter=Q(
|
||||
Q(issue_intake__status=1)
|
||||
| Q(issue_intake__status=-1)
|
||||
| Q(issue_intake__status=2)
|
||||
| Q(issue_intake__isnull=True),
|
||||
Q(issue_inbox__status=1)
|
||||
| Q(issue_inbox__status=-1)
|
||||
| Q(issue_inbox__status=2)
|
||||
| Q(issue_inbox__isnull=True),
|
||||
archived_at__isnull=True,
|
||||
is_draft=False,
|
||||
),
|
||||
@@ -399,10 +365,10 @@ class WorkspaceViewIssuesViewSet(BaseViewSet):
|
||||
),
|
||||
group_by_field_name=group_by,
|
||||
count_filter=Q(
|
||||
Q(issue_intake__status=1)
|
||||
| Q(issue_intake__status=-1)
|
||||
| Q(issue_intake__status=2)
|
||||
| Q(issue_intake__isnull=True),
|
||||
Q(issue_inbox__status=1)
|
||||
| Q(issue_inbox__status=-1)
|
||||
| Q(issue_inbox__status=2)
|
||||
| Q(issue_inbox__isnull=True),
|
||||
archived_at__isnull=True,
|
||||
is_draft=False,
|
||||
),
|
||||
@@ -455,20 +421,19 @@ class IssueViewViewSet(BaseViewSet):
|
||||
.distinct()
|
||||
)
|
||||
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST]
|
||||
)
|
||||
|
||||
def list(self, request, slug, project_id):
|
||||
queryset = self.get_queryset()
|
||||
project = Project.objects.get(id=project_id)
|
||||
if (
|
||||
ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
member=request.user,
|
||||
role=5,
|
||||
is_active=True,
|
||||
).exists()
|
||||
and not project.guest_view_all_features
|
||||
):
|
||||
if ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
member=request.user,
|
||||
role=5,
|
||||
is_active=True,
|
||||
).exists():
|
||||
queryset = queryset.filter(owned_by=request.user)
|
||||
fields = [
|
||||
field
|
||||
@@ -480,33 +445,14 @@ class IssueViewViewSet(BaseViewSet):
|
||||
).data
|
||||
return Response(views, status=status.HTTP_200_OK)
|
||||
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST]
|
||||
)
|
||||
|
||||
def retrieve(self, request, slug, project_id, pk):
|
||||
issue_view = (
|
||||
self.get_queryset().filter(pk=pk, project_id=project_id).first()
|
||||
)
|
||||
project = Project.objects.get(id=project_id)
|
||||
"""
|
||||
if the role is guest and guest_view_all_features is false and owned by is not
|
||||
the requesting user then dont show the view
|
||||
"""
|
||||
|
||||
if (
|
||||
ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
member=request.user,
|
||||
role=5,
|
||||
is_active=True,
|
||||
).exists()
|
||||
and not project.guest_view_all_features
|
||||
and not issue_view.owned_by == request.user
|
||||
):
|
||||
return Response(
|
||||
{"error": "You are not allowed to view this issue"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
serializer = IssueViewSerializer(issue_view)
|
||||
recent_visited_task.delay(
|
||||
slug=slug,
|
||||
@@ -520,7 +466,8 @@ class IssueViewViewSet(BaseViewSet):
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
@allow_permission(allowed_roles=[], creator=True, model=IssueView)
|
||||
allow_permission(allowed_roles=[], creator=True, model=IssueView)
|
||||
|
||||
def partial_update(self, request, slug, project_id, pk):
|
||||
with transaction.atomic():
|
||||
issue_view = IssueView.objects.select_for_update().get(
|
||||
@@ -553,9 +500,8 @@ class IssueViewViewSet(BaseViewSet):
|
||||
serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN], creator=True, model=IssueView
|
||||
)
|
||||
allow_permission(allowed_roles=[ROLE.ADMIN], creator=True, model=IssueView)
|
||||
|
||||
def destroy(self, request, slug, project_id, pk):
|
||||
project_view = IssueView.objects.get(
|
||||
pk=pk,
|
||||
@@ -600,7 +546,8 @@ class IssueViewFavoriteViewSet(BaseViewSet):
|
||||
.select_related("view")
|
||||
)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
|
||||
def create(self, request, slug, project_id):
|
||||
_ = UserFavorite.objects.create(
|
||||
user=request.user,
|
||||
@@ -610,7 +557,8 @@ class IssueViewFavoriteViewSet(BaseViewSet):
|
||||
)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
|
||||
def destroy(self, request, slug, project_id, view_id):
|
||||
view_favorite = UserFavorite.objects.get(
|
||||
project=project_id,
|
||||
|
||||
@@ -43,7 +43,7 @@ from plane.db.models import (
|
||||
WorkspaceMember,
|
||||
WorkspaceTheme,
|
||||
)
|
||||
from plane.app.permissions import ROLE, allow_permission
|
||||
from plane.utils.cache import cache_response, invalidate_cache
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.cache import cache_control
|
||||
from django.views.decorators.vary import vary_on_cookie
|
||||
@@ -98,6 +98,9 @@ class WorkSpaceViewSet(BaseViewSet):
|
||||
.select_related("owner")
|
||||
)
|
||||
|
||||
@invalidate_cache(path="/api/workspaces/", user=False)
|
||||
@invalidate_cache(path="/api/users/me/workspaces/")
|
||||
@invalidate_cache(path="/api/instances/", user=False)
|
||||
def create(self, request):
|
||||
try:
|
||||
serializer = WorkSpaceSerializer(data=request.data)
|
||||
@@ -143,27 +146,22 @@ class WorkSpaceViewSet(BaseViewSet):
|
||||
status=status.HTTP_410_GONE,
|
||||
)
|
||||
|
||||
@allow_permission(
|
||||
[
|
||||
ROLE.ADMIN,
|
||||
ROLE.MEMBER,
|
||||
ROLE.GUEST,
|
||||
],
|
||||
level="WORKSPACE",
|
||||
)
|
||||
@cache_response(60 * 60 * 2)
|
||||
def list(self, request, *args, **kwargs):
|
||||
return super().list(request, *args, **kwargs)
|
||||
|
||||
@allow_permission(
|
||||
[
|
||||
ROLE.ADMIN,
|
||||
],
|
||||
level="WORKSPACE",
|
||||
)
|
||||
@invalidate_cache(path="/api/workspaces/", user=False)
|
||||
@invalidate_cache(path="/api/users/me/workspaces/")
|
||||
def partial_update(self, request, *args, **kwargs):
|
||||
return super().partial_update(request, *args, **kwargs)
|
||||
|
||||
@allow_permission([ROLE.ADMIN], level="WORKSPACE")
|
||||
@invalidate_cache(path="/api/workspaces/", user=False)
|
||||
@invalidate_cache(
|
||||
path="/api/users/me/workspaces/", multiple=True, user=False
|
||||
)
|
||||
@invalidate_cache(
|
||||
path="/api/users/me/settings/", multiple=True, user=False
|
||||
)
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
return super().destroy(request, *args, **kwargs)
|
||||
|
||||
@@ -176,6 +174,7 @@ class UserWorkSpacesEndpoint(BaseAPIView):
|
||||
"owner",
|
||||
]
|
||||
|
||||
@cache_response(60 * 60 * 2)
|
||||
@method_decorator(cache_control(private=True, max_age=12))
|
||||
@method_decorator(vary_on_cookie)
|
||||
def get(self, request):
|
||||
|
||||
@@ -33,7 +33,6 @@ class WorkspaceCyclesEndpoint(BaseAPIView):
|
||||
filter=Q(
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -44,8 +43,6 @@ class WorkspaceCyclesEndpoint(BaseAPIView):
|
||||
issue_cycle__issue__state__group="completed",
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
issue_cycle__issue__deleted_at__isnull=True,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -56,8 +53,6 @@ class WorkspaceCyclesEndpoint(BaseAPIView):
|
||||
issue_cycle__issue__state__group="cancelled",
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
issue_cycle__issue__deleted_at__isnull=True,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -68,8 +63,6 @@ class WorkspaceCyclesEndpoint(BaseAPIView):
|
||||
issue_cycle__issue__state__group="started",
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
issue_cycle__issue__deleted_at__isnull=True,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -80,8 +73,6 @@ class WorkspaceCyclesEndpoint(BaseAPIView):
|
||||
issue_cycle__issue__state__group="unstarted",
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
issue_cycle__issue__deleted_at__isnull=True,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -92,8 +83,6 @@ class WorkspaceCyclesEndpoint(BaseAPIView):
|
||||
issue_cycle__issue__state__group="backlog",
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
issue_cycle__issue__deleted_at__isnull=True,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,352 +0,0 @@
|
||||
# Python imports
|
||||
import json
|
||||
|
||||
# Django imports
|
||||
from django.utils import timezone
|
||||
from django.core import serializers
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.contrib.postgres.aggregates import ArrayAgg
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.db.models import (
|
||||
Q,
|
||||
UUIDField,
|
||||
Value,
|
||||
Subquery,
|
||||
OuterRef,
|
||||
)
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.gzip import gzip_page
|
||||
|
||||
# Third Party imports
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
|
||||
# Module imports
|
||||
from plane.app.permissions import allow_permission, ROLE
|
||||
from plane.app.serializers import (
|
||||
IssueCreateSerializer,
|
||||
DraftIssueCreateSerializer,
|
||||
DraftIssueSerializer,
|
||||
DraftIssueDetailSerializer,
|
||||
)
|
||||
from plane.db.models import (
|
||||
Issue,
|
||||
DraftIssue,
|
||||
CycleIssue,
|
||||
ModuleIssue,
|
||||
DraftIssueCycle,
|
||||
Workspace,
|
||||
FileAsset,
|
||||
)
|
||||
from .. import BaseViewSet
|
||||
from plane.bgtasks.issue_activities_task import issue_activity
|
||||
from plane.utils.issue_filters import issue_filters
|
||||
|
||||
|
||||
class WorkspaceDraftIssueViewSet(BaseViewSet):
|
||||
model = DraftIssue
|
||||
|
||||
def get_queryset(self):
|
||||
return (
|
||||
DraftIssue.objects.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.select_related("workspace", "project", "state", "parent")
|
||||
.prefetch_related(
|
||||
"assignees", "labels", "draft_issue_module__module"
|
||||
)
|
||||
.annotate(
|
||||
cycle_id=Subquery(
|
||||
DraftIssueCycle.objects.filter(
|
||||
draft_issue=OuterRef("id"), deleted_at__isnull=True
|
||||
).values("cycle_id")[:1]
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
label_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"labels__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(labels__id__isnull=True)
|
||||
& (Q(draft_label_issue__deleted_at__isnull=True))
|
||||
),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
assignee_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"assignees__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True)
|
||||
& Q(draft_issue_assignee__deleted_at__isnull=True)
|
||||
),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
module_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"draft_issue_module__module_id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(draft_issue_module__module_id__isnull=True)
|
||||
& Q(
|
||||
draft_issue_module__module__archived_at__isnull=True
|
||||
)
|
||||
& Q(draft_issue_module__deleted_at__isnull=True)
|
||||
),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
)
|
||||
).distinct()
|
||||
|
||||
@method_decorator(gzip_page)
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
|
||||
)
|
||||
def list(self, request, slug):
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
issues = (
|
||||
self.get_queryset()
|
||||
.filter(created_by=request.user)
|
||||
.order_by("-created_at")
|
||||
)
|
||||
|
||||
issues = issues.filter(**filters)
|
||||
# List Paginate
|
||||
return self.paginate(
|
||||
request=request,
|
||||
queryset=(issues),
|
||||
on_results=lambda issues: DraftIssueSerializer(
|
||||
issues,
|
||||
many=True,
|
||||
).data,
|
||||
)
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
|
||||
)
|
||||
def create(self, request, slug):
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
|
||||
serializer = DraftIssueCreateSerializer(
|
||||
data=request.data,
|
||||
context={
|
||||
"workspace_id": workspace.id,
|
||||
"project_id": request.data.get("project_id", None),
|
||||
},
|
||||
)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
issue = (
|
||||
self.get_queryset()
|
||||
.filter(pk=serializer.data.get("id"))
|
||||
.values(
|
||||
"id",
|
||||
"name",
|
||||
"state_id",
|
||||
"sort_order",
|
||||
"completed_at",
|
||||
"estimate_point",
|
||||
"priority",
|
||||
"start_date",
|
||||
"target_date",
|
||||
"project_id",
|
||||
"parent_id",
|
||||
"cycle_id",
|
||||
"module_ids",
|
||||
"label_ids",
|
||||
"assignee_ids",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"type_id",
|
||||
"description_html",
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
return Response(issue, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER],
|
||||
creator=True,
|
||||
model=Issue,
|
||||
level="WORKSPACE",
|
||||
)
|
||||
def partial_update(self, request, slug, pk):
|
||||
issue = (
|
||||
self.get_queryset().filter(pk=pk, created_by=request.user).first()
|
||||
)
|
||||
|
||||
if not issue:
|
||||
return Response(
|
||||
{"error": "Issue not found"},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
|
||||
serializer = DraftIssueCreateSerializer(
|
||||
issue,
|
||||
data=request.data,
|
||||
partial=True,
|
||||
context={
|
||||
"project_id": request.data.get("project_id", None),
|
||||
"cycle_id": request.data.get("cycle_id", "not_provided"),
|
||||
},
|
||||
)
|
||||
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN],
|
||||
creator=True,
|
||||
model=Issue,
|
||||
level="WORKSPACE",
|
||||
)
|
||||
def retrieve(self, request, slug, pk=None):
|
||||
issue = (
|
||||
self.get_queryset().filter(pk=pk, created_by=request.user).first()
|
||||
)
|
||||
|
||||
if not issue:
|
||||
return Response(
|
||||
{"error": "The required object does not exist."},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
|
||||
serializer = DraftIssueDetailSerializer(issue)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN],
|
||||
creator=True,
|
||||
model=DraftIssue,
|
||||
level="WORKSPACE",
|
||||
)
|
||||
def destroy(self, request, slug, pk=None):
|
||||
draft_issue = DraftIssue.objects.get(workspace__slug=slug, pk=pk)
|
||||
draft_issue.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER],
|
||||
level="WORKSPACE",
|
||||
)
|
||||
def create_draft_to_issue(self, request, slug, draft_id):
|
||||
draft_issue = self.get_queryset().filter(pk=draft_id).first()
|
||||
|
||||
if not draft_issue.project_id:
|
||||
return Response(
|
||||
{"error": "Project is required to create an issue."},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
serializer = IssueCreateSerializer(
|
||||
data=request.data,
|
||||
context={
|
||||
"project_id": draft_issue.project_id,
|
||||
"workspace_id": draft_issue.project.workspace_id,
|
||||
"default_assignee_id": draft_issue.project.default_assignee_id,
|
||||
},
|
||||
)
|
||||
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
|
||||
issue_activity.delay(
|
||||
type="issue.activity.created",
|
||||
requested_data=json.dumps(
|
||||
self.request.data, cls=DjangoJSONEncoder
|
||||
),
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=str(serializer.data.get("id", None)),
|
||||
project_id=str(draft_issue.project_id),
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
|
||||
if request.data.get("cycle_id", None):
|
||||
created_records = CycleIssue.objects.create(
|
||||
cycle_id=request.data.get("cycle_id", None),
|
||||
issue_id=serializer.data.get("id", None),
|
||||
project_id=draft_issue.project_id,
|
||||
workspace_id=draft_issue.workspace_id,
|
||||
created_by_id=draft_issue.created_by_id,
|
||||
updated_by_id=draft_issue.updated_by_id,
|
||||
)
|
||||
# Capture Issue Activity
|
||||
issue_activity.delay(
|
||||
type="cycle.activity.created",
|
||||
requested_data=None,
|
||||
actor_id=str(self.request.user.id),
|
||||
issue_id=None,
|
||||
project_id=str(self.kwargs.get("project_id", None)),
|
||||
current_instance=json.dumps(
|
||||
{
|
||||
"updated_cycle_issues": None,
|
||||
"created_cycle_issues": serializers.serialize(
|
||||
"json", [created_records]
|
||||
),
|
||||
}
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
|
||||
if request.data.get("module_ids", []):
|
||||
# bulk create the module
|
||||
ModuleIssue.objects.bulk_create(
|
||||
[
|
||||
ModuleIssue(
|
||||
module_id=module,
|
||||
issue_id=serializer.data.get("id", None),
|
||||
workspace_id=draft_issue.workspace_id,
|
||||
project_id=draft_issue.project_id,
|
||||
created_by_id=draft_issue.created_by_id,
|
||||
updated_by_id=draft_issue.updated_by_id,
|
||||
)
|
||||
for module in request.data.get("module_ids", [])
|
||||
],
|
||||
batch_size=10,
|
||||
)
|
||||
# Update the activity
|
||||
_ = [
|
||||
issue_activity.delay(
|
||||
type="module.activity.created",
|
||||
requested_data=json.dumps({"module_id": str(module)}),
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=serializer.data.get("id", None),
|
||||
project_id=draft_issue.project_id,
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
for module in request.data.get("module_ids", [])
|
||||
]
|
||||
|
||||
# Update file assets
|
||||
file_assets = FileAsset.objects.filter(draft_issue_id=draft_id)
|
||||
file_assets.update(
|
||||
issue_id=serializer.data.get("id", None),
|
||||
entity_type=FileAsset.EntityTypeContext.ISSUE_DESCRIPTION,
|
||||
draft_issue_id=None,
|
||||
)
|
||||
|
||||
# delete the draft issue
|
||||
draft_issue.delete()
|
||||
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user