Compare commits

..

1 Commits

Author SHA1 Message Date
NarayanBavisetti 28612a975b chore: corrected the issue activity for drafts 2024-09-06 10:45:32 +05:30
763 changed files with 11043 additions and 19301 deletions
+59
View File
@@ -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,
},
},
],
},
};
+10
View File
@@ -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
View File
@@ -1 +0,0 @@
*.sh text eol=lf
+13 -48
View File
@@ -2,12 +2,6 @@ name: Branch Build
on:
workflow_dispatch:
inputs:
arm64:
description: "Build for ARM64 architecture"
required: false
default: false
type: boolean
push:
branches:
- master
@@ -17,8 +11,6 @@ on:
env:
TARGET_BRANCH: ${{ github.ref_name || github.event.release.target_commitish }}
ARM64_BUILD: ${{ github.event.inputs.arm64 }}
IS_PRERELEASE: ${{ github.event.release.prerelease }}
jobs:
branch_build_setup:
@@ -36,13 +28,12 @@ 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 }}
flat_branch_name: ${{ steps.set_env_variables.outputs.FLAT_BRANCH_NAME }}
steps:
- id: set_env_variables
name: Set Environment Variables
run: |
if [ "${{ env.TARGET_BRANCH }}" == "master" ] || [ "${{ env.ARM64_BUILD }}" == "true" ] || ([ "${{ github.event_name }}" == "release" ] && [ "${{ env.IS_PRERELEASE }}" != "true" ]); then
if [ "${{ env.TARGET_BRANCH }}" == "master" ] || [ "${{ github.event_name }}" == "release" ]; then
echo "BUILDX_DRIVER=cloud" >> $GITHUB_OUTPUT
echo "BUILDX_VERSION=lab:latest" >> $GITHUB_OUTPUT
echo "BUILDX_PLATFORMS=linux/amd64,linux/arm64" >> $GITHUB_OUTPUT
@@ -54,8 +45,6 @@ jobs:
echo "BUILDX_ENDPOINT=" >> $GITHUB_OUTPUT
fi
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
@@ -101,11 +90,10 @@ jobs:
branch_build_push_web:
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 }}
FRONTEND_TAG: makeplane/plane-frontend:${{ needs.branch_build_setup.outputs.gh_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 }}
@@ -115,10 +103,7 @@ jobs:
- 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
TAG=makeplane/plane-frontend:stable,makeplane/plane-frontend:${{ github.event.release.tag_name }}
elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
TAG=makeplane/plane-frontend:latest
else
@@ -157,11 +142,10 @@ jobs:
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 }}
ADMIN_TAG: makeplane/plane-admin:${{ needs.branch_build_setup.outputs.gh_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 }}
@@ -171,10 +155,7 @@ jobs:
- 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
TAG=makeplane/plane-admin:stable,makeplane/plane-admin:${{ github.event.release.tag_name }}
elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
TAG=makeplane/plane-admin:latest
else
@@ -213,11 +194,10 @@ jobs:
branch_build_push_space:
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 }}
SPACE_TAG: makeplane/plane-space:${{ needs.branch_build_setup.outputs.gh_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 }}
@@ -227,10 +207,7 @@ jobs:
- 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
TAG=makeplane/plane-space:stable,makeplane/plane-space:${{ github.event.release.tag_name }}
elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
TAG=makeplane/plane-space:latest
else
@@ -269,11 +246,10 @@ jobs:
branch_build_push_apiserver:
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 }}
BACKEND_TAG: makeplane/plane-backend:${{ needs.branch_build_setup.outputs.gh_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 }}
@@ -283,10 +259,7 @@ jobs:
- 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
TAG=makeplane/plane-backend:stable,makeplane/plane-backend:${{ github.event.release.tag_name }}
elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
TAG=makeplane/plane-backend:latest
else
@@ -325,11 +298,10 @@ jobs:
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 }}
LIVE_TAG: makeplane/plane-live:${{ needs.branch_build_setup.outputs.gh_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 }}
@@ -339,10 +311,7 @@ jobs:
- 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
TAG=makeplane/plane-live:stable,makeplane/plane-live:${{ github.event.release.tag_name }}
elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
TAG=makeplane/plane-live:latest
else
@@ -381,11 +350,10 @@ jobs:
branch_build_push_proxy:
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]
env:
PROXY_TAG: makeplane/plane-proxy:${{ needs.branch_build_setup.outputs.flat_branch_name }}
PROXY_TAG: makeplane/plane-proxy:${{ needs.branch_build_setup.outputs.gh_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 }}
@@ -395,10 +363,7 @@ jobs:
- 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
TAG=makeplane/plane-proxy:stable,makeplane/plane-proxy:${{ github.event.release.tag_name }}
elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
TAG=makeplane/plane-proxy:latest
else
+18 -2
View File
@@ -8,6 +8,7 @@ on:
env:
CURRENT_BRANCH: ${{ github.ref_name }}
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 }}
@@ -15,7 +16,22 @@ env:
ACCOUNT_USER_EMAIL: ${{ vars.ACCOUNT_USER_EMAIL }}
jobs:
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
@@ -43,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 "sync: community changes" --body "")
PR_URL=$(gh pr create --base $TARGET_BRANCH --head $SOURCE_BRANCH --title "sync: community changes" --body "")
echo "Pull Request created: $PR_URL"
fi
+1 -2
View File
@@ -35,9 +35,8 @@ jobs:
env:
GH_TOKEN: ${{ secrets.ACCESS_TOKEN }}
run: |
RUN_ID="${{ github.run_id }}"
TARGET_REPO="${{ vars.SYNC_TARGET_REPO }}"
TARGET_BRANCH="sync/${RUN_ID}"
TARGET_BRANCH="${{ vars.SYNC_TARGET_BRANCH_NAME }}"
SOURCE_BRANCH="${{ env.SOURCE_BRANCH_NAME }}"
git checkout $SOURCE_BRANCH
View File
+3
View File
@@ -0,0 +1,3 @@
{
"*.{ts,tsx,js,jsx}": ["eslint -c ./.eslintrc-staged.js", "prettier --check"]
}
+1 -1
View File
@@ -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
View File
@@ -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 communitys 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 users 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
View File
@@ -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,
},
},
],
},
}
@@ -10,9 +10,8 @@ import {
// components
import { AuthenticationMethodCard } from "@/components/authentication";
// helpers
import { UpgradeButton } from "@/components/common/upgrade-button";
import { getBaseAuthenticationModes } from "@/helpers/authentication.helper";
// plane admin components
import { UpgradeButton } from "@/plane-admin/components/common";
// images
import OIDCLogo from "@/public/logos/oidc-logo.svg";
import SAMLLogo from "@/public/logos/saml-logo.svg";
@@ -28,24 +27,24 @@ export const getAuthenticationModes: (props: TGetBaseAuthenticationModeProps) =>
updateConfig,
resolvedTheme,
}) => [
...getBaseAuthenticationModes({ disabled, updateConfig, resolvedTheme }),
{
key: "oidc",
name: "OIDC",
description: "Authenticate your users via the OpenID Connect protocol.",
icon: <Image src={OIDCLogo} height={22} width={22} alt="OIDC Logo" />,
config: <UpgradeButton />,
unavailable: true,
},
{
key: "saml",
name: "SAML",
description: "Authenticate your users via the Security Assertion Markup Language protocol.",
icon: <Image src={SAMLLogo} height={22} width={22} alt="SAML Logo" className="pl-0.5" />,
config: <UpgradeButton />,
unavailable: true,
},
];
...getBaseAuthenticationModes({ disabled, updateConfig, resolvedTheme }),
{
key: "oidc",
name: "OIDC",
description: "Authenticate your users via the OpenID Connect protocol.",
icon: <Image src={OIDCLogo} height={22} width={22} alt="OIDC Logo" />,
config: <UpgradeButton />,
unavailable: true,
},
{
key: "saml",
name: "SAML",
description: "Authenticate your users via the Security Assertion Markup Language protocol.",
icon: <Image src={SAMLLogo} height={22} width={22} alt="SAML Logo" className="pl-0.5" />,
config: <UpgradeButton />,
unavailable: true,
},
];
export const AuthenticationModes: React.FC<TAuthenticationModeProps> = observer((props) => {
const { disabled, updateConfig } = props;
-1
View File
@@ -1 +0,0 @@
export * from "./upgrade-button";
-19
View File
@@ -1,19 +0,0 @@
import { enableStaticRendering } from "mobx-react";
// stores
import { CoreRootStore } from "@/store/root.store";
enableStaticRendering(typeof window === "undefined");
export class RootStore extends CoreRootStore {
constructor() {
super();
}
hydrate(initialData: any) {
super.hydrate(initialData);
}
resetOnSignOut() {
super.resetOnSignOut();
}
}
+6 -5
View File
@@ -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();
+1
View File
@@ -8,3 +8,4 @@ export * from "./empty-state";
export * from "./logo-spinner";
export * from "./page-header";
export * from "./code-block";
export * from "./upgrade-button";
@@ -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
View File
@@ -18,7 +18,6 @@ export const AdminLayout: FC<TAdminLayout> = observer((props) => {
const { children } = props;
// router
const router = useRouter();
// store hooks
const { isUserLoggedIn } = useUser();
useEffect(() => {
+2 -2
View File
@@ -1,8 +1,8 @@
"use client";
import { ReactNode, createContext } from "react";
// plane admin store
import { RootStore } from "@/plane-admin/store/root.store";
// store
import { RootStore } from "@/store/root.store";
let rootStore = new RootStore();
+1 -1
View File
@@ -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";
+2 -2
View File
@@ -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";
+2 -2
View File
@@ -13,7 +13,7 @@ import { EInstanceStatus, TInstanceStatus } from "@/helpers/instance.helper";
// services
import { InstanceService } from "@/services/instance.service";
// root store
import { CoreRootStore } from "@/store/root.store";
import { RootStore } from "@/store/root.store";
export interface IInstanceStore {
// issues
@@ -46,7 +46,7 @@ export class InstanceStore implements IInstanceStore {
// service
instanceService;
constructor(private store: CoreRootStore) {
constructor(private store: RootStore) {
makeObservable(this, {
// observable
isLoading: observable.ref,
+1 -1
View File
@@ -6,7 +6,7 @@ import { IUserStore, UserStore } from "./user.store";
enableStaticRendering(typeof window === "undefined");
export abstract class CoreRootStore {
export class RootStore {
theme: IThemeStore;
instance: IInstanceStore;
user: IUserStore;
+2 -2
View File
@@ -1,6 +1,6 @@
import { action, observable, makeObservable } from "mobx";
// root store
import { CoreRootStore } from "@/store/root.store";
import { RootStore } from "@/store/root.store";
type TTheme = "dark" | "light";
export interface IThemeStore {
@@ -21,7 +21,7 @@ export class ThemeStore implements IThemeStore {
isSidebarCollapsed: boolean | undefined = undefined;
theme: string | undefined = undefined;
constructor(private store: CoreRootStore) {
constructor(private store: RootStore) {
makeObservable(this, {
// observables
isNewUserPopup: observable.ref,
+2 -2
View File
@@ -6,7 +6,7 @@ import { EUserStatus, TUserStatus } from "@/helpers/user.helper";
import { AuthService } from "@/services/auth.service";
import { UserService } from "@/services/user.service";
// root store
import { CoreRootStore } from "@/store/root.store";
import { RootStore } from "@/store/root.store";
export interface IUserStore {
// observables
@@ -31,7 +31,7 @@ export class UserStore implements IUserStore {
userService;
authService;
constructor(private store: CoreRootStore) {
constructor(private store: RootStore) {
makeObservable(this, {
// observables
isLoading: observable.ref,
-1
View File
@@ -1 +0,0 @@
export * from "ce/components/common";
-1
View File
@@ -1 +0,0 @@
export * from "ce/store/root.store";
+1 -1
View File
@@ -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.
+7 -10
View File
@@ -1,6 +1,6 @@
{
"name": "admin",
"version": "0.23.0",
"version": "0.22.0",
"private": true,
"scripts": {
"dev": "turbo run develop",
@@ -8,16 +8,13 @@
"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",
@@ -27,7 +24,7 @@
"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",
@@ -39,15 +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.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
View File
@@ -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"
}
]
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
{
"name": "plane-api",
"version": "0.23.0"
"version": "0.22.0"
}
+4 -4
View File
@@ -1,3 +1,6 @@
from django.core.exceptions import ValidationError
from django.core.validators import URLValidator
# Django imports
from django.utils import timezone
from lxml import html
@@ -27,9 +30,6 @@ from .module import ModuleLiteSerializer, ModuleSerializer
from .state import StateLiteSerializer
from .user import UserLiteSerializer
# Django imports
from django.core.exceptions import ValidationError
from django.core.validators import URLValidator
class IssueSerializer(BaseSerializer):
assignees = serializers.ListField(
@@ -315,7 +315,7 @@ class IssueLinkSerializer(BaseSerializer):
"created_at",
"updated_at",
]
def validate_url(self, value):
# Check URL format
validate_url = URLValidator()
-23
View File
@@ -71,16 +71,6 @@ class ModuleSerializer(BaseSerializer):
project_id = self.context["project_id"]
workspace_id = self.context["workspace_id"]
module_name = validated_data.get("name")
if module_name:
# Lookup for the module name in the module table for that project
if Module.objects.filter(
name=module_name, project_id=project_id
).exists():
raise serializers.ValidationError(
{"error": "Module with this name already exists"}
)
module = Module.objects.create(**validated_data, project_id=project_id)
if members is not None:
ModuleMember.objects.bulk_create(
@@ -103,19 +93,6 @@ class ModuleSerializer(BaseSerializer):
def update(self, instance, validated_data):
members = validated_data.pop("members", None)
module_name = validated_data.get("name")
if module_name:
# Lookup for the module name in the module table for that project
if (
Module.objects.filter(
name=module_name, project=instance.project
)
.exclude(id=instance.id)
.exists()
):
raise serializers.ValidationError(
{"error": "Module with this name already exists"}
)
if members is not None:
ModuleMember.objects.filter(module=instance).delete()
+4 -4
View File
@@ -207,7 +207,7 @@ class CycleAPIEndpoint(BaseAPIView):
# Incomplete Cycles
if cycle_view == "incomplete":
queryset = queryset.filter(
Q(end_date__gte=timezone.now())
Q(end_date__gte=timezone.now().date())
| Q(end_date__isnull=True),
)
return self.paginate(
@@ -311,7 +311,7 @@ class CycleAPIEndpoint(BaseAPIView):
if (
cycle.end_date is not None
and cycle.end_date < timezone.now()
and cycle.end_date < timezone.now().date()
):
if "sort_order" in request_data:
# Can only change sort order
@@ -537,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,
@@ -1146,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(
{
+5 -4
View File
@@ -210,7 +210,7 @@ class InboxIssueAPIEndpoint(BaseAPIView):
)
# Only project members admins and created_by users can access this endpoint
if project_member.role <= 5 and str(inbox_issue.created_by_id) != str(
if project_member.role <= 10 and str(inbox_issue.created_by_id) != str(
request.user.id
):
return Response(
@@ -244,8 +244,9 @@ class InboxIssueAPIEndpoint(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(
@@ -285,7 +286,7 @@ class InboxIssueAPIEndpoint(BaseAPIView):
)
# Only project admins and members can edit inbox issue attributes
if project_member.role > 5:
if project_member.role > 10:
serializer = InboxIssueSerializer(
inbox_issue, data=request.data, partial=True
)
+2 -2
View File
@@ -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()
+3 -8
View File
@@ -301,16 +301,11 @@ class ProjectAPIEndpoint(BaseAPIView):
if serializer.is_valid():
serializer.save()
if serializer.data["inbox_view"]:
inbox = Inbox.objects.filter(
Inbox.objects.get_or_create(
name=f"{project.name} Inbox",
project=project,
is_default=True,
).first()
if not inbox:
Inbox.objects.create(
name=f"{project.name} Inbox",
project=project,
is_default=True,
)
)
# Create the triage state in Backlog group
State.objects.get_or_create(
+1
View File
@@ -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
+8 -7
View File
@@ -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()
@@ -124,9 +124,3 @@ from .webhook import WebhookSerializer, WebhookLogSerializer
from .dashboard import DashboardSerializer, WidgetSerializer
from .favorite import UserFavoriteSerializer
from .draft import (
DraftIssueCreateSerializer,
DraftIssueSerializer,
DraftIssueDetailSerializer,
)
-278
View File
@@ -1,278 +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",
"project",
"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"]
# Create Issue
issue = DraftIssue.objects.create(
**validated_data,
workspace_id=workspace_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,
issue=issue,
workspace_id=workspace_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,
issue=issue,
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,
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,
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.initial_data.get("cycle_id", None)
modules = self.initial_data.get("module_ids", None)
# Related models
workspace_id = instance.workspace_id
created_by_id = instance.created_by_id
updated_by_id = instance.updated_by_id
if assignees is not None:
DraftIssueAssignee.objects.filter(issue=instance).delete()
DraftIssueAssignee.objects.bulk_create(
[
DraftIssueAssignee(
assignee=user,
issue=instance,
workspace_id=workspace_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(issue=instance).delete()
DraftIssueLabel.objects.bulk_create(
[
DraftIssueLabel(
label=label,
issue=instance,
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.filter(draft_issue=instance).delete()
DraftIssueCycle.objects.create(
cycle_id=cycle_id,
draft_issue=instance,
workspace_id=workspace_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=module,
draft_issue=instance,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
for module 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",
]
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
+10 -20
View File
@@ -437,21 +437,17 @@ class IssueLinkSerializer(BaseSerializer):
"issue",
]
def to_internal_value(self, data):
# Modify the URL before validation by appending http:// if missing
url = data.get("url", "")
if url and not url.startswith(("http://", "https://")):
data["url"] = "http://" + url
return super().to_internal_value(data)
def validate_url(self, value):
# Use Django's built-in URLValidator for validation
url_validator = URLValidator()
# Check URL format
validate_url = URLValidator()
try:
url_validator(value)
validate_url(value)
except ValidationError:
raise serializers.ValidationError({"error": "Invalid URL format."})
raise serializers.ValidationError("Invalid URL format.")
# Check URL scheme
if not value.startswith(("http://", "https://")):
raise serializers.ValidationError("Invalid URL scheme.")
return value
@@ -537,7 +533,7 @@ class IssueReactionSerializer(BaseSerializer):
"project",
"issue",
"actor",
"deleted_at",
"deleted_at"
]
@@ -556,13 +552,7 @@ class CommentReactionSerializer(BaseSerializer):
class Meta:
model = CommentReaction
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"comment",
"actor",
"deleted_at",
]
read_only_fields = ["workspace", "project", "comment", "actor", "deleted_at"]
class IssueVoteSerializer(BaseSerializer):
+3 -69
View File
@@ -5,10 +5,6 @@ from rest_framework import serializers
from .base import BaseSerializer, DynamicBaseSerializer
from .project import ProjectLiteSerializer
# Django imports
from django.core.validators import URLValidator
from django.core.exceptions import ValidationError
from plane.db.models import (
User,
Module,
@@ -68,16 +64,6 @@ class ModuleWriteSerializer(BaseSerializer):
members = validated_data.pop("member_ids", None)
project = self.context["project"]
module_name = validated_data.get("name")
if module_name:
# Lookup for the module name in the module table for that project
if Module.objects.filter(
name=module_name, project=project
).exists():
raise serializers.ValidationError(
{"error": "Module with this name already exists"}
)
module = Module.objects.create(**validated_data, project=project)
if members is not None:
ModuleMember.objects.bulk_create(
@@ -100,19 +86,6 @@ class ModuleWriteSerializer(BaseSerializer):
def update(self, instance, validated_data):
members = validated_data.pop("member_ids", None)
module_name = validated_data.get("name")
if module_name:
# Lookup for the module name in the module table for that project
if (
Module.objects.filter(
name=module_name, project=instance.project
)
.exclude(id=instance.id)
.exists()
):
raise serializers.ValidationError(
{"error": "Module with this name already exists"}
)
if members is not None:
ModuleMember.objects.filter(module=instance).delete()
@@ -182,48 +155,16 @@ class ModuleLinkSerializer(BaseSerializer):
"module",
]
def to_internal_value(self, data):
# Modify the URL before validation by appending http:// if missing
url = data.get("url", "")
if url and not url.startswith(("http://", "https://")):
data["url"] = "http://" + url
return super().to_internal_value(data)
def validate_url(self, value):
# Use Django's built-in URLValidator for validation
url_validator = URLValidator()
try:
url_validator(value)
except ValidationError:
raise serializers.ValidationError({"error": "Invalid URL format."})
return value
# Validation if url already exists
def create(self, validated_data):
validated_data["url"] = self.validate_url(validated_data.get("url"))
if ModuleLink.objects.filter(
url=validated_data.get("url"),
module_id=validated_data.get("module_id"),
).exists():
raise serializers.ValidationError({"error": "URL already exists."})
return super().create(validated_data)
def update(self, instance, validated_data):
validated_data["url"] = self.validate_url(validated_data.get("url"))
if (
ModuleLink.objects.filter(
url=validated_data.get("url"),
module_id=instance.module_id,
)
.exclude(pk=instance.id)
.exists()
):
raise serializers.ValidationError(
{"error": "URL already exists for this Issue"}
)
return super().update(instance, validated_data)
return ModuleLink.objects.create(**validated_data)
class ModuleSerializer(DynamicBaseSerializer):
@@ -288,14 +229,7 @@ class ModuleDetailSerializer(ModuleSerializer):
cancelled_estimate_points = serializers.FloatField(read_only=True)
class Meta(ModuleSerializer.Meta):
fields = ModuleSerializer.Meta.fields + [
"link_module",
"sub_issues",
"backlog_estimate_points",
"unstarted_estimate_points",
"started_estimate_points",
"cancelled_estimate_points",
]
fields = ModuleSerializer.Meta.fields + ["link_module", "sub_issues", "backlog_estimate_points", "unstarted_estimate_points", "started_estimate_points", "cancelled_estimate_points"]
class ModuleUserPropertiesSerializer(BaseSerializer):
-1
View File
@@ -176,7 +176,6 @@ class UserAdminLiteSerializer(BaseSerializer):
"is_bot",
"display_name",
"email",
"last_login_medium",
]
read_only_fields = [
"id",
+2 -2
View File
@@ -40,7 +40,7 @@ class WebhookSerializer(DynamicBaseSerializer):
for addr in ip_addresses:
ip = ipaddress.ip_address(addr[4][0])
if ip.is_loopback:
if ip.is_private or ip.is_loopback:
raise serializers.ValidationError(
{"url": "URL resolves to a blocked IP address."}
)
@@ -92,7 +92,7 @@ class WebhookSerializer(DynamicBaseSerializer):
for addr in ip_addresses:
ip = ipaddress.ip_address(addr[4][0])
if ip.is_loopback:
if ip.is_private or ip.is_loopback:
raise serializers.ValidationError(
{"url": "URL resolves to a blocked IP address."}
)
+22 -5
View File
@@ -11,6 +11,7 @@ from plane.app.views import (
IssueActivityEndpoint,
IssueArchiveViewSet,
IssueCommentViewSet,
IssueDraftViewSet,
IssueListEndpoint,
IssueReactionViewSet,
IssueRelationViewSet,
@@ -19,7 +20,6 @@ from plane.app.views import (
IssueViewSet,
LabelViewSet,
BulkArchiveIssuesEndpoint,
DeletedIssuesListViewSet,
IssuePaginatedViewSet,
)
@@ -39,7 +39,7 @@ urlpatterns = [
),
name="project-issue",
),
# updated v2 paginated issues
# updated v1 paginated issues
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/v2/issues/",
IssuePaginatedViewSet.as_view({"get": "list"}),
@@ -289,9 +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-drafts/<uuid:pk>/",
IssueDraftViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="project-issue-draft",
),
]
-22
View File
@@ -27,7 +27,6 @@ from plane.app.views import (
WorkspaceCyclesEndpoint,
WorkspaceFavoriteEndpoint,
WorkspaceFavoriteGroupEndpoint,
WorkspaceDraftIssueViewSet,
)
@@ -255,25 +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",
),
]
+2 -3
View File
@@ -40,8 +40,6 @@ from .workspace.base import (
ExportWorkspaceUserActivityEndpoint,
)
from .workspace.draft import WorkspaceDraftIssueViewSet
from .workspace.favorite import (
WorkspaceFavoriteEndpoint,
WorkspaceFavoriteGroupEndpoint,
@@ -116,7 +114,6 @@ from .issue.base import (
IssueViewSet,
IssueUserDisplayPropertyEndpoint,
BulkDeleteIssuesEndpoint,
DeletedIssuesListViewSet,
IssuePaginatedViewSet,
)
@@ -135,6 +132,8 @@ from .issue.comment import (
CommentReactionViewSet,
)
from .issue.draft import IssueDraftViewSet
from .issue.label import (
LabelViewSet,
BulkCreateIssueLabelsEndpoint,
+6 -16
View File
@@ -21,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)
@@ -207,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(
@@ -244,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)
@@ -314,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(
+2 -7
View File
@@ -288,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 = (
@@ -604,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,
+14 -13
View File
@@ -150,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")
@@ -308,7 +308,7 @@ class CycleViewSet(BaseViewSet):
if (
cycle.end_date is not None
and cycle.end_date < timezone.now()
and cycle.end_date < timezone.now().date()
):
if "sort_order" in request_data:
# Can only change sort order for a completed cycle``
@@ -370,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)
@@ -502,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)
@@ -570,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)
@@ -925,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(
{
@@ -980,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,
@@ -1003,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,
@@ -1016,8 +1014,10 @@ 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",
@@ -1148,9 +1148,10 @@ class CycleProgressEndpoint(BaseAPIView):
status=status.HTTP_200_OK,
)
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 = (
+2 -7
View File
@@ -80,12 +80,7 @@ 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")
@@ -248,7 +243,7 @@ class CycleIssueViewSet(BaseViewSet):
if (
cycle.end_date is not None
and cycle.end_date < timezone.now()
and cycle.end_date < timezone.now().date()
):
return Response(
{
+15 -60
View File
@@ -40,6 +40,8 @@ from plane.db.models import (
IssueLink,
IssueRelation,
Project,
ProjectMember,
User,
Widget,
WorkspaceMember,
)
@@ -50,28 +52,23 @@ 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,
project__project_projectmember__member=request.user,
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()
)
@@ -83,22 +80,8 @@ def dashboard_overview_stats(self, request, slug):
project__project_projectmember__member=request.user,
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()
)
@@ -108,22 +91,8 @@ def dashboard_overview_stats(self, request, slug):
project__project_projectmember__is_active=True,
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()
)
@@ -134,22 +103,8 @@ def dashboard_overview_stats(self, request, slug):
project__project_projectmember__member=request.user,
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()
)
+3 -68
View File
@@ -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(
+18 -41
View File
@@ -165,12 +165,11 @@ class InboxIssueViewSet(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):
inbox_id = Inbox.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__")
inbox_issue = (
InboxIssue.objects.filter(
@@ -200,16 +199,13 @@ class InboxIssueViewSet(BaseViewSet):
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
):
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,
@@ -342,7 +338,7 @@ class InboxIssueViewSet(BaseViewSet):
is_active=True,
)
# Only project members admins and created_by users can access this endpoint
if project_member.role <= 5 and str(inbox_issue.created_by_id) != str(
if project_member.role <= 10 and str(inbox_issue.created_by_id) != str(
request.user.id
):
return Response(
@@ -375,8 +371,9 @@ class InboxIssueViewSet(BaseViewSet):
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(
@@ -418,7 +415,7 @@ class InboxIssueViewSet(BaseViewSet):
)
# Only project admins and members can edit inbox issue attributes
if project_member.role > 5:
if project_member.role > 10:
serializer = InboxIssueSerializer(
inbox_issue, data=request.data, partial=True
)
@@ -518,19 +515,14 @@ class InboxIssueViewSet(BaseViewSet):
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):
inbox_id = Inbox.objects.filter(
inbox_id = Inbox.objects.get(
workspace__slug=slug, project_id=project_id
).first()
project = Project.objects.get(pk=project_id)
)
inbox_issue = (
InboxIssue.objects.select_related("issue")
.prefetch_related(
@@ -557,21 +549,6 @@ class InboxIssueViewSet(BaseViewSet):
)
.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 inbox_issue.created_by == request.user
):
return Response(
{"error": "You are not allowed to view this issue"},
status=status.HTTP_400_BAD_REQUEST,
)
issue = InboxIssueDetailSerializer(inbox_issue).data
return Response(
issue,
+3 -13
View File
@@ -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:
@@ -52,7 +42,7 @@ class IssueActivityEndpoint(BaseAPIView):
issue_activities = (
IssueActivity.objects.filter(issue_id=issue_id)
.filter(
~Q(field__in=["comment", "vote", "reaction", "draft"]),
~Q(field__in=["comment", "vote", "reaction"]),
project__project_projectmember__member=self.request.user,
project__project_projectmember__is_active=True,
project__archived_at__isnull=True,
+2 -12
View File
@@ -97,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")
@@ -218,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()
@@ -64,13 +64,7 @@ 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 = IssueAttachment.objects.filter(
issue_id=issue_id, workspace__slug=slug, project_id=project_id
+23 -107
View File
@@ -62,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)
@@ -232,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
@@ -271,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:
@@ -450,17 +440,9 @@ 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 = (
self.get_queryset()
.filter(pk=pk)
@@ -529,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",
@@ -561,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()
@@ -659,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,
@@ -679,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
@@ -719,38 +672,16 @@ 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=F("issue_cycle__cycle_id"))
.annotate(
@@ -788,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)
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",
@@ -816,6 +746,7 @@ class IssuePaginatedViewSet(BaseViewSet):
"updated_by",
"is_draft",
"archived_at",
"deleted_at",
"module_ids",
"label_ids",
"assignee_ids",
@@ -830,28 +761,13 @@ class IssuePaginatedViewSet(BaseViewSet):
# 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(
+6 -42
View File
@@ -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,
+410
View File
@@ -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)
+3 -3
View File
@@ -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)
+4 -3
View File
@@ -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,
+1 -1
View File
@@ -267,7 +267,7 @@ class IssueRelationViewSet(BaseViewSet):
IssueRelationSerializer(issue_relation).data,
cls=DjangoJSONEncoder,
)
issue_relation.delete(soft=False)
issue_relation.delete()
issue_activity.delay(
type="issue_relation.activity.deleted",
requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
+5 -15
View File
@@ -317,12 +317,7 @@ 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)
@@ -386,7 +381,7 @@ 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)
@@ -435,12 +430,7 @@ 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 = (
@@ -871,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,
@@ -894,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 -6
View File
@@ -91,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)
@@ -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):
@@ -174,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):
@@ -195,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(
@@ -207,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(
@@ -219,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(
@@ -231,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(
@@ -245,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):
@@ -286,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)
+11 -66
View File
@@ -32,10 +32,8 @@ from plane.db.models import (
UserFavorite,
ProjectMember,
ProjectPage,
Project,
)
from plane.utils.error_codes import ERROR_CODES
# Module imports
from ..base import BaseAPIView, BaseViewSet
@@ -122,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,
@@ -144,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(
@@ -210,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"},
@@ -265,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
@@ -275,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
@@ -286,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(
@@ -309,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
@@ -368,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
@@ -526,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(
@@ -562,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(
+1 -1
View File
@@ -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
+24 -47
View File
@@ -71,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",
@@ -148,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):
@@ -158,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
):
@@ -195,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):
@@ -413,20 +406,9 @@ 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)
project = Project.objects.get(pk=pk)
@@ -449,16 +431,11 @@ class ProjectViewSet(BaseViewSet):
if serializer.is_valid():
serializer.save()
if serializer.data["inbox_view"]:
inbox = Inbox.objects.filter(
Inbox.objects.get_or_create(
name=f"{project.name} Inbox",
project=project,
is_default=True,
).first()
if not inbox:
Inbox.objects.create(
name=f"{project.name} Inbox",
project=project,
is_default=True,
)
)
# Create the triage state in Backlog group
State.objects.get_or_create(
+24 -18
View File
@@ -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,
)
+8 -21
View File
@@ -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 = {
+1 -1
View File
@@ -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"]
+1 -1
View File
@@ -97,6 +97,6 @@ class IssueSearchEndpoint(BaseAPIView):
"state__name",
"state__group",
"state__color",
)[:100],
),
status=status.HTTP_200_OK,
)
+4 -6
View File
@@ -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,
+23 -55
View File
@@ -35,7 +35,6 @@ from plane.db.models import (
Workspace,
WorkspaceMember,
ProjectMember,
Project,
)
from plane.utils.grouper import (
issue_group_values,
@@ -76,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):
@@ -260,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):
@@ -273,24 +272,15 @@ class WorkspaceViewIssuesViewSet(BaseViewSet):
.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(
@@ -431,21 +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
@@ -457,34 +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,
@@ -43,7 +43,6 @@ 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
@@ -148,25 +147,11 @@ class WorkSpaceViewSet(BaseViewSet):
)
@cache_response(60 * 60 * 2)
@allow_permission(
[
ROLE.ADMIN,
ROLE.MEMBER,
ROLE.GUEST,
],
level="WORKSPACE",
)
def list(self, request, *args, **kwargs):
return super().list(request, *args, **kwargs)
@invalidate_cache(path="/api/workspaces/", user=False)
@invalidate_cache(path="/api/users/me/workspaces/")
@allow_permission(
[
ROLE.ADMIN,
],
level="WORKSPACE",
)
def partial_update(self, request, *args, **kwargs):
return super().partial_update(request, *args, **kwargs)
@@ -177,7 +162,6 @@ class WorkSpaceViewSet(BaseViewSet):
@invalidate_cache(
path="/api/users/me/settings/", multiple=True, user=False
)
@allow_permission([ROLE.ADMIN], level="WORKSPACE")
def destroy(self, request, *args, **kwargs):
return super().destroy(request, *args, **kwargs)
@@ -1,236 +0,0 @@
# Django imports
from django.contrib.postgres.aggregates import ArrayAgg
from django.contrib.postgres.fields import ArrayField
from django.db.models import (
F,
Q,
UUIDField,
Value,
)
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,
Workspace,
)
from .. import BaseViewSet
class WorkspaceDraftIssueViewSet(BaseViewSet):
model = DraftIssue
@method_decorator(gzip_page)
@allow_permission(
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
)
def list(self, request, slug):
issues = (
DraftIssue.objects.filter(workspace__slug=slug)
.filter(created_by=request.user)
.select_related("workspace", "project", "state", "parent")
.prefetch_related(
"assignees", "labels", "draft_issue_module__module"
)
.annotate(cycle_id=F("draft_issue_cycle__cycle_id"))
.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(
"draft_issue_module__module_id",
distinct=True,
filter=~Q(draft_issue_module__module_id__isnull=True)
& Q(
draft_issue_module__module__archived_at__isnull=True
),
),
Value([], output_field=ArrayField(UUIDField())),
),
)
.order_by("-created_at")
)
serializer = DraftIssueSerializer(issues, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
@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,
},
)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@allow_permission(
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER],
creator=True,
model=Issue,
level="WORKSPACE",
)
def partial_update(self, request, slug, pk):
issue = (
DraftIssue.objects.filter(workspace__slug=slug)
.filter(pk=pk)
.filter(created_by=request.user)
.select_related("workspace", "project", "state", "parent")
.prefetch_related(
"assignees", "labels", "draft_issue_module__module"
)
.annotate(cycle_id=F("draft_issue_cycle__cycle_id"))
.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(
"draft_issue_module__module_id",
distinct=True,
filter=~Q(draft_issue_module__module_id__isnull=True)
& Q(
draft_issue_module__module__archived_at__isnull=True
),
),
Value([], output_field=ArrayField(UUIDField())),
),
)
.first()
)
if not issue:
return Response(
{"error": "Issue not found"},
status=status.HTTP_404_NOT_FOUND,
)
serializer = IssueCreateSerializer(
issue, data=request.data, partial=True
)
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 = (
DraftIssue.objects.filter(workspace__slug=slug)
.filter(pk=pk)
.filter(created_by=request.user)
.select_related("workspace", "project", "state", "parent")
.prefetch_related(
"assignees", "labels", "draft_issue_module__module"
)
.annotate(cycle_id=F("draft_issue_cycle__cycle_id"))
.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(
"draft_issue_module__module_id",
distinct=True,
filter=~Q(draft_issue_module__module_id__isnull=True)
& Q(
draft_issue_module__module__archived_at__isnull=True
),
),
Value([], output_field=ArrayField(UUIDField())),
),
)
).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=Issue,
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)
@@ -74,7 +74,7 @@ class WorkspaceInvitationsViewset(BaseViewSet):
[
email
for email in emails
if int(email.get("role", 5)) > requesting_user.role
if int(email.get("role", 10)) > requesting_user.role
]
):
return Response(
@@ -119,7 +119,7 @@ class WorkspaceInvitationsViewset(BaseViewSet):
settings.SECRET_KEY,
algorithm="HS256",
),
role=email.get("role", 5),
role=email.get("role", 10),
created_by=request.user,
)
)
+38 -15
View File
@@ -13,8 +13,7 @@ from rest_framework.response import Response
from plane.app.permissions import (
WorkSpaceAdminPermission,
WorkspaceEntityPermission,
allow_permission,
ROLE,
WorkspaceUserPermission,
)
# Module imports
@@ -44,6 +43,22 @@ class WorkSpaceMemberViewSet(BaseViewSet):
serializer_class = WorkspaceMemberAdminSerializer
model = WorkspaceMember
permission_classes = [
WorkspaceEntityPermission,
]
def get_permissions(self):
if self.action == "leave":
self.permission_classes = [
WorkspaceUserPermission,
]
else:
self.permission_classes = [
WorkspaceEntityPermission,
]
return super(WorkSpaceMemberViewSet, self).get_permissions()
search_fields = [
"member__display_name",
"member__first_name",
@@ -62,9 +77,6 @@ class WorkSpaceMemberViewSet(BaseViewSet):
)
@cache_response(60 * 60 * 2)
@allow_permission(
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
)
def list(self, request, slug):
workspace_member = WorkspaceMember.objects.get(
member=request.user,
@@ -74,7 +86,8 @@ class WorkSpaceMemberViewSet(BaseViewSet):
# Get all active workspace members
workspace_members = self.get_queryset()
if workspace_member.role > 5:
if workspace_member.role > 10:
serializer = WorkspaceMemberAdminSerializer(
workspace_members,
fields=("id", "member", "role"),
@@ -94,7 +107,6 @@ class WorkSpaceMemberViewSet(BaseViewSet):
user=False,
multiple=True,
)
@allow_permission(allowed_roles=[ROLE.ADMIN], level="WORKSPACE")
def partial_update(self, request, slug, pk):
workspace_member = WorkspaceMember.objects.get(
pk=pk,
@@ -108,10 +120,25 @@ class WorkSpaceMemberViewSet(BaseViewSet):
status=status.HTTP_400_BAD_REQUEST,
)
if workspace_member.role > int(request.data.get("role")):
_ = ProjectMember.objects.filter(
workspace__slug=slug, member_id=workspace_member.member_id
).update(role=int(request.data.get("role")))
# Get the requested user role
requested_workspace_member = WorkspaceMember.objects.get(
workspace__slug=slug,
member=request.user,
is_active=True,
)
# Check if role is being updated
# One cannot update role higher than his own role
if (
"role" in request.data
and int(request.data.get("role", workspace_member.role))
> requested_workspace_member.role
):
return Response(
{
"error": "You cannot update a role that is higher than your own role"
},
status=status.HTTP_400_BAD_REQUEST,
)
serializer = WorkSpaceMemberSerializer(
workspace_member, data=request.data, partial=True
@@ -132,7 +159,6 @@ class WorkSpaceMemberViewSet(BaseViewSet):
@invalidate_cache(
path="/api/users/me/workspaces/", user=False, multiple=True
)
@allow_permission(allowed_roles=[ROLE.ADMIN], level="WORKSPACE")
def destroy(self, request, slug, pk):
# Check the user role who is deleting the user
workspace_member = WorkspaceMember.objects.get(
@@ -207,9 +233,6 @@ class WorkSpaceMemberViewSet(BaseViewSet):
@invalidate_cache(
path="api/users/me/workspaces/", user=False, multiple=True
)
@allow_permission(
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
)
def leave(self, request, slug):
workspace_member = WorkspaceMember.objects.get(
workspace__slug=slug,
+4 -4
View File
@@ -288,7 +288,7 @@ class WorkspaceUserProfileEndpoint(BaseAPIView):
is_active=True,
)
projects = []
if requesting_workspace_member.role >= 15:
if requesting_workspace_member.role >= 10:
projects = (
Project.objects.filter(
workspace__slug=slug,
@@ -504,7 +504,7 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView):
upcoming_cycles = CycleIssue.objects.filter(
workspace__slug=slug,
cycle__start_date__gt=timezone.now(),
cycle__start_date__gt=timezone.now().date(),
issue__assignees__in=[
user_id,
],
@@ -512,8 +512,8 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView):
present_cycle = CycleIssue.objects.filter(
workspace__slug=slug,
cycle__start_date__lt=timezone.now(),
cycle__end_date__gt=timezone.now(),
cycle__start_date__lt=timezone.now().date(),
cycle__end_date__gt=timezone.now().date(),
issue__assignees__in=[
user_id,
],
@@ -49,7 +49,7 @@ def process_workspace_project_invitations(user):
workspace_id=project_member_invite.workspace_id,
role=(
project_member_invite.role
if project_member_invite.role in [5, 15]
if project_member_invite.role in [5, 10, 15]
else 15
),
member=user,
@@ -67,7 +67,7 @@ def process_workspace_project_invitations(user):
workspace_id=project_member_invite.workspace_id,
role=(
project_member_invite.role
if project_member_invite.role in [5, 15]
if project_member_invite.role in [5, 10, 15]
else 15
),
member=user,
+13 -21
View File
@@ -2,7 +2,6 @@
from django.utils import timezone
from django.apps import apps
from django.conf import settings
from django.db import models
from django.core.exceptions import ObjectDoesNotExist
# Third party imports
@@ -19,25 +18,17 @@ def soft_delete_related_objects(
for field in related_fields:
if field.one_to_many or field.one_to_one:
try:
# Check if the field has CASCADE on delete
if (
hasattr(field.remote_field, "on_delete")
and field.remote_field.on_delete == models.CASCADE
):
if field.one_to_many:
related_objects = getattr(instance, field.name).all()
elif field.one_to_one:
related_object = getattr(instance, field.name)
related_objects = (
[related_object]
if related_object is not None
else []
)
for obj in related_objects:
if obj:
obj.deleted_at = timezone.now()
obj.save(using=using)
if field.one_to_many:
related_objects = getattr(instance, field.name).all()
elif field.one_to_one:
related_object = getattr(instance, field.name)
related_objects = (
[related_object] if related_object is not None else []
)
for obj in related_objects:
if obj:
obj.deleted_at = timezone.now()
obj.save(using=using)
except ObjectDoesNotExist:
pass
@@ -163,7 +154,8 @@ def hard_delete():
if hasattr(model, "deleted_at"):
# Get all instances where 'deleted_at' is greater than 30 days ago
_ = model.all_objects.filter(
deleted_at__lt=timezone.now() - timezone.timedelta(days=days)
deleted_at__lt=timezone.now()
- timezone.timedelta(days=days)
).delete()
return
+20 -33
View File
@@ -347,7 +347,7 @@ def create_issues(workspace, project, user_id, issue_count):
)
)
text = fake.text(max_nb_chars=3000)
text = fake.text(max_nb_chars=60000)
issues.append(
Issue(
state_id=states[random.randint(0, len(states) - 1)],
@@ -490,23 +490,18 @@ def create_issue_assignees(workspace, project, user_id, issue_count):
def create_issue_labels(workspace, project, user_id, issue_count):
# labels
labels = Label.objects.filter(project=project).values_list("id", flat=True)
# issues = random.sample(
# list(
# Issue.objects.filter(project=project).values_list("id", flat=True)
# ),
# int(issue_count / 2),
# )
issues = list(
issues = random.sample(
list(
Issue.objects.filter(project=project).values_list("id", flat=True)
)
shuffled_labels = list(labels)
),
int(issue_count / 2),
)
# Bulk issue
bulk_issue_labels = []
for issue in issues:
random.shuffle(shuffled_labels)
for label in random.sample(
shuffled_labels, random.randint(0, 5)
list(labels), random.randint(0, len(labels) - 1)
):
bulk_issue_labels.append(
IssueLabel(
@@ -557,33 +552,25 @@ def create_module_issues(workspace, project, user_id, issue_count):
modules = Module.objects.filter(project=project).values_list(
"id", flat=True
)
# issues = random.sample(
# list(
# Issue.objects.filter(project=project).values_list("id", flat=True)
# ),
# int(issue_count / 2),
# )
issues = list(
issues = random.sample(
list(
Issue.objects.filter(project=project).values_list("id", flat=True)
)
shuffled_modules = list(modules)
),
int(issue_count / 2),
)
# Bulk issue
bulk_module_issues = []
for issue in issues:
random.shuffle(shuffled_modules)
for module in random.sample(
shuffled_modules, random.randint(0, 5)
):
bulk_module_issues.append(
ModuleIssue(
module_id=module,
issue_id=issue,
project=project,
workspace=workspace,
)
module = modules[random.randint(0, len(modules) - 1)]
bulk_module_issues.append(
ModuleIssue(
module_id=module,
issue_id=issue,
project=project,
workspace=workspace,
)
)
# Issue assignees
ModuleIssue.objects.bulk_create(
bulk_module_issues, batch_size=1000, ignore_conflicts=True
+5 -13
View File
@@ -244,13 +244,9 @@ def update_json_row(rows, row):
)
assignee, label = row["Assignee"], row["Labels"]
if assignee is not None and (
existing_assignees is None or label not in existing_assignees
):
if assignee is not None and assignee not in existing_assignees:
rows[matched_index]["Assignee"] += f", {assignee}"
if label is not None and (
existing_labels is None or label not in existing_labels
):
if label is not None and label not in existing_labels:
rows[matched_index]["Labels"] += f", {label}"
else:
rows.append(row)
@@ -270,13 +266,9 @@ def update_table_row(rows, row):
existing_assignees, existing_labels = rows[matched_index][7:9]
assignee, label = row[7:9]
if assignee is not None and (
existing_assignees is None or label not in existing_assignees
):
rows[matched_index][8] += f", {assignee}"
if label is not None and (
existing_labels is None or label not in existing_labels
):
if assignee is not None and assignee not in existing_assignees:
rows[matched_index][7] += f", {assignee}"
if label is not None and label not in existing_labels:
rows[matched_index][8] += f", {label}"
else:
rows.append(row)
@@ -465,7 +465,7 @@ def track_estimate_points(
IssueActivity(
issue_id=issue_id,
actor_id=actor_id,
verb="removed" if new_estimate is None else "updated",
verb="updated",
old_identifier=(
current_instance.get("estimate_point")
if current_instance.get("estimate_point") is not None
@@ -1530,19 +1530,7 @@ def update_draft_issue_activity(
workspace_id=workspace_id,
comment="created the issue",
verb="updated",
actor_id=actor_id,
epoch=epoch,
)
)
else:
issue_activities.append(
IssueActivity(
issue_id=issue_id,
project_id=project_id,
workspace_id=workspace_id,
comment="updated the draft issue",
field="draft",
verb="updated",
actor_id=actor_id,
epoch=epoch,
)
@@ -1700,12 +1688,16 @@ def issue_activity(
event=(
"issue_comment"
if activity.field == "comment"
else "inbox_issue" if inbox else "issue"
else "inbox_issue"
if inbox
else "issue"
),
event_id=(
activity.issue_comment_id
if activity.field == "comment"
else inbox if inbox else activity.issue_id
else inbox
if inbox
else activity.issue_id
),
verb=activity.verb,
field=(
@@ -42,12 +42,14 @@ def archive_old_issues():
),
Q(issue_cycle__isnull=True)
| (
Q(issue_cycle__cycle__end_date__lt=timezone.now())
Q(issue_cycle__cycle__end_date__lt=timezone.now().date())
& Q(issue_cycle__isnull=False)
),
Q(issue_module__isnull=True)
| (
Q(issue_module__module__target_date__lt=timezone.now())
Q(
issue_module__module__target_date__lt=timezone.now().date()
)
& Q(issue_module__isnull=False)
),
).filter(
@@ -120,12 +122,14 @@ def close_old_issues():
),
Q(issue_cycle__isnull=True)
| (
Q(issue_cycle__cycle__end_date__lt=timezone.now())
Q(issue_cycle__cycle__end_date__lt=timezone.now().date())
& Q(issue_cycle__isnull=False)
),
Q(issue_module__isnull=True)
| (
Q(issue_module__module__target_date__lt=timezone.now())
Q(
issue_module__module__target_date__lt=timezone.now().date()
)
& Q(issue_module__isnull=False)
),
).filter(
@@ -73,7 +73,7 @@ class Command(BaseCommand):
from plane.bgtasks.dummy_data_task import create_dummy_data
create_dummy_data(
create_dummy_data.delay(
slug=workspace_slug,
email=creator,
members=members,
@@ -1,60 +0,0 @@
# Generated by Django 4.2.15 on 2024-08-30 07:34
from django.db import migrations, models
def update_workspace_project_member_role(apps, schema_editor):
WorkspaceMember = apps.get_model("db", "WorkspaceMember")
ProjectMember = apps.get_model("db", "ProjectMember")
# update all existing members with role 10 to role 5
WorkspaceMember.objects.filter(role=10).update(role=5)
ProjectMember.objects.filter(role=10).update(role=5)
class Migration(migrations.Migration):
dependencies = [
("db", "0075_alter_fileasset_asset"),
]
operations = [
migrations.AlterField(
model_name="projectmember",
name="role",
field=models.PositiveSmallIntegerField(
choices=[(20, "Admin"), (15, "Member"), (5, "Guest")],
default=5,
),
),
migrations.AlterField(
model_name="projectmemberinvite",
name="role",
field=models.PositiveSmallIntegerField(
choices=[(20, "Admin"), (15, "Member"), (5, "Guest")],
default=5,
),
),
migrations.AlterField(
model_name="workspacemember",
name="role",
field=models.PositiveSmallIntegerField(
choices=[(20, "Admin"), (15, "Member"), (5, "Guest")],
default=5,
),
),
migrations.AlterField(
model_name="workspacememberinvite",
name="role",
field=models.PositiveSmallIntegerField(
choices=[(20, "Admin"), (15, "Member"), (5, "Guest")],
default=5,
),
),
migrations.AddField(
model_name="project",
name="guest_view_all_features",
field=models.BooleanField(default=False),
),
migrations.RunPython(update_workspace_project_member_role),
]
-1
View File
@@ -5,7 +5,6 @@ from .base import BaseModel
from .cycle import Cycle, CycleFavorite, CycleIssue, CycleUserProperties
from .dashboard import Dashboard, DashboardWidget, Widget
from .deploy_board import DeployBoard
from .draft import DraftIssue, DraftIssueAssignee, DraftIssueLabel, DraftIssueModule, DraftIssueCycle
from .estimate import Estimate, EstimatePoint
from .exporter import ExporterHistory
from .importer import Importer
+2 -13
View File
@@ -1,6 +1,3 @@
# Python imports
import pytz
# Django imports
from django.conf import settings
from django.db import models
@@ -58,12 +55,10 @@ class Cycle(ProjectBaseModel):
description = models.TextField(
verbose_name="Cycle Description", blank=True
)
start_date = models.DateTimeField(
start_date = models.DateField(
verbose_name="Start Date", blank=True, null=True
)
end_date = models.DateTimeField(
verbose_name="End Date", blank=True, null=True
)
end_date = models.DateField(verbose_name="End Date", blank=True, null=True)
owned_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
@@ -76,12 +71,6 @@ class Cycle(ProjectBaseModel):
progress_snapshot = models.JSONField(default=dict)
archived_at = models.DateTimeField(null=True)
logo_props = models.JSONField(default=dict)
# timezone
TIMEZONE_CHOICES = tuple(zip(pytz.all_timezones, pytz.all_timezones))
timezone = models.CharField(
max_length=255, default="UTC", choices=TIMEZONE_CHOICES
)
version = models.IntegerField(default=1)
class Meta:
verbose_name = "Cycle"
-253
View File
@@ -1,253 +0,0 @@
# Django imports
from django.conf import settings
from django.db import models
from django.utils import timezone
# Module imports
from plane.utils.html_processor import strip_tags
from .workspace import WorkspaceBaseModel
class DraftIssue(WorkspaceBaseModel):
PRIORITY_CHOICES = (
("urgent", "Urgent"),
("high", "High"),
("medium", "Medium"),
("low", "Low"),
("none", "None"),
)
parent = models.ForeignKey(
"db.Issue",
on_delete=models.CASCADE,
null=True,
blank=True,
related_name="draft_parent_issue",
)
state = models.ForeignKey(
"db.State",
on_delete=models.CASCADE,
null=True,
blank=True,
related_name="state_draft_issue",
)
estimate_point = models.ForeignKey(
"db.EstimatePoint",
on_delete=models.SET_NULL,
related_name="draft_issue_estimates",
null=True,
blank=True,
)
name = models.CharField(
max_length=255, verbose_name="Issue Name", blank=True, null=True
)
description = models.JSONField(blank=True, default=dict)
description_html = models.TextField(blank=True, default="<p></p>")
description_stripped = models.TextField(blank=True, null=True)
description_binary = models.BinaryField(null=True)
priority = models.CharField(
max_length=30,
choices=PRIORITY_CHOICES,
verbose_name="Issue Priority",
default="none",
)
start_date = models.DateField(null=True, blank=True)
target_date = models.DateField(null=True, blank=True)
assignees = models.ManyToManyField(
settings.AUTH_USER_MODEL,
blank=True,
related_name="draft_assignee",
through="DraftIssueAssignee",
through_fields=("draft_issue", "assignee"),
)
labels = models.ManyToManyField(
"db.Label",
blank=True,
related_name="draft_labels",
through="DraftIssueLabel",
)
sort_order = models.FloatField(default=65535)
completed_at = models.DateTimeField(null=True)
external_source = models.CharField(max_length=255, null=True, blank=True)
external_id = models.CharField(max_length=255, blank=True, null=True)
type = models.ForeignKey(
"db.IssueType",
on_delete=models.SET_NULL,
related_name="draft_issue_type",
null=True,
blank=True,
)
class Meta:
verbose_name = "DraftIssue"
verbose_name_plural = "DraftIssues"
db_table = "draft_issues"
ordering = ("-created_at",)
def save(self, *args, **kwargs):
if self.state is None:
try:
from plane.db.models import State
default_state = State.objects.filter(
~models.Q(is_triage=True),
project=self.project,
default=True,
).first()
if default_state is None:
random_state = State.objects.filter(
~models.Q(is_triage=True), project=self.project
).first()
self.state = random_state
else:
self.state = default_state
except ImportError:
pass
else:
try:
from plane.db.models import State
if self.state.group == "completed":
self.completed_at = timezone.now()
else:
self.completed_at = None
except ImportError:
pass
if self._state.adding:
# Strip the html tags using html parser
self.description_stripped = (
None
if (
self.description_html == ""
or self.description_html is None
)
else strip_tags(self.description_html)
)
largest_sort_order = DraftIssue.objects.filter(
project=self.project, state=self.state
).aggregate(largest=models.Max("sort_order"))["largest"]
if largest_sort_order is not None:
self.sort_order = largest_sort_order + 10000
super(DraftIssue, self).save(*args, **kwargs)
else:
# Strip the html tags using html parser
self.description_stripped = (
None
if (
self.description_html == ""
or self.description_html is None
)
else strip_tags(self.description_html)
)
super(DraftIssue, self).save(*args, **kwargs)
def __str__(self):
"""Return name of the draft issue"""
return f"{self.name} <{self.project.name}>"
class DraftIssueAssignee(WorkspaceBaseModel):
draft_issue = models.ForeignKey(
DraftIssue,
on_delete=models.CASCADE,
related_name="draft_issue_assignee",
)
assignee = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="draft_issue_assignee",
)
class Meta:
unique_together = ["draft_issue", "assignee", "deleted_at"]
constraints = [
models.UniqueConstraint(
fields=["draft_issue", "assignee"],
condition=models.Q(deleted_at__isnull=True),
name="draft_issue_assignee_unique_issue_assignee_when_deleted_at_null",
)
]
verbose_name = "Draft Issue Assignee"
verbose_name_plural = "Draft Issue Assignees"
db_table = "draft_issue_assignees"
ordering = ("-created_at",)
def __str__(self):
return f"{self.draft_issue.name} {self.assignee.email}"
class DraftIssueLabel(WorkspaceBaseModel):
draft_issue = models.ForeignKey(
"db.DraftIssue",
on_delete=models.CASCADE,
related_name="draft_label_issue",
)
label = models.ForeignKey(
"db.Label", on_delete=models.CASCADE, related_name="draft_label_issue"
)
class Meta:
verbose_name = "Draft Issue Label"
verbose_name_plural = "Draft Issue Labels"
db_table = "draft_issue_labels"
ordering = ("-created_at",)
def __str__(self):
return f"{self.draft_issue.name} {self.label.name}"
class DraftIssueModule(WorkspaceBaseModel):
module = models.ForeignKey(
"db.Module",
on_delete=models.CASCADE,
related_name="draft_issue_module",
)
draft_issue = models.ForeignKey(
"db.DraftIssue",
on_delete=models.CASCADE,
related_name="draft_issue_module",
)
class Meta:
unique_together = ["draft_issue", "module", "deleted_at"]
constraints = [
models.UniqueConstraint(
fields=["draft_issue", "module"],
condition=models.Q(deleted_at__isnull=True),
name="module_draft_issue_unique_issue_module_when_deleted_at_null",
)
]
verbose_name = "Draft Issue Module"
verbose_name_plural = "Draft Issue Modules"
db_table = "draft_issue_modules"
ordering = ("-created_at",)
def __str__(self):
return f"{self.module.name} {self.draft_issue.name}"
class DraftIssueCycle(WorkspaceBaseModel):
"""
Draft Issue Cycles
"""
draft_issue = models.OneToOneField(
"db.DraftIssue",
on_delete=models.CASCADE,
related_name="draft_issue_cycle",
)
cycle = models.ForeignKey(
"db.Cycle", on_delete=models.CASCADE, related_name="draft_issue_cycle"
)
class Meta:
verbose_name = "Draft Issue Cycle"
verbose_name_plural = "Draft Issue Cycles"
db_table = "draft_issue_cycles"
ordering = ("-created_at",)
def __str__(self):
return f"{self.cycle}"
+4 -10
View File
@@ -1,5 +1,4 @@
# Python imports
import pytz
from uuid import uuid4
# Django imports
@@ -8,7 +7,7 @@ from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.db.models import Q
# Module imports
# Modeule imports
from plane.db.mixins import AuditModel
# Module imports
@@ -17,6 +16,7 @@ from .base import BaseModel
ROLE_CHOICES = (
(20, "Admin"),
(15, "Member"),
(10, "Viewer"),
(5, "Guest"),
)
@@ -98,7 +98,6 @@ class Project(BaseModel):
inbox_view = models.BooleanField(default=False)
is_time_tracking_enabled = models.BooleanField(default=False)
is_issue_type_enabled = models.BooleanField(default=False)
guest_view_all_features = models.BooleanField(default=False)
cover_image = models.URLField(blank=True, null=True, max_length=800)
estimate = models.ForeignKey(
"db.Estimate",
@@ -120,11 +119,6 @@ class Project(BaseModel):
related_name="default_state",
)
archived_at = models.DateTimeField(null=True)
# timezone
TIMEZONE_CHOICES = tuple(zip(pytz.all_timezones, pytz.all_timezones))
timezone = models.CharField(
max_length=255, default="UTC", choices=TIMEZONE_CHOICES
)
def __str__(self):
"""Return name of the project"""
@@ -179,7 +173,7 @@ class ProjectMemberInvite(ProjectBaseModel):
token = models.CharField(max_length=255)
message = models.TextField(null=True)
responded_at = models.DateTimeField(null=True)
role = models.PositiveSmallIntegerField(choices=ROLE_CHOICES, default=5)
role = models.PositiveSmallIntegerField(choices=ROLE_CHOICES, default=10)
class Meta:
verbose_name = "Project Member Invite"
@@ -200,7 +194,7 @@ class ProjectMember(ProjectBaseModel):
related_name="member_project",
)
comment = models.TextField(blank=True, null=True)
role = models.PositiveSmallIntegerField(choices=ROLE_CHOICES, default=5)
role = models.PositiveSmallIntegerField(choices=ROLE_CHOICES, default=10)
view_props = models.JSONField(default=get_default_props)
default_props = models.JSONField(default=get_default_props)
preferences = models.JSONField(default=get_default_preferences)
+5 -4
View File
@@ -8,8 +8,9 @@ from .base import BaseModel
from plane.utils.constants import RESTRICTED_WORKSPACE_SLUGS
ROLE_CHOICES = (
(20, "Admin"),
(15, "Member"),
(20, "Owner"),
(15, "Admin"),
(10, "Member"),
(5, "Guest"),
)
@@ -176,7 +177,7 @@ class WorkspaceMember(BaseModel):
on_delete=models.CASCADE,
related_name="member_workspace",
)
role = models.PositiveSmallIntegerField(choices=ROLE_CHOICES, default=5)
role = models.PositiveSmallIntegerField(choices=ROLE_CHOICES, default=10)
company_role = models.TextField(null=True, blank=True)
view_props = models.JSONField(default=get_default_props)
default_props = models.JSONField(default=get_default_props)
@@ -213,7 +214,7 @@ class WorkspaceMemberInvite(BaseModel):
token = models.CharField(max_length=255)
message = models.TextField(null=True)
responded_at = models.DateTimeField(null=True)
role = models.PositiveSmallIntegerField(choices=ROLE_CHOICES, default=5)
role = models.PositiveSmallIntegerField(choices=ROLE_CHOICES, default=10)
class Meta:
unique_together = ["email", "workspace", "deleted_at"]
-97
View File
@@ -1,97 +0,0 @@
# Third party imports
from celery import shared_task
from opentelemetry import trace
# Module imports
from plane.license.models import Instance
from plane.db.models import (
User,
Workspace,
Project,
Issue,
Module,
Cycle,
CycleIssue,
ModuleIssue,
Page,
WorkspaceMember,
)
@shared_task
def instance_traces():
# Get the tracer
tracer = trace.get_tracer(__name__)
# Check if the instance is registered
instance = Instance.objects.first()
# If instance is None then return
if instance is None:
return
if instance.is_telemetry_enabled:
# Instance details
with tracer.start_as_current_span("instance_details") as span:
# Count of all models
workspace_count = Workspace.objects.count()
user_count = User.objects.count()
project_count = Project.objects.count()
issue_count = Issue.objects.count()
module_count = Module.objects.count()
cycle_count = Cycle.objects.count()
cycle_issue_count = CycleIssue.objects.count()
module_issue_count = ModuleIssue.objects.count()
page_count = Page.objects.count()
# Set span attributes
span.set_attribute("instance_id", instance.instance_id)
span.set_attribute("instance_name", instance.instance_name)
span.set_attribute("current_version", instance.current_version)
span.set_attribute("latest_version", instance.latest_version)
span.set_attribute(
"is_telemetry_enabled", instance.is_telemetry_enabled
)
span.set_attribute("user_count", user_count)
span.set_attribute("workspace_count", workspace_count)
span.set_attribute("project_count", project_count)
span.set_attribute("issue_count", issue_count)
span.set_attribute("module_count", module_count)
span.set_attribute("cycle_count", cycle_count)
span.set_attribute("cycle_issue_count", cycle_issue_count)
span.set_attribute("module_issue_count", module_issue_count)
span.set_attribute("page_count", page_count)
# Workspace details
for workspace in Workspace.objects.all():
# Count of all models
project_count = Project.objects.filter(workspace=workspace).count()
issue_count = Issue.objects.filter(workspace=workspace).count()
module_count = Module.objects.filter(workspace=workspace).count()
cycle_count = Cycle.objects.filter(workspace=workspace).count()
cycle_issue_count = CycleIssue.objects.filter(
workspace=workspace
).count()
module_issue_count = ModuleIssue.objects.filter(
workspace=workspace
).count()
page_count = Page.objects.filter(workspace=workspace).count()
member_count = WorkspaceMember.objects.filter(
workspace=workspace
).count()
# Set span attributes
with tracer.start_as_current_span("workspace_details") as span:
span.set_attribute("instance_id", instance.instance_id)
span.set_attribute("workspace_id", str(workspace.id))
span.set_attribute("workspace_slug", workspace.slug)
span.set_attribute("project_count", project_count)
span.set_attribute("issue_count", issue_count)
span.set_attribute("module_count", module_count)
span.set_attribute("cycle_count", cycle_count)
span.set_attribute("cycle_issue_count", cycle_issue_count)
span.set_attribute("module_issue_count", module_issue_count)
span.set_attribute("page_count", page_count)
span.set_attribute("member_count", member_count)
return
@@ -9,10 +9,7 @@ from django.conf import settings
# Module imports
from plane.license.models import Instance
from plane.db.models import (
User,
)
from plane.license.bgtasks.tracer import instance_traces
from plane.db.models import User
class Command(BaseCommand):
@@ -24,24 +21,16 @@ class Command(BaseCommand):
"machine_signature", type=str, help="Machine signature"
)
def read_package_json(self):
with open("package.json", "r") as file:
# Load JSON content from the file
data = json.load(file)
payload = {
"instance_key": settings.INSTANCE_KEY,
"version": data.get("version", 0.1),
"user_count": User.objects.filter(is_bot=False).count(),
}
return payload
def handle(self, *args, **options):
# Check if the instance is registered
instance = Instance.objects.first()
# If instance is None then register this instance
if instance is None:
with open("package.json", "r") as file:
# Load JSON content from the file
data = json.load(file)
machine_signature = options.get(
"machine_signature", "machine-signature"
)
@@ -49,7 +38,12 @@ class Command(BaseCommand):
if not machine_signature:
raise CommandError("Machine signature is required")
payload = self.read_package_json()
payload = {
"instance_key": settings.INSTANCE_KEY,
"version": data.get("version", 0.1),
"machine_signature": machine_signature,
"user_count": User.objects.filter(is_bot=False).count(),
}
instance = Instance.objects.create(
instance_name="Plane Community Edition",
@@ -66,15 +60,4 @@ class Command(BaseCommand):
self.stdout.write(
self.style.SUCCESS("Instance already registered")
)
payload = self.read_package_json()
# Update the instance details
instance.last_checked_at = timezone.now()
instance.user_count = payload.get("user_count", 0)
instance.current_version = payload.get("version")
instance.latest_version = payload.get("version")
instance.save()
# Call the instance traces task
instance_traces.delay()
return
return

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