Compare commits

..

3 Commits

Author SHA1 Message Date
gurusainath 4ec2c61e0d chore: updated the discriptiob_binary key 2024-11-14 17:40:52 +05:30
gurusainath 641ef9fd14 chore: handled descritpion_binary error 2024-11-14 17:10:55 +05:30
gurusainath 68a4d02847 chore: added description_binary in issues v2 pagination list 2024-11-12 15:20:02 +05:30
839 changed files with 14987 additions and 15247 deletions
-20
View File
@@ -1,20 +0,0 @@
### Description
<!-- Provide a detailed description of the changes in this PR -->
### Type of Change
<!-- Put an 'x' in the boxes that apply -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] Feature (non-breaking change which adds functionality)
- [ ] Improvement (change that would cause existing functionality to not work as expected)
- [ ] Code refactoring
- [ ] Performance improvements
- [ ] Documentation update
### Screenshots and Media (if applicable)
<!-- Add screenshots to help explain your changes, ideally showcasing before and after -->
### Test Scenarios
<!-- Please describe the tests that you ran to verify your changes -->
### References
<!-- Link related issues if there are any -->
+2 -2
View File
@@ -83,7 +83,7 @@ jobs:
endpoint: ${{ env.BUILDX_ENDPOINT }}
- name: Build and Push to Docker Hub
uses: docker/build-push-action@v6.9.0
uses: docker/build-push-action@v5.1.0
with:
context: ./aio
file: ./aio/Dockerfile-base-full
@@ -124,7 +124,7 @@ jobs:
endpoint: ${{ env.BUILDX_ENDPOINT }}
- name: Build and Push to Docker Hub
uses: docker/build-push-action@v6.9.0
uses: docker/build-push-action@v5.1.0
with:
context: ./aio
file: ./aio/Dockerfile-base-slim
+2 -2
View File
@@ -128,7 +128,7 @@ jobs:
uses: actions/checkout@v4
- name: Build and Push to Docker Hub
uses: docker/build-push-action@v6.9.0
uses: docker/build-push-action@v5.1.0
with:
context: .
file: ./aio/Dockerfile-app
@@ -188,7 +188,7 @@ jobs:
uses: actions/checkout@v4
- name: Build and Push to Docker Hub
uses: docker/build-push-action@v6.9.0
uses: docker/build-push-action@v5.1.0
with:
context: .
file: ./aio/Dockerfile-app
+6 -4
View File
@@ -25,6 +25,9 @@ on:
required: false
default: false
type: boolean
# push:
# branches:
# - master
env:
TARGET_BRANCH: ${{ github.ref_name }}
@@ -314,8 +317,8 @@ jobs:
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
attach_assets_to_build:
if: ${{ needs.branch_build_setup.outputs.build_type == 'Release' }}
name: Attach Assets to Release
if: ${{ needs.branch_build_setup.outputs.build_type == 'Build' }}
name: Attach Assets to Build
runs-on: ubuntu-20.04
needs: [branch_build_setup]
steps:
@@ -351,7 +354,6 @@ jobs:
branch_build_push_live,
branch_build_push_apiserver,
branch_build_push_proxy,
attach_assets_to_build,
]
env:
REL_VERSION: ${{ needs.branch_build_setup.outputs.release_version }}
@@ -365,7 +367,7 @@ jobs:
- name: Create Release
id: create_release
uses: softprops/action-gh-release@v2.1.0
uses: softprops/action-gh-release@v2.0.8
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token
with:
+4 -4
View File
@@ -29,11 +29,11 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -46,7 +46,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v3
uses: github/codeql-action/autobuild@v2
# ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@@ -59,6 +59,6 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
uses: github/codeql-action/analyze@v2
with:
category: "/language:${{matrix.language}}"
+1 -1
View File
@@ -79,7 +79,7 @@ jobs:
uses: actions/checkout@v4
- name: Build and Push to Docker Hub
uses: docker/build-push-action@v6.9.0
uses: docker/build-push-action@v5.1.0
with:
context: .
file: ./aio/Dockerfile-app
+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v4.1.1
with:
fetch-depth: 0 # Fetch all history for all branches and tags
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
contents: read
steps:
- name: Checkout Code
uses: actions/checkout@v4
uses: actions/checkout@v4.1.1
with:
persist-credentials: false
fetch-depth: 0
+1 -6
View File
@@ -121,12 +121,7 @@ export const InstanceAIForm: FC<IInstanceAIForm> = (props) => {
<div className="relative inline-flex items-center gap-2 rounded border border-custom-primary-100/20 bg-custom-primary-100/10 px-4 py-2 text-xs text-custom-primary-200">
<Lightbulb height="14" width="14" />
<div>
If you have a preferred AI models vendor, please get in{" "}
<a className="underline font-medium" href="https://plane.so/contact">
touch with us.
</a>
</div>
<div>If you have a preferred AI models vendor, please get in touch with us.</div>
</div>
</div>
</div>
+1 -1
View File
@@ -195,7 +195,7 @@ export const InstanceGithubConfigForm: FC<Props> = (props) => {
</Button>
<Link
href="/authentication"
className={cn(getButtonStyling("neutral-primary", "md"), "font-medium")}
className={cn(getButtonStyling("link-neutral", "md"), "font-medium")}
onClick={handleGoBack}
>
Go back
+3 -3
View File
@@ -44,7 +44,7 @@ const InstanceGithubAuthenticationPage = observer(() => {
loading: "Saving Configuration...",
success: {
title: "Configuration saved",
message: () => `GitHub authentication is now ${value ? "active" : "disabled"}.`,
message: () => `Github authentication is now ${value ? "active" : "disabled"}.`,
},
error: {
title: "Error",
@@ -67,8 +67,8 @@ const InstanceGithubAuthenticationPage = observer(() => {
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
<div className="border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
<AuthenticationMethodCard
name="GitHub"
description="Allow members to login or sign up to plane with their GitHub accounts."
name="Github"
description="Allow members to login or sign up to plane with their Github accounts."
icon={
<Image
src={resolveGeneralTheme(resolvedTheme) === "dark" ? githubDarkModeImage : githubLightModeImage}
+1 -1
View File
@@ -191,7 +191,7 @@ export const InstanceGitlabConfigForm: FC<Props> = (props) => {
</Button>
<Link
href="/authentication"
className={cn(getButtonStyling("neutral-primary", "md"), "font-medium")}
className={cn(getButtonStyling("link-neutral", "md"), "font-medium")}
onClick={handleGoBack}
>
Go back
+1 -1
View File
@@ -192,7 +192,7 @@ export const InstanceGoogleConfigForm: FC<Props> = (props) => {
</Button>
<Link
href="/authentication"
className={cn(getButtonStyling("neutral-primary", "md"), "font-medium")}
className={cn(getButtonStyling("link-neutral", "md"), "font-medium")}
onClick={handleGoBack}
>
Go back
+5 -7
View File
@@ -60,7 +60,7 @@ const InstanceAuthenticationPage = observer(() => {
<div className="border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
<div className="text-xl font-medium text-custom-text-100">Manage authentication modes for your instance</div>
<div className="text-sm font-normal text-custom-text-300">
Configure authentication modes for your team and restrict sign-ups to be invite only.
Configure authentication modes for your team and restrict sign ups to be invite only.
</div>
</div>
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
@@ -80,11 +80,9 @@ const InstanceAuthenticationPage = observer(() => {
<ToggleSwitch
value={Boolean(parseInt(enableSignUpConfig))}
onChange={() => {
if (Boolean(parseInt(enableSignUpConfig)) === true) {
updateConfig("ENABLE_SIGNUP", "0");
} else {
updateConfig("ENABLE_SIGNUP", "1");
}
Boolean(parseInt(enableSignUpConfig)) === true
? updateConfig("ENABLE_SIGNUP", "0")
: updateConfig("ENABLE_SIGNUP", "1");
}}
size="sm"
disabled={isSubmitting}
@@ -92,7 +90,7 @@ const InstanceAuthenticationPage = observer(() => {
</div>
</div>
</div>
<div className="text-lg font-medium pt-6">Available authentication modes</div>
<div className="text-lg font-medium pt-6">Authentication modes</div>
<AuthenticationModes disabled={isSubmitting} updateConfig={updateConfig} />
</div>
) : (
+4 -4
View File
@@ -72,7 +72,7 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
{
key: "EMAIL_FROM",
type: "text",
label: "Sender's email address",
label: "Sender email address",
description:
"This is the email address your users will see when getting emails from this instance. You will need to verify this address.",
placeholder: "no-reply@projectplane.so",
@@ -174,12 +174,12 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
</div>
</div>
<div className="flex flex-col gap-6 my-6 pt-4 border-t border-custom-border-100">
<div className="flex w-full max-w-xl flex-col gap-y-10 px-1">
<div className="flex w-full max-w-md flex-col gap-y-10 px-1">
<div className="mr-8 flex items-center gap-10 pt-4">
<div className="grow">
<div className="text-sm font-medium text-custom-text-100">Authentication</div>
<div className="text-sm font-medium text-custom-text-100">Authentication (optional)</div>
<div className="text-xs font-normal text-custom-text-300">
This is optional, but we recommend setting up a username and a password for your SMTP server.
We recommend setting up a username password for your SMTP server
</div>
</div>
</div>
+3 -4
View File
@@ -117,18 +117,17 @@ export const GeneralConfigurationForm: FC<IGeneralConfigurationForm> = observer(
</div>
<div className="grow">
<div className="text-sm font-medium text-custom-text-100 leading-5">
Let Plane collect anonymous usage data
Allow Plane to collect anonymous usage events
</div>
<div className="text-xs font-normal text-custom-text-300 leading-5">
No PII is collected.This anonymized data is used to understand how you use Plane and build new features
in line with{" "}
We collect usage events without any PII to analyse and improve Plane.{" "}
<a
href="https://docs.plane.so/self-hosting/telemetry"
target="_blank"
className="text-custom-primary-100 hover:underline"
rel="noreferrer"
>
our Telemetry Policy.
Know more.
</a>
</div>
</div>
+2 -2
View File
@@ -60,9 +60,9 @@ export const IntercomConfig: FC<TIntercomConfig> = observer((props) => {
</div>
<div className="grow">
<div className="text-sm font-medium text-custom-text-100 leading-5">Chat with us</div>
<div className="text-sm font-medium text-custom-text-100 leading-5">Talk to Plane</div>
<div className="text-xs font-normal text-custom-text-300 leading-5">
Let your users chat with us via Intercom or another service. Toggling Telemetry off turns this off
Let your members chat with us via Intercom or another service. Toggling Telemetry off turns this off
automatically.
</div>
</div>
-214
View File
@@ -1,214 +0,0 @@
import { useState, useEffect } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { Controller, useForm } from "react-hook-form";
// constants
import { ORGANIZATION_SIZE, RESTRICTED_URLS } from "@plane/constants";
// types
import { IWorkspace } from "@plane/types";
// components
import { Button, CustomSelect, getButtonStyling, Input, setToast, TOAST_TYPE } from "@plane/ui";
// helpers
import { WEB_BASE_URL } from "@/helpers/common.helper";
// hooks
import { useWorkspace } from "@/hooks/store";
// services
import { WorkspaceService } from "@/services/workspace.service";
const workspaceService = new WorkspaceService();
export const WorkspaceCreateForm = () => {
// router
const router = useRouter();
// states
const [slugError, setSlugError] = useState(false);
const [invalidSlug, setInvalidSlug] = useState(false);
const [defaultValues, setDefaultValues] = useState<Partial<IWorkspace>>({
name: "",
slug: "",
organization_size: "",
});
// store hooks
const { createWorkspace } = useWorkspace();
// form info
const {
handleSubmit,
control,
setValue,
getValues,
formState: { errors, isSubmitting, isValid },
} = useForm<IWorkspace>({ defaultValues, mode: "onChange" });
// derived values
const workspaceBaseURL = encodeURI(WEB_BASE_URL || window.location.origin + "/");
const handleCreateWorkspace = async (formData: IWorkspace) => {
await workspaceService
.workspaceSlugCheck(formData.slug)
.then(async (res) => {
if (res.status === true && !RESTRICTED_URLS.includes(formData.slug)) {
setSlugError(false);
await createWorkspace(formData)
.then(async () => {
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "Workspace created successfully.",
});
router.push(`/workspace`);
})
.catch(() => {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Workspace could not be created. Please try again.",
});
});
} else setSlugError(true);
})
.catch(() => {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Some error occurred while creating workspace. Please try again.",
});
});
};
useEffect(
() => () => {
// when the component unmounts set the default values to whatever user typed in
setDefaultValues(getValues());
},
[getValues, setDefaultValues]
);
return (
<div className="space-y-8">
<div className="grid-col grid w-full max-w-4xl grid-cols-1 items-start justify-between gap-x-10 gap-y-6 lg:grid-cols-2">
<div className="flex flex-col gap-1">
<h4 className="text-sm text-custom-text-300">Name your workspace</h4>
<div className="flex flex-col gap-1">
<Controller
control={control}
name="name"
rules={{
required: "This is a required field.",
validate: (value) =>
/^[\w\s-]*$/.test(value) ||
`Workspaces names can contain only (" "), ( - ), ( _ ) and alphanumeric characters.`,
maxLength: {
value: 80,
message: "Limit your name to 80 characters.",
},
}}
render={({ field: { value, ref, onChange } }) => (
<Input
id="workspaceName"
type="text"
value={value}
onChange={(e) => {
onChange(e.target.value);
setValue("name", e.target.value);
setValue("slug", e.target.value.toLocaleLowerCase().trim().replace(/ /g, "-"), {
shouldValidate: true,
});
}}
ref={ref}
hasError={Boolean(errors.name)}
placeholder="Something familiar and recognizable is always best."
className="w-full"
/>
)}
/>
<span className="text-xs text-red-500">{errors?.name?.message}</span>
</div>
</div>
<div className="flex flex-col gap-1">
<h4 className="text-sm text-custom-text-300">Set your workspace&apos;s URL</h4>
<div className="flex gap-0.5 w-full items-center rounded-md border-[0.5px] border-custom-border-200 px-3">
<span className="whitespace-nowrap text-sm text-custom-text-200">{workspaceBaseURL}</span>
<Controller
control={control}
name="slug"
rules={{
required: "The URL is a required field.",
maxLength: {
value: 48,
message: "Limit your URL to 48 characters.",
},
}}
render={({ field: { onChange, value, ref } }) => (
<Input
id="workspaceUrl"
type="text"
value={value.toLocaleLowerCase().trim().replace(/ /g, "-")}
onChange={(e) => {
if (/^[a-zA-Z0-9_-]+$/.test(e.target.value)) setInvalidSlug(false);
else setInvalidSlug(true);
onChange(e.target.value.toLowerCase());
}}
ref={ref}
hasError={Boolean(errors.slug)}
placeholder="workspace-name"
className="block w-full rounded-md border-none bg-transparent !px-0 py-2 text-sm"
/>
)}
/>
</div>
{slugError && <p className="text-sm text-red-500">This URL is taken. Try something else.</p>}
{invalidSlug && (
<p className="text-sm text-red-500">{`URLs can contain only ( - ), ( _ ) and alphanumeric characters.`}</p>
)}
{errors.slug && <span className="text-xs text-red-500">{errors.slug.message}</span>}
</div>
<div className="flex flex-col gap-1">
<h4 className="text-sm text-custom-text-300">How many people will use this workspace?</h4>
<div className="w-full">
<Controller
name="organization_size"
control={control}
rules={{ required: "This is a required field." }}
render={({ field: { value, onChange } }) => (
<CustomSelect
value={value}
onChange={onChange}
label={
ORGANIZATION_SIZE.find((c) => c === value) ?? (
<span className="text-custom-text-400">Select a range</span>
)
}
buttonClassName="!border-[0.5px] !border-custom-border-200 !shadow-none"
input
optionsClassName="w-full"
>
{ORGANIZATION_SIZE.map((item) => (
<CustomSelect.Option key={item} value={item}>
{item}
</CustomSelect.Option>
))}
</CustomSelect>
)}
/>
{errors.organization_size && (
<span className="text-sm text-red-500">{errors.organization_size.message}</span>
)}
</div>
</div>
</div>
<div className="flex max-w-4xl items-center py-1 gap-4">
<Button
variant="primary"
size="sm"
onClick={handleSubmit(handleCreateWorkspace)}
disabled={!isValid}
loading={isSubmitting}
>
{isSubmitting ? "Creating workspace" : "Create workspace"}
</Button>
<Link className={getButtonStyling("neutral-primary", "sm")} href="/workspace">
Go back
</Link>
</div>
</div>
);
};
-21
View File
@@ -1,21 +0,0 @@
"use client";
import { observer } from "mobx-react";
// components
import { WorkspaceCreateForm } from "./form";
const WorkspaceCreatePage = observer(() => (
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
<div className="border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
<div className="text-xl font-medium text-custom-text-100">Create a new workspace on this instance.</div>
<div className="text-sm font-normal text-custom-text-300">
You will need to invite users from Workspace Settings after you create this workspace.
</div>
</div>
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
<WorkspaceCreateForm />
</div>
</div>
));
export default WorkspaceCreatePage;
-12
View File
@@ -1,12 +0,0 @@
import { ReactNode } from "react";
import { Metadata } from "next";
// layouts
import { AdminLayout } from "@/layouts/admin-layout";
export const metadata: Metadata = {
title: "Workspace Management - Plane Web",
};
export default function WorkspaceManagementLayout({ children }: { children: ReactNode }) {
return <AdminLayout>{children}</AdminLayout>;
}
-169
View File
@@ -1,169 +0,0 @@
"use client";
import { useState } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import useSWR from "swr";
import { Loader as LoaderIcon } from "lucide-react";
// types
import { TInstanceConfigurationKeys } from "@plane/types";
// ui
import { Button, getButtonStyling, Loader, setPromiseToast, ToggleSwitch } from "@plane/ui";
// components
import { WorkspaceListItem } from "@/components/workspace";
// helpers
import { cn } from "@/helpers/common.helper";
// hooks
import { useInstance, useWorkspace } from "@/hooks/store";
const WorkspaceManagementPage = observer(() => {
// states
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
// store
const { formattedConfig, fetchInstanceConfigurations, updateInstanceConfigurations } = useInstance();
const {
workspaceIds,
loader: workspaceLoader,
paginationInfo,
fetchWorkspaces,
fetchNextWorkspaces,
} = useWorkspace();
// derived values
const disableWorkspaceCreation = formattedConfig?.DISABLE_WORKSPACE_CREATION ?? "";
const hasNextPage = paginationInfo?.next_page_results && paginationInfo?.next_cursor !== undefined;
// fetch data
useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations());
useSWR("INSTANCE_WORKSPACES", () => fetchWorkspaces());
const updateConfig = async (key: TInstanceConfigurationKeys, value: string) => {
setIsSubmitting(true);
const payload = {
[key]: value,
};
const updateConfigPromise = updateInstanceConfigurations(payload);
setPromiseToast(updateConfigPromise, {
loading: "Saving configuration",
success: {
title: "Success",
message: () => "Configuration saved successfully",
},
error: {
title: "Error",
message: () => "Failed to save configuration",
},
});
await updateConfigPromise
.then(() => {
setIsSubmitting(false);
})
.catch((err) => {
console.error(err);
setIsSubmitting(false);
});
};
return (
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
<div className="flex items-center justify-between gap-4 border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
<div className="flex flex-col gap-1">
<div className="text-xl font-medium text-custom-text-100">Workspaces on this instance</div>
<div className="text-sm font-normal text-custom-text-300">
See all workspaces and control who can create them.
</div>
</div>
</div>
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
<div className="space-y-3">
{formattedConfig ? (
<div className={cn("w-full flex items-center gap-14 rounded")}>
<div className="flex grow items-center gap-4">
<div className="grow">
<div className="text-lg font-medium pb-1">Prevent anyone else from creating a workspace.</div>
<div className={cn("font-normal leading-5 text-custom-text-300 text-xs")}>
Toggling this on will let only you create workspaces. You will have to invite users to new
workspaces.
</div>
</div>
</div>
<div className={`shrink-0 pr-4 ${isSubmitting && "opacity-70"}`}>
<div className="flex items-center gap-4">
<ToggleSwitch
value={Boolean(parseInt(disableWorkspaceCreation))}
onChange={() => {
if (Boolean(parseInt(disableWorkspaceCreation)) === true) {
updateConfig("DISABLE_WORKSPACE_CREATION", "0");
} else {
updateConfig("DISABLE_WORKSPACE_CREATION", "1");
}
}}
size="sm"
disabled={isSubmitting}
/>
</div>
</div>
</div>
) : (
<Loader>
<Loader.Item height="50px" width="100%" />
</Loader>
)}
{workspaceLoader !== "init-loader" ? (
<>
<div className="pt-6 flex items-center justify-between gap-2">
<div className="flex flex-col items-start gap-x-2">
<div className="flex items-center gap-2 text-lg font-medium">
All workspaces on this instance{" "}
<span className="text-custom-text-300"> {workspaceIds.length}</span>
{workspaceLoader && ["mutation", "pagination"].includes(workspaceLoader) && (
<LoaderIcon className="w-4 h-4 animate-spin" />
)}
</div>
<div className={cn("font-normal leading-5 text-custom-text-300 text-xs")}>
You can&apos;t yet delete workspaces and you can only go to the workspace if you are an Admin or a
Member.
</div>
</div>
<div className="flex items-center gap-2">
<Link href="/workspace/create" className={getButtonStyling("primary", "sm")}>
Create workspace
</Link>
</div>
</div>
<div className="flex flex-col gap-4 py-2">
{workspaceIds.map((workspaceId) => (
<WorkspaceListItem key={workspaceId} workspaceId={workspaceId} />
))}
</div>
{hasNextPage && (
<div className="flex justify-center">
<Button
variant="link-primary"
onClick={() => fetchNextWorkspaces()}
disabled={workspaceLoader === "pagination"}
>
Load more
{workspaceLoader === "pagination" && <LoaderIcon className="w-3 h-3 animate-spin" />}
</Button>
</div>
)}
</>
) : (
<Loader className="space-y-10 py-8">
<Loader.Item height="24px" width="20%" />
<Loader.Item height="92px" width="100%" />
<Loader.Item height="92px" width="100%" />
<Loader.Item height="92px" width="100%" />
</Loader>
)}
</div>
</div>
</div>
);
});
export default WorkspaceManagementPage;
@@ -9,8 +9,8 @@ import { getButtonStyling } from "@plane/ui";
import { cn } from "@/helpers/common.helper";
export const UpgradeButton: React.FC = () => (
<a href="https://plane.so/pricing?mode=self-hosted" target="_blank" className={cn(getButtonStyling("primary", "sm"))}>
Upgrade
<a href="https://plane.so/one" target="_blank" className={cn(getButtonStyling("primary", "sm"))}>
Available on One
<SquareArrowOutUpRight className="h-3.5 w-3.5 p-0.5" />
</a>
);
@@ -52,13 +52,13 @@ export const HelpSection: FC = observer(() => {
)}
>
<div className={`flex items-center gap-1 ${isSidebarCollapsed ? "flex-col justify-center" : "w-full"}`}>
<Tooltip tooltipContent="Redirect to Plane" position="right" className="ml-4" disabled={!isSidebarCollapsed}>
<Tooltip tooltipContent="Redirect to plane" position="right" className="ml-4" disabled={!isSidebarCollapsed}>
<a
href={redirectionLink}
className={`relative px-2 py-1.5 flex items-center gap-2 font-medium rounded border border-custom-primary-100/20 bg-custom-primary-100/10 text-xs text-custom-primary-200 whitespace-nowrap`}
>
<ExternalLink size={14} />
{!isSidebarCollapsed && "Redirect to Plane"}
{!isSidebarCollapsed && "Redirect to plane"}
</a>
</Tooltip>
<Tooltip tooltipContent="Help" position={isSidebarCollapsed ? "right" : "top"} className="ml-4">
+1 -1
View File
@@ -3,7 +3,7 @@
import { FC, useEffect, useRef } from "react";
import { observer } from "mobx-react";
// plane helpers
import { useOutsideClickDetector } from "@plane/hooks";
import { useOutsideClickDetector } from "@plane/helpers";
// components
import { HelpSection, SidebarMenu, SidebarDropdown } from "@/components/admin-sidebar";
// hooks
@@ -4,7 +4,7 @@ import { observer } from "mobx-react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { Image, BrainCog, Cog, Lock, Mail } from "lucide-react";
import { Tooltip, WorkspaceIcon } from "@plane/ui";
import { Tooltip } from "@plane/ui";
// hooks
import { cn } from "@/helpers/common.helper";
import { useTheme } from "@/hooks/store";
@@ -14,37 +14,31 @@ const INSTANCE_ADMIN_LINKS = [
{
Icon: Cog,
name: "General",
description: "Identify your instances and get key details.",
description: "Identify your instances and get key details",
href: `/general/`,
},
{
Icon: WorkspaceIcon,
name: "Workspaces",
description: "Manage all workspaces on this instance.",
href: `/workspace/`,
},
{
Icon: Mail,
name: "Email",
description: "Configure your SMTP controls.",
description: "Set up emails to your users",
href: `/email/`,
},
{
Icon: Lock,
name: "Authentication",
description: "Configure authentication modes.",
description: "Configure authentication modes",
href: `/authentication/`,
},
{
Icon: BrainCog,
name: "Artificial intelligence",
description: "Configure your OpenAI creds.",
description: "Configure your OpenAI creds",
href: `/ai/`,
},
{
Icon: Image,
name: "Images in Plane",
description: "Allow third-party image libraries.",
description: "Allow third-party image libraries",
href: `/image/`,
},
];
+1 -5
View File
@@ -30,13 +30,9 @@ export const InstanceHeader: FC = observer(() => {
case "google":
return "Google";
case "github":
return "GitHub";
return "Github";
case "gitlab":
return "GitLab";
case "workspace":
return "Workspace";
case "create":
return "Create";
default:
return pathName.toUpperCase();
}
+7 -5
View File
@@ -1,13 +1,13 @@
"use client";
import React from "react";
import { resolveGeneralTheme } from "helpers/common.helper";
import { observer } from "mobx-react";
import Image from "next/image";
import Link from "next/link";
import { useTheme as nextUseTheme } from "next-themes";
// ui
import { Button, getButtonStyling } from "@plane/ui";
// helpers
import { WEB_BASE_URL, resolveGeneralTheme } from "helpers/common.helper";
// hooks
import { useTheme } from "@/hooks/store";
// icons
@@ -20,6 +20,8 @@ export const NewUserPopup: React.FC = observer(() => {
// theme
const { resolvedTheme } = nextUseTheme();
const redirectionLink = encodeURI(WEB_BASE_URL + "/create-workspace");
if (!isNewUserPopup) return <></>;
return (
<div className="absolute bottom-8 right-8 p-6 w-96 border border-custom-border-100 shadow-md rounded-lg bg-custom-background-100">
@@ -28,12 +30,12 @@ export const NewUserPopup: React.FC = observer(() => {
<div className="text-base font-semibold">Create workspace</div>
<div className="py-2 text-sm font-medium text-custom-text-300">
Instance setup done! Welcome to Plane instance portal. Start your journey with by creating your first
workspace.
workspace, you will need to login again.
</div>
<div className="flex items-center gap-4 pt-2">
<Link href="/workspace/create" className={getButtonStyling("primary", "sm")}>
<a href={redirectionLink} className={getButtonStyling("primary", "sm")}>
Create workspace
</Link>
</a>
<Button variant="neutral-primary" size="sm" onClick={toggleNewUserPopup}>
Close
</Button>
-1
View File
@@ -1 +0,0 @@
export * from "./list-item";
@@ -1,81 +0,0 @@
import { observer } from "mobx-react";
import { ExternalLink } from "lucide-react";
// helpers
import { Tooltip } from "@plane/ui";
import { WEB_BASE_URL } from "@/helpers/common.helper";
import { getFileURL } from "@/helpers/file.helper";
// hooks
import { useWorkspace } from "@/hooks/store";
type TWorkspaceListItemProps = {
workspaceId: string;
};
export const WorkspaceListItem = observer(({ workspaceId }: TWorkspaceListItemProps) => {
// store hooks
const { getWorkspaceById } = useWorkspace();
// derived values
const workspace = getWorkspaceById(workspaceId);
if (!workspace) return null;
return (
<a
key={workspaceId}
href={`${WEB_BASE_URL}/${encodeURIComponent(workspace.slug)}`}
target="_blank"
className="group flex items-center justify-between p-4 gap-2.5 truncate border border-custom-border-200/70 hover:border-custom-border-200 hover:bg-custom-background-90 rounded-md"
>
<div className="flex items-start gap-4">
<span
className={`relative flex h-8 w-8 flex-shrink-0 items-center justify-center p-2 mt-1 text-xs uppercase ${
!workspace?.logo_url && "rounded bg-custom-primary-500 text-white"
}`}
>
{workspace?.logo_url && workspace.logo_url !== "" ? (
<img
src={getFileURL(workspace.logo_url)}
className="absolute left-0 top-0 h-full w-full rounded object-cover"
alt="Workspace Logo"
/>
) : (
(workspace?.name?.[0] ?? "...")
)}
</span>
<div className="flex flex-col items-start gap-1">
<div className="flex flex-wrap w-full items-center gap-2.5">
<h3 className={`text-base font-medium capitalize`}>{workspace.name}</h3>/
<Tooltip tooltipContent="The unique URL of your workspace">
<h4 className="text-sm text-custom-text-300">[{workspace.slug}]</h4>
</Tooltip>
</div>
{workspace.owner.email && (
<div className="flex items-center gap-1 text-xs">
<h3 className="text-custom-text-200 font-medium">Owned by:</h3>
<h4 className="text-custom-text-300">{workspace.owner.email}</h4>
</div>
)}
<div className="flex items-center gap-2.5 text-xs">
{workspace.total_projects !== null && (
<span className="flex items-center gap-1">
<h3 className="text-custom-text-200 font-medium">Total projects:</h3>
<h4 className="text-custom-text-300">{workspace.total_projects}</h4>
</span>
)}
{workspace.total_members !== null && (
<>
<span className="flex items-center gap-1">
<h3 className="text-custom-text-200 font-medium">Total members:</h3>
<h4 className="text-custom-text-300">{workspace.total_members}</h4>
</span>
</>
)}
</div>
</div>
</div>
<div className="flex-shrink-0">
<ExternalLink size={14} className="text-custom-text-400 group-hover:text-custom-text-200" />
</div>
</a>
);
});
-1
View File
@@ -1,4 +1,3 @@
export * from "./use-theme";
export * from "./use-instance";
export * from "./use-user";
export * from "./use-workspace";
-10
View File
@@ -1,10 +0,0 @@
import { useContext } from "react";
// store
import { StoreContext } from "@/lib/store-provider";
import { IWorkspaceStore } from "@/store/workspace.store";
export const useWorkspace = (): IWorkspaceStore => {
const context = useContext(StoreContext);
if (context === undefined) throw new Error("useWorkspace must be used within StoreProvider");
return context.workspace;
};
-53
View File
@@ -1,53 +0,0 @@
// types
import type { IWorkspace, TWorkspacePaginationInfo } from "@plane/types";
// helpers
import { API_BASE_URL } from "@/helpers/common.helper";
// services
import { APIService } from "@/services/api.service";
export class WorkspaceService extends APIService {
constructor() {
super(API_BASE_URL);
}
/**
* @description Fetches all workspaces
* @returns Promise<TWorkspacePaginationInfo>
*/
async getWorkspaces(nextPageCursor?: string): Promise<TWorkspacePaginationInfo> {
return this.get<TWorkspacePaginationInfo>("/api/instances/workspaces/", {
cursor: nextPageCursor,
})
.then((response) => response.data)
.catch((error) => {
throw error?.response?.data;
});
}
/**
* @description Checks if a slug is available
* @param slug - string
* @returns Promise<any>
*/
async workspaceSlugCheck(slug: string): Promise<any> {
const params = new URLSearchParams({ slug });
return this.get(`/api/instances/workspace-slug-check/?${params.toString()}`)
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
/**
* @description Creates a new workspace
* @param data - IWorkspace
* @returns Promise<IWorkspace>
*/
async createWorkspace(data: IWorkspace): Promise<IWorkspace> {
return this.post<IWorkspace, IWorkspace>("/api/instances/workspaces/", data)
.then((response) => response.data)
.catch((error) => {
throw error?.response?.data;
});
}
}
-5
View File
@@ -3,7 +3,6 @@ import { enableStaticRendering } from "mobx-react";
import { IInstanceStore, InstanceStore } from "./instance.store";
import { IThemeStore, ThemeStore } from "./theme.store";
import { IUserStore, UserStore } from "./user.store";
import { IWorkspaceStore, WorkspaceStore } from "./workspace.store";
enableStaticRendering(typeof window === "undefined");
@@ -11,20 +10,17 @@ export abstract class CoreRootStore {
theme: IThemeStore;
instance: IInstanceStore;
user: IUserStore;
workspace: IWorkspaceStore;
constructor() {
this.theme = new ThemeStore(this);
this.instance = new InstanceStore(this);
this.user = new UserStore(this);
this.workspace = new WorkspaceStore(this);
}
hydrate(initialData: any) {
this.theme.hydrate(initialData.theme);
this.instance.hydrate(initialData.instance);
this.user.hydrate(initialData.user);
this.workspace.hydrate(initialData.workspace);
}
resetOnSignOut() {
@@ -32,6 +28,5 @@ export abstract class CoreRootStore {
this.instance = new InstanceStore(this);
this.user = new UserStore(this);
this.theme = new ThemeStore(this);
this.workspace = new WorkspaceStore(this);
}
}
-150
View File
@@ -1,150 +0,0 @@
import set from "lodash/set";
import { action, observable, runInAction, makeObservable, computed } from "mobx";
import { IWorkspace, TLoader, TPaginationInfo } from "@plane/types";
// services
import { WorkspaceService } from "@/services/workspace.service";
// root store
import { CoreRootStore } from "@/store/root.store";
export interface IWorkspaceStore {
// observables
loader: TLoader;
workspaces: Record<string, IWorkspace>;
paginationInfo: TPaginationInfo | undefined;
// computed
workspaceIds: string[];
// helper actions
hydrate: (data: Record<string, IWorkspace>) => void;
getWorkspaceById: (workspaceId: string) => IWorkspace | undefined;
// fetch actions
fetchWorkspaces: () => Promise<IWorkspace[]>;
fetchNextWorkspaces: () => Promise<IWorkspace[]>;
// curd actions
createWorkspace: (data: IWorkspace) => Promise<IWorkspace>;
}
export class WorkspaceStore implements IWorkspaceStore {
// observables
loader: TLoader = "init-loader";
workspaces: Record<string, IWorkspace> = {};
paginationInfo: TPaginationInfo | undefined = undefined;
// services
workspaceService;
constructor(private store: CoreRootStore) {
makeObservable(this, {
// observables
loader: observable,
workspaces: observable,
paginationInfo: observable,
// computed
workspaceIds: computed,
// helper actions
hydrate: action,
getWorkspaceById: action,
// fetch actions
fetchWorkspaces: action,
fetchNextWorkspaces: action,
// curd actions
createWorkspace: action,
});
this.workspaceService = new WorkspaceService();
}
// computed
get workspaceIds() {
return Object.keys(this.workspaces);
}
// helper actions
/**
* @description Hydrates the workspaces
* @param data - Record<string, IWorkspace>
*/
hydrate = (data: Record<string, IWorkspace>) => {
if (data) this.workspaces = data;
};
/**
* @description Gets a workspace by id
* @param workspaceId - string
* @returns IWorkspace | undefined
*/
getWorkspaceById = (workspaceId: string) => this.workspaces[workspaceId];
// fetch actions
/**
* @description Fetches all workspaces
* @returns Promise<>
*/
fetchWorkspaces = async (): Promise<IWorkspace[]> => {
try {
if (this.workspaceIds.length > 0) {
this.loader = "mutation";
} else {
this.loader = "init-loader";
}
const paginatedWorkspaceData = await this.workspaceService.getWorkspaces();
runInAction(() => {
const { results, ...paginationInfo } = paginatedWorkspaceData;
results.forEach((workspace: IWorkspace) => {
set(this.workspaces, [workspace.id], workspace);
});
set(this, "paginationInfo", paginationInfo);
});
return paginatedWorkspaceData.results;
} catch (error) {
console.error("Error fetching workspaces", error);
throw error;
} finally {
this.loader = "loaded";
}
};
/**
* @description Fetches the next page of workspaces
* @returns Promise<IWorkspace[]>
*/
fetchNextWorkspaces = async (): Promise<IWorkspace[]> => {
if (!this.paginationInfo || this.paginationInfo.next_page_results === false) return [];
try {
this.loader = "pagination";
const paginatedWorkspaceData = await this.workspaceService.getWorkspaces(this.paginationInfo.next_cursor);
runInAction(() => {
const { results, ...paginationInfo } = paginatedWorkspaceData;
results.forEach((workspace: IWorkspace) => {
set(this.workspaces, [workspace.id], workspace);
});
set(this, "paginationInfo", paginationInfo);
});
return paginatedWorkspaceData.results;
} catch (error) {
console.error("Error fetching next workspaces", error);
throw error;
} finally {
this.loader = "loaded";
}
};
// curd actions
/**
* @description Creates a new workspace
* @param data - IWorkspace
* @returns Promise<IWorkspace>
*/
createWorkspace = async (data: IWorkspace): Promise<IWorkspace> => {
try {
this.loader = "mutation";
const workspace = await this.workspaceService.createWorkspace(data);
runInAction(() => {
set(this.workspaces, [workspace.id], workspace);
});
return workspace;
} catch (error) {
console.error("Error creating workspace", error);
throw error;
} finally {
this.loader = "loaded";
}
};
}
+3 -4
View File
@@ -1,6 +1,6 @@
{
"name": "admin",
"version": "0.24.1",
"version": "0.23.1",
"private": true,
"scripts": {
"dev": "turbo run develop",
@@ -14,10 +14,9 @@
"dependencies": {
"@headlessui/react": "^1.7.19",
"@plane/constants": "*",
"@plane/hooks": "*",
"@plane/helpers": "*",
"@plane/types": "*",
"@plane/ui": "*",
"@plane/utils": "*",
"@sentry/nextjs": "^8.32.0",
"@tailwindcss/typography": "^0.5.9",
"@types/lodash": "^4.17.0",
@@ -27,7 +26,7 @@
"lucide-react": "^0.356.0",
"mobx": "^6.12.0",
"mobx-react": "^9.1.1",
"next": "^14.2.20",
"next": "^14.2.12",
"next-themes": "^0.2.1",
"postcss": "^8.4.38",
"react": "^18.3.1",
+1 -1
View File
@@ -4,7 +4,7 @@ FROM python:3.12.5-alpine AS backend
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
ENV INSTANCE_CHANGELOG_URL https://sites.plane.so/pages/691ef037bcfe416a902e48cb55f59891/
ENV INSTANCE_CHANGELOG_URL https://api.plane.so/api/public/anchor/8e1c2e4c7bc5493eb7731be3862f6960/pages/
WORKDIR /code
+1 -1
View File
@@ -4,7 +4,7 @@ FROM python:3.12.5-alpine AS backend
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
ENV INSTANCE_CHANGELOG_URL https://sites.plane.so/pages/691ef037bcfe416a902e48cb55f59891/
ENV INSTANCE_CHANGELOG_URL https://api.plane.so/api/public/anchor/8e1c2e4c7bc5493eb7731be3862f6960/pages/
RUN apk --no-cache add \
"bash~=5.2" \
+21 -7
View File
@@ -26,7 +26,9 @@ def update_description():
updated_issues.append(issue)
Issue.objects.bulk_update(
updated_issues, ["description_html", "description_stripped"], batch_size=100
updated_issues,
["description_html", "description_stripped"],
batch_size=100,
)
print("Success")
except Exception as e:
@@ -40,7 +42,9 @@ def update_comments():
updated_issue_comments = []
for issue_comment in issue_comments:
issue_comment.comment_html = f"<p>{issue_comment.comment_stripped}</p>"
issue_comment.comment_html = (
f"<p>{issue_comment.comment_stripped}</p>"
)
updated_issue_comments.append(issue_comment)
IssueComment.objects.bulk_update(
@@ -99,7 +103,9 @@ def updated_issue_sort_order():
issue.sort_order = issue.sequence_id * random.randint(100, 500)
updated_issues.append(issue)
Issue.objects.bulk_update(updated_issues, ["sort_order"], batch_size=100)
Issue.objects.bulk_update(
updated_issues, ["sort_order"], batch_size=100
)
print("Success")
except Exception as e:
print(e)
@@ -137,7 +143,9 @@ def update_project_cover_images():
project.cover_image = project_cover_images[random.randint(0, 19)]
updated_projects.append(project)
Project.objects.bulk_update(updated_projects, ["cover_image"], batch_size=100)
Project.objects.bulk_update(
updated_projects, ["cover_image"], batch_size=100
)
print("Success")
except Exception as e:
print(e)
@@ -186,7 +194,9 @@ def update_label_color():
def create_slack_integration():
try:
_ = Integration.objects.create(provider="slack", network=2, title="Slack")
_ = Integration.objects.create(
provider="slack", network=2, title="Slack"
)
print("Success")
except Exception as e:
print(e)
@@ -212,12 +222,16 @@ def update_integration_verified():
def update_start_date():
try:
issues = Issue.objects.filter(state__group__in=["started", "completed"])
issues = Issue.objects.filter(
state__group__in=["started", "completed"]
)
updated_issues = []
for issue in issues:
issue.start_date = issue.created_at.date()
updated_issues.append(issue)
Issue.objects.bulk_update(updated_issues, ["start_date"], batch_size=500)
Issue.objects.bulk_update(
updated_issues, ["start_date"], batch_size=500
)
print("Success")
except Exception as e:
print(e)
+3 -1
View File
@@ -3,7 +3,9 @@ import os
import sys
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "plane.settings.production")
os.environ.setdefault(
"DJANGO_SETTINGS_MODULE", "plane.settings.production"
)
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
+1 -1
View File
@@ -1,4 +1,4 @@
{
"name": "plane-api",
"version": "0.24.1"
"version": "0.23.1"
}
@@ -25,7 +25,10 @@ class APIKeyAuthentication(authentication.BaseAuthentication):
def validate_api_token(self, token):
try:
api_token = APIToken.objects.get(
Q(Q(expired_at__gt=timezone.now()) | Q(expired_at__isnull=True)),
Q(
Q(expired_at__gt=timezone.now())
| Q(expired_at__isnull=True)
),
token=token,
is_active=True,
)
+1 -1
View File
@@ -80,4 +80,4 @@ class ServiceTokenRateThrottle(SimpleRateThrottle):
request.META["X-RateLimit-Remaining"] = max(0, available)
request.META["X-RateLimit-Reset"] = reset_time
return allowed
return allowed
+5 -1
View File
@@ -13,5 +13,9 @@ from .issue import (
)
from .state import StateLiteSerializer, StateSerializer
from .cycle import CycleSerializer, CycleIssueSerializer, CycleLiteSerializer
from .module import ModuleSerializer, ModuleIssueSerializer, ModuleLiteSerializer
from .module import (
ModuleSerializer,
ModuleIssueSerializer,
ModuleLiteSerializer,
)
from .intake import IntakeIssueSerializer
+3 -1
View File
@@ -102,6 +102,8 @@ class BaseSerializer(serializers.ModelSerializer):
response[expand] = exp_serializer.data
else:
# You might need to handle this case differently
response[expand] = getattr(instance, f"{expand}_id", None)
response[expand] = getattr(
instance, f"{expand}_id", None
)
return response
+8 -14
View File
@@ -4,7 +4,7 @@ from rest_framework import serializers
# Module imports
from .base import BaseSerializer
from plane.db.models import Cycle, CycleIssue
from plane.utils.timezone_converter import convert_to_utc
class CycleSerializer(BaseSerializer):
total_issues = serializers.IntegerField(read_only=True)
@@ -23,18 +23,8 @@ class CycleSerializer(BaseSerializer):
and data.get("end_date", None) is not None
and data.get("start_date", None) > data.get("end_date", None)
):
raise serializers.ValidationError("Start date cannot exceed end date")
if (
data.get("start_date", None) is not None
and data.get("end_date", None) is not None
):
project_id = self.initial_data.get("project_id") or self.instance.project_id
data["start_date"] = convert_to_utc(
str(data.get("start_date").date()), project_id, is_start_date=True
)
data["end_date"] = convert_to_utc(
str(data.get("end_date", None).date()), project_id
raise serializers.ValidationError(
"Start date cannot exceed end date"
)
return data
@@ -60,7 +50,11 @@ class CycleIssueSerializer(BaseSerializer):
class Meta:
model = CycleIssue
fields = "__all__"
read_only_fields = ["workspace", "project", "cycle"]
read_only_fields = [
"workspace",
"project",
"cycle",
]
class CycleLiteSerializer(BaseSerializer):
@@ -6,6 +6,7 @@ from rest_framework import serializers
class IntakeIssueSerializer(BaseSerializer):
issue_detail = IssueExpandSerializer(read_only=True, source="issue")
inbox = serializers.UUIDField(source="intake.id", read_only=True)
+61 -18
View File
@@ -49,13 +49,25 @@ class IssueSerializer(BaseSerializer):
required=False,
)
type_id = serializers.PrimaryKeyRelatedField(
source="type", queryset=IssueType.objects.all(), required=False, allow_null=True
source="type",
queryset=IssueType.objects.all(),
required=False,
allow_null=True,
)
class Meta:
model = Issue
read_only_fields = ["id", "workspace", "project", "updated_by", "updated_at"]
exclude = ["description", "description_stripped"]
read_only_fields = [
"id",
"workspace",
"project",
"updated_by",
"updated_at",
]
exclude = [
"description",
"description_stripped",
]
def validate(self, data):
if (
@@ -63,7 +75,9 @@ class IssueSerializer(BaseSerializer):
and data.get("target_date", None) is not None
and data.get("start_date", None) > data.get("target_date", None)
):
raise serializers.ValidationError("Start date cannot exceed target date")
raise serializers.ValidationError(
"Start date cannot exceed target date"
)
try:
if data.get("description_html", None) is not None:
@@ -85,14 +99,16 @@ class IssueSerializer(BaseSerializer):
# Validate labels are from project
if data.get("labels", []):
data["labels"] = Label.objects.filter(
project_id=self.context.get("project_id"), id__in=data["labels"]
project_id=self.context.get("project_id"),
id__in=data["labels"],
).values_list("id", flat=True)
# Check state is from the project only else raise validation error
if (
data.get("state")
and not State.objects.filter(
project_id=self.context.get("project_id"), pk=data.get("state").id
project_id=self.context.get("project_id"),
pk=data.get("state").id,
).exists()
):
raise serializers.ValidationError(
@@ -103,7 +119,8 @@ class IssueSerializer(BaseSerializer):
if (
data.get("parent")
and not Issue.objects.filter(
workspace_id=self.context.get("workspace_id"), pk=data.get("parent").id
workspace_id=self.context.get("workspace_id"),
pk=data.get("parent").id,
).exists()
):
raise serializers.ValidationError(
@@ -130,7 +147,9 @@ class IssueSerializer(BaseSerializer):
issue_type = issue_type
issue = Issue.objects.create(
**validated_data, project_id=project_id, type=issue_type
**validated_data,
project_id=project_id,
type=issue_type,
)
# Issue Audit Users
@@ -245,9 +264,13 @@ class IssueSerializer(BaseSerializer):
]
if "labels" in self.fields:
if "labels" in self.expand:
data["labels"] = LabelSerializer(instance.labels.all(), many=True).data
data["labels"] = LabelSerializer(
instance.labels.all(), many=True
).data
else:
data["labels"] = [str(label.id) for label in instance.labels.all()]
data["labels"] = [
str(label.id) for label in instance.labels.all()
]
return data
@@ -255,7 +278,11 @@ class IssueSerializer(BaseSerializer):
class IssueLiteSerializer(BaseSerializer):
class Meta:
model = Issue
fields = ["id", "sequence_id", "project_id"]
fields = [
"id",
"sequence_id",
"project_id",
]
read_only_fields = fields
@@ -307,7 +334,8 @@ class IssueLinkSerializer(BaseSerializer):
# Validation if url already exists
def create(self, validated_data):
if IssueLink.objects.filter(
url=validated_data.get("url"), issue_id=validated_data.get("issue_id")
url=validated_data.get("url"),
issue_id=validated_data.get("issue_id"),
).exists():
raise serializers.ValidationError(
{"error": "URL already exists for this Issue"}
@@ -317,7 +345,8 @@ class IssueLinkSerializer(BaseSerializer):
def update(self, instance, validated_data):
if (
IssueLink.objects.filter(
url=validated_data.get("url"), issue_id=instance.issue_id
url=validated_data.get("url"),
issue_id=instance.issue_id,
)
.exclude(pk=instance.id)
.exists()
@@ -358,7 +387,10 @@ class IssueCommentSerializer(BaseSerializer):
"created_at",
"updated_at",
]
exclude = ["comment_stripped", "comment_json"]
exclude = [
"comment_stripped",
"comment_json",
]
def validate(self, data):
try:
@@ -375,27 +407,38 @@ class IssueCommentSerializer(BaseSerializer):
class IssueActivitySerializer(BaseSerializer):
class Meta:
model = IssueActivity
exclude = ["created_by", "updated_by"]
exclude = [
"created_by",
"updated_by",
]
class CycleIssueSerializer(BaseSerializer):
cycle = CycleSerializer(read_only=True)
class Meta:
fields = ["cycle"]
fields = [
"cycle",
]
class ModuleIssueSerializer(BaseSerializer):
module = ModuleSerializer(read_only=True)
class Meta:
fields = ["module"]
fields = [
"module",
]
class LabelLiteSerializer(BaseSerializer):
class Meta:
model = Label
fields = ["id", "name", "color"]
fields = [
"id",
"name",
"color",
]
class IssueExpandSerializer(BaseSerializer):
+13 -5
View File
@@ -53,11 +53,14 @@ class ModuleSerializer(BaseSerializer):
and data.get("target_date", None) is not None
and data.get("start_date", None) > data.get("target_date", None)
):
raise serializers.ValidationError("Start date cannot exceed target date")
raise serializers.ValidationError(
"Start date cannot exceed target date"
)
if data.get("members", []):
data["members"] = ProjectMember.objects.filter(
project_id=self.context.get("project_id"), member_id__in=data["members"]
project_id=self.context.get("project_id"),
member_id__in=data["members"],
).values_list("member_id", flat=True)
return data
@@ -71,7 +74,9 @@ class ModuleSerializer(BaseSerializer):
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():
if Module.objects.filter(
name=module_name, project_id=project_id
).exists():
raise serializers.ValidationError(
{"error": "Module with this name already exists"}
)
@@ -102,7 +107,9 @@ class ModuleSerializer(BaseSerializer):
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)
Module.objects.filter(
name=module_name, project=instance.project
)
.exclude(id=instance.id)
.exists()
):
@@ -165,7 +172,8 @@ class ModuleLinkSerializer(BaseSerializer):
# Validation if url already exists
def create(self, validated_data):
if ModuleLink.objects.filter(
url=validated_data.get("url"), module_id=validated_data.get("module_id")
url=validated_data.get("url"),
module_id=validated_data.get("module_id"),
).exists():
raise serializers.ValidationError(
{"error": "URL already exists for this Issue"}
+11 -3
View File
@@ -2,7 +2,11 @@
from rest_framework import serializers
# Module imports
from plane.db.models import Project, ProjectIdentifier, WorkspaceMember
from plane.db.models import (
Project,
ProjectIdentifier,
WorkspaceMember,
)
from .base import BaseSerializer
@@ -63,12 +67,16 @@ class ProjectSerializer(BaseSerializer):
def create(self, validated_data):
identifier = validated_data.get("identifier", "").strip().upper()
if identifier == "":
raise serializers.ValidationError(detail="Project Identifier is required")
raise serializers.ValidationError(
detail="Project Identifier is required"
)
if ProjectIdentifier.objects.filter(
name=identifier, workspace_id=self.context["workspace_id"]
).exists():
raise serializers.ValidationError(detail="Project Identifier is taken")
raise serializers.ValidationError(
detail="Project Identifier is taken"
)
project = Project.objects.create(
**validated_data, workspace_id=self.context["workspace_id"]
+9 -4
View File
@@ -7,9 +7,9 @@ class StateSerializer(BaseSerializer):
def validate(self, data):
# If the default is being provided then make all other states default False
if data.get("default", False):
State.objects.filter(project_id=self.context.get("project_id")).update(
default=False
)
State.objects.filter(
project_id=self.context.get("project_id")
).update(default=False)
return data
class Meta:
@@ -30,5 +30,10 @@ class StateSerializer(BaseSerializer):
class StateLiteSerializer(BaseSerializer):
class Meta:
model = State
fields = ["id", "name", "color", "group"]
fields = [
"id",
"name",
"color",
"group",
]
read_only_fields = fields
+5 -1
View File
@@ -8,5 +8,9 @@ class WorkspaceLiteSerializer(BaseSerializer):
class Meta:
model = Workspace
fields = ["name", "slug", "id"]
fields = [
"name",
"slug",
"id",
]
read_only_fields = fields
+4 -2
View File
@@ -1,11 +1,13 @@
from django.urls import path
from plane.api.views import ProjectMemberAPIEndpoint
from plane.api.views import (
ProjectMemberAPIEndpoint,
)
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<str:project_id>/members/",
ProjectMemberAPIEndpoint.as_view(),
name="users",
)
),
]
+7 -2
View File
@@ -1,10 +1,15 @@
from django.urls import path
from plane.api.views import ProjectAPIEndpoint, ProjectArchiveUnarchiveAPIEndpoint
from plane.api.views import (
ProjectAPIEndpoint,
ProjectArchiveUnarchiveAPIEndpoint,
)
urlpatterns = [
path(
"workspaces/<str:slug>/projects/", ProjectAPIEndpoint.as_view(), name="project"
"workspaces/<str:slug>/projects/",
ProjectAPIEndpoint.as_view(),
name="project",
),
path(
"workspaces/<str:slug>/projects/<uuid:pk>/",
+18 -7
View File
@@ -37,9 +37,13 @@ class TimezoneMixin:
class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
authentication_classes = [APIKeyAuthentication]
authentication_classes = [
APIKeyAuthentication,
]
permission_classes = [IsAuthenticated]
permission_classes = [
IsAuthenticated,
]
def filter_queryset(self, queryset):
for backend in list(self.filter_backends):
@@ -52,7 +56,8 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
if api_key:
service_token = APIToken.objects.filter(
token=api_key, is_service=True
token=api_key,
is_service=True,
).first()
if service_token:
@@ -118,7 +123,9 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
def finalize_response(self, request, response, *args, **kwargs):
# Call super to get the default response
response = super().finalize_response(request, response, *args, **kwargs)
response = super().finalize_response(
request, response, *args, **kwargs
)
# Add custom headers if they exist in the request META
ratelimit_remaining = request.META.get("X-RateLimit-Remaining")
@@ -147,13 +154,17 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
@property
def fields(self):
fields = [
field for field in self.request.GET.get("fields", "").split(",") if field
field
for field in self.request.GET.get("fields", "").split(",")
if field
]
return fields if fields else None
@property
def expand(self):
expand = [
expand for expand in self.request.GET.get("expand", "").split(",") if expand
expand
for expand in self.request.GET.get("expand", "").split(",")
if expand
]
return expand if expand else None
return expand if expand else None
+138 -46
View File
@@ -25,7 +25,10 @@ from rest_framework import status
from rest_framework.response import Response
# Module imports
from plane.api.serializers import CycleIssueSerializer, CycleSerializer
from plane.api.serializers import (
CycleIssueSerializer,
CycleSerializer,
)
from plane.app.permissions import ProjectEntityPermission
from plane.bgtasks.issue_activities_task import issue_activity
from plane.db.models import (
@@ -54,7 +57,9 @@ class CycleAPIEndpoint(BaseAPIView):
serializer_class = CycleSerializer
model = Cycle
webhook_event = "cycle"
permission_classes = [ProjectEntityPermission]
permission_classes = [
ProjectEntityPermission,
]
def get_queryset(self):
return (
@@ -138,18 +143,26 @@ class CycleAPIEndpoint(BaseAPIView):
def get(self, request, slug, project_id, pk=None):
if pk:
queryset = self.get_queryset().filter(archived_at__isnull=True).get(pk=pk)
queryset = (
self.get_queryset().filter(archived_at__isnull=True).get(pk=pk)
)
data = CycleSerializer(
queryset, fields=self.fields, expand=self.expand
queryset,
fields=self.fields,
expand=self.expand,
).data
return Response(data, status=status.HTTP_200_OK)
return Response(
data,
status=status.HTTP_200_OK,
)
queryset = self.get_queryset().filter(archived_at__isnull=True)
cycle_view = request.GET.get("cycle_view", "all")
# Current Cycle
if cycle_view == "current":
queryset = queryset.filter(
start_date__lte=timezone.now(), end_date__gte=timezone.now()
start_date__lte=timezone.now(),
end_date__gte=timezone.now(),
)
data = CycleSerializer(
queryset, many=True, fields=self.fields, expand=self.expand
@@ -163,7 +176,10 @@ class CycleAPIEndpoint(BaseAPIView):
request=request,
queryset=(queryset),
on_results=lambda cycles: CycleSerializer(
cycles, many=True, fields=self.fields, expand=self.expand
cycles,
many=True,
fields=self.fields,
expand=self.expand,
).data,
)
@@ -174,38 +190,53 @@ class CycleAPIEndpoint(BaseAPIView):
request=request,
queryset=(queryset),
on_results=lambda cycles: CycleSerializer(
cycles, many=True, fields=self.fields, expand=self.expand
cycles,
many=True,
fields=self.fields,
expand=self.expand,
).data,
)
# Draft Cycles
if cycle_view == "draft":
queryset = queryset.filter(end_date=None, start_date=None)
queryset = queryset.filter(
end_date=None,
start_date=None,
)
return self.paginate(
request=request,
queryset=(queryset),
on_results=lambda cycles: CycleSerializer(
cycles, many=True, fields=self.fields, expand=self.expand
cycles,
many=True,
fields=self.fields,
expand=self.expand,
).data,
)
# Incomplete Cycles
if cycle_view == "incomplete":
queryset = queryset.filter(
Q(end_date__gte=timezone.now()) | Q(end_date__isnull=True)
Q(end_date__gte=timezone.now()) | Q(end_date__isnull=True),
)
return self.paginate(
request=request,
queryset=(queryset),
on_results=lambda cycles: CycleSerializer(
cycles, many=True, fields=self.fields, expand=self.expand
cycles,
many=True,
fields=self.fields,
expand=self.expand,
).data,
)
return self.paginate(
request=request,
queryset=(queryset),
on_results=lambda cycles: CycleSerializer(
cycles, many=True, fields=self.fields, expand=self.expand
cycles,
many=True,
fields=self.fields,
expand=self.expand,
).data,
)
@@ -242,7 +273,10 @@ class CycleAPIEndpoint(BaseAPIView):
},
status=status.HTTP_409_CONFLICT,
)
serializer.save(project_id=project_id, owned_by=request.user)
serializer.save(
project_id=project_id,
owned_by=request.user,
)
# Send the model activity
model_activity.delay(
model_name="cycle",
@@ -253,8 +287,12 @@ class CycleAPIEndpoint(BaseAPIView):
slug=slug,
origin=request.META.get("HTTP_ORIGIN"),
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
return Response(
serializer.data, status=status.HTTP_201_CREATED
)
return Response(
serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
else:
return Response(
{
@@ -264,7 +302,9 @@ class CycleAPIEndpoint(BaseAPIView):
)
def patch(self, request, slug, project_id, pk):
cycle = Cycle.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
cycle = Cycle.objects.get(
workspace__slug=slug, project_id=project_id, pk=pk
)
current_instance = json.dumps(
CycleSerializer(cycle).data, cls=DjangoJSONEncoder
@@ -282,7 +322,9 @@ class CycleAPIEndpoint(BaseAPIView):
if "sort_order" in request_data:
# Can only change sort order
request_data = {
"sort_order": request_data.get("sort_order", cycle.sort_order)
"sort_order": request_data.get(
"sort_order", cycle.sort_order
)
}
else:
return Response(
@@ -329,7 +371,9 @@ class CycleAPIEndpoint(BaseAPIView):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, slug, project_id, pk):
cycle = Cycle.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
cycle = Cycle.objects.get(
workspace__slug=slug, project_id=project_id, pk=pk
)
if cycle.owned_by_id != request.user.id and (
not ProjectMember.objects.filter(
workspace__slug=slug,
@@ -345,9 +389,9 @@ class CycleAPIEndpoint(BaseAPIView):
)
cycle_issues = list(
CycleIssue.objects.filter(cycle_id=self.kwargs.get("pk")).values_list(
"issue", flat=True
)
CycleIssue.objects.filter(
cycle_id=self.kwargs.get("pk")
).values_list("issue", flat=True)
)
issue_activity.delay(
@@ -369,13 +413,17 @@ class CycleAPIEndpoint(BaseAPIView):
cycle.delete()
# Delete the user favorite cycle
UserFavorite.objects.filter(
entity_type="cycle", entity_identifier=pk, project_id=project_id
entity_type="cycle",
entity_identifier=pk,
project_id=project_id,
).delete()
return Response(status=status.HTTP_204_NO_CONTENT)
class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView):
permission_classes = [ProjectEntityPermission]
permission_classes = [
ProjectEntityPermission,
]
def get_queryset(self):
return (
@@ -454,7 +502,9 @@ class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView):
),
)
)
.annotate(total_estimates=Sum("issue_cycle__issue__estimate_point"))
.annotate(
total_estimates=Sum("issue_cycle__issue__estimate_point")
)
.annotate(
completed_estimates=Sum(
"issue_cycle__issue__estimate_point",
@@ -486,7 +536,10 @@ class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView):
request=request,
queryset=(self.get_queryset()),
on_results=lambda cycles: CycleSerializer(
cycles, many=True, fields=self.fields, expand=self.expand
cycles,
many=True,
fields=self.fields,
expand=self.expand,
).data,
)
@@ -529,12 +582,16 @@ class CycleIssueAPIEndpoint(BaseAPIView):
model = CycleIssue
webhook_event = "cycle_issue"
bulk = True
permission_classes = [ProjectEntityPermission]
permission_classes = [
ProjectEntityPermission,
]
def get_queryset(self):
return (
CycleIssue.objects.annotate(
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("issue_id"))
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("issue_id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
@@ -573,10 +630,13 @@ class CycleIssueAPIEndpoint(BaseAPIView):
order_by = request.GET.get("order_by", "created_at")
issues = (
Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id, issue_cycle__deleted_at__isnull=True
issue_cycle__cycle_id=cycle_id,
issue_cycle__deleted_at__isnull=True,
)
.annotate(
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
@@ -612,7 +672,10 @@ class CycleIssueAPIEndpoint(BaseAPIView):
request=request,
queryset=(issues),
on_results=lambda issues: CycleSerializer(
issues, many=True, fields=self.fields, expand=self.expand
issues,
many=True,
fields=self.fields,
expand=self.expand,
).data,
)
@@ -621,7 +684,8 @@ class CycleIssueAPIEndpoint(BaseAPIView):
if not issues:
return Response(
{"error": "Issues are required"}, status=status.HTTP_400_BAD_REQUEST
{"error": "Issues are required"},
status=status.HTTP_400_BAD_REQUEST,
)
cycle = Cycle.objects.get(
@@ -630,7 +694,9 @@ class CycleIssueAPIEndpoint(BaseAPIView):
# Get all CycleIssues already created
cycle_issues = list(
CycleIssue.objects.filter(~Q(cycle_id=cycle_id), issue_id__in=issues)
CycleIssue.objects.filter(
~Q(cycle_id=cycle_id), issue_id__in=issues
)
)
existing_issues = [
@@ -675,7 +741,9 @@ class CycleIssueAPIEndpoint(BaseAPIView):
)
# Update the cycle issues
CycleIssue.objects.bulk_update(updated_records, ["cycle_id"], batch_size=100)
CycleIssue.objects.bulk_update(
updated_records, ["cycle_id"], batch_size=100
)
# Capture Issue Activity
issue_activity.delay(
@@ -734,7 +802,9 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
"""
permission_classes = [ProjectEntityPermission]
permission_classes = [
ProjectEntityPermission,
]
def post(self, request, slug, project_id, cycle_id):
new_cycle_id = request.data.get("new_cycle_id", False)
@@ -860,7 +930,9 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
)
.values("display_name", "assignee_id", "avatar", "avatar_url")
.annotate(
total_estimates=Sum(Cast("estimate_point__value", FloatField()))
total_estimates=Sum(
Cast("estimate_point__value", FloatField())
)
)
.annotate(
completed_estimates=Sum(
@@ -889,7 +961,9 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
{
"display_name": item["display_name"],
"assignee_id": (
str(item["assignee_id"]) if item["assignee_id"] else None
str(item["assignee_id"])
if item["assignee_id"]
else None
),
"avatar": item.get("avatar", None),
"avatar_url": item.get("avatar_url", None),
@@ -912,7 +986,9 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
.annotate(label_id=F("labels__id"))
.values("label_name", "color", "label_id")
.annotate(
total_estimates=Sum(Cast("estimate_point__value", FloatField()))
total_estimates=Sum(
Cast("estimate_point__value", FloatField())
)
)
.annotate(
completed_estimates=Sum(
@@ -949,7 +1025,9 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
{
"label_name": item["label_name"],
"color": item["color"],
"label_id": (str(item["label_id"]) if item["label_id"] else None),
"label_id": (
str(item["label_id"]) if item["label_id"] else None
),
"total_estimates": item["total_estimates"],
"completed_estimates": item["completed_estimates"],
"pending_estimates": item["pending_estimates"],
@@ -981,7 +1059,8 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
),
# If `avatar_asset` is None, fall back to using `avatar` field directly
When(
assignees__avatar_asset__isnull=True, then="assignees__avatar"
assignees__avatar_asset__isnull=True,
then="assignees__avatar",
),
default=Value(None),
output_field=models.CharField(),
@@ -990,8 +1069,12 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
.values("display_name", "assignee_id", "avatar_url")
.annotate(
total_issues=Count(
"id", filter=Q(archived_at__isnull=True, is_draft=False)
)
"id",
filter=Q(
archived_at__isnull=True,
is_draft=False,
),
),
)
.annotate(
completed_issues=Count(
@@ -1045,8 +1128,12 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
.values("label_name", "color", "label_id")
.annotate(
total_issues=Count(
"id", filter=Q(archived_at__isnull=True, is_draft=False)
)
"id",
filter=Q(
archived_at__isnull=True,
is_draft=False,
),
),
)
.annotate(
completed_issues=Count(
@@ -1076,7 +1163,9 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
{
"label_name": item["label_name"],
"color": item["color"],
"label_id": (str(item["label_id"]) if item["label_id"] else None),
"label_id": (
str(item["label_id"]) if item["label_id"] else None
),
"total_issues": item["total_issues"],
"completed_issues": item["completed_issues"],
"pending_issues": item["pending_issues"],
@@ -1121,7 +1210,10 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
}
current_cycle.save(update_fields=["progress_snapshot"])
if new_cycle.end_date is not None and new_cycle.end_date < timezone.now():
if (
new_cycle.end_date is not None
and new_cycle.end_date < timezone.now()
):
return Response(
{
"error": "The cycle where the issues are transferred is already completed"
+77 -24
View File
@@ -17,7 +17,14 @@ from rest_framework.response import Response
from plane.api.serializers import IntakeIssueSerializer, IssueSerializer
from plane.app.permissions import ProjectLitePermission
from plane.bgtasks.issue_activities_task import issue_activity
from plane.db.models import Intake, IntakeIssue, Issue, Project, ProjectMember, State
from plane.db.models import (
Intake,
IntakeIssue,
Issue,
Project,
ProjectMember,
State,
)
from .base import BaseAPIView
@@ -29,12 +36,16 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
"""
permission_classes = [ProjectLitePermission]
permission_classes = [
ProjectLitePermission,
]
serializer_class = IntakeIssueSerializer
model = IntakeIssue
filterset_fields = ["status"]
filterset_fields = [
"status",
]
def get_queryset(self):
intake = Intake.objects.filter(
@@ -43,7 +54,8 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
).first()
project = Project.objects.get(
workspace__slug=self.kwargs.get("slug"), pk=self.kwargs.get("project_id")
workspace__slug=self.kwargs.get("slug"),
pk=self.kwargs.get("project_id"),
)
if intake is None and not project.intake_view:
@@ -51,7 +63,8 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
return (
IntakeIssue.objects.filter(
Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True),
Q(snoozed_till__gte=timezone.now())
| Q(snoozed_till__isnull=True),
workspace__slug=self.kwargs.get("slug"),
project_id=self.kwargs.get("project_id"),
intake_id=intake.id,
@@ -64,29 +77,41 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
if issue_id:
intake_issue_queryset = self.get_queryset().get(issue_id=issue_id)
intake_issue_data = IntakeIssueSerializer(
intake_issue_queryset, fields=self.fields, expand=self.expand
intake_issue_queryset,
fields=self.fields,
expand=self.expand,
).data
return Response(intake_issue_data, status=status.HTTP_200_OK)
return Response(
intake_issue_data,
status=status.HTTP_200_OK,
)
issue_queryset = self.get_queryset()
return self.paginate(
request=request,
queryset=(issue_queryset),
on_results=lambda intake_issues: IntakeIssueSerializer(
intake_issues, many=True, fields=self.fields, expand=self.expand
intake_issues,
many=True,
fields=self.fields,
expand=self.expand,
).data,
)
def post(self, request, slug, project_id):
if not request.data.get("issue", {}).get("name", False):
return Response(
{"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST
{"error": "Name is required"},
status=status.HTTP_400_BAD_REQUEST,
)
intake = Intake.objects.filter(
workspace__slug=slug, project_id=project_id
).first()
project = Project.objects.get(workspace__slug=slug, pk=project_id)
project = Project.objects.get(
workspace__slug=slug,
pk=project_id,
)
# Intake view
if intake is None and not project.intake_view:
@@ -106,7 +131,8 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
"none",
]:
return Response(
{"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST
{"error": "Invalid priority"},
status=status.HTTP_400_BAD_REQUEST,
)
# Create or get state
@@ -158,7 +184,10 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
workspace__slug=slug, project_id=project_id
).first()
project = Project.objects.get(workspace__slug=slug, pk=project_id)
project = Project.objects.get(
workspace__slug=slug,
pk=project_id,
)
# Intake view
if intake is None and not project.intake_view:
@@ -205,7 +234,7 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
distinct=True,
filter=Q(
~Q(labels__id__isnull=True)
& Q(label_issue__deleted_at__isnull=True)
& Q(label_issue__deleted_at__isnull=True),
),
),
Value([], output_field=ArrayField(UUIDField())),
@@ -222,7 +251,11 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
),
Value([], output_field=ArrayField(UUIDField())),
),
).get(pk=issue_id, workspace__slug=slug, project_id=project_id)
).get(
pk=issue_id,
workspace__slug=slug,
project_id=project_id,
)
# Only allow guests to edit name and description
if project_member.role <= 5:
issue_data = {
@@ -230,10 +263,14 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
"description_html": issue_data.get(
"description_html", issue.description_html
),
"description": issue_data.get("description", issue.description),
"description": issue_data.get(
"description", issue.description
),
}
issue_serializer = IssueSerializer(issue, data=issue_data, partial=True)
issue_serializer = IssueSerializer(
issue, data=issue_data, partial=True
)
if issue_serializer.is_valid():
current_instance = issue
@@ -273,10 +310,14 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
# Update the issue state if the issue is rejected or marked as duplicate
if serializer.data["status"] in [-1, 2]:
issue = Issue.objects.get(
pk=issue_id, workspace__slug=slug, project_id=project_id
pk=issue_id,
workspace__slug=slug,
project_id=project_id,
)
state = State.objects.filter(
group="cancelled", workspace__slug=slug, project_id=project_id
group="cancelled",
workspace__slug=slug,
project_id=project_id,
).first()
if state is not None:
issue.state = state
@@ -285,14 +326,18 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
# Update the issue state if it is accepted
if serializer.data["status"] in [1]:
issue = Issue.objects.get(
pk=issue_id, workspace__slug=slug, project_id=project_id
pk=issue_id,
workspace__slug=slug,
project_id=project_id,
)
# Update the issue state only if it is in triage state
if issue.state.is_triage:
# Move to default state
state = State.objects.filter(
workspace__slug=slug, project_id=project_id, default=True
workspace__slug=slug,
project_id=project_id,
default=True,
).first()
if state is not None:
issue.state = state
@@ -301,7 +346,9 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
# create a activity for status change
issue_activity.delay(
type="intake.activity.created",
requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
requested_data=json.dumps(
request.data, cls=DjangoJSONEncoder
),
actor_id=str(request.user.id),
issue_id=str(issue_id),
project_id=str(project_id),
@@ -313,10 +360,13 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
return Response(
serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
else:
return Response(
IntakeIssueSerializer(intake_issue).data, status=status.HTTP_200_OK
IntakeIssueSerializer(intake_issue).data,
status=status.HTTP_200_OK,
)
def delete(self, request, slug, project_id, issue_id):
@@ -324,7 +374,10 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
workspace__slug=slug, project_id=project_id
).first()
project = Project.objects.get(workspace__slug=slug, pk=project_id)
project = Project.objects.get(
workspace__slug=slug,
pk=project_id,
)
# Intake view
if intake is None and not project.intake_view:
+192 -57
View File
@@ -73,7 +73,9 @@ class WorkspaceIssueAPIEndpoint(BaseAPIView):
def get_queryset(self):
return (
Issue.issue_objects.annotate(
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
@@ -89,10 +91,14 @@ class WorkspaceIssueAPIEndpoint(BaseAPIView):
.order_by(self.kwargs.get("order_by", "-created_at"))
).distinct()
def get(self, request, slug, project__identifier=None, issue__identifier=None):
def get(
self, request, slug, project__identifier=None, issue__identifier=None
):
if issue__identifier and project__identifier:
issue = Issue.issue_objects.annotate(
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
@@ -102,7 +108,11 @@ class WorkspaceIssueAPIEndpoint(BaseAPIView):
sequence_id=issue__identifier,
)
return Response(
IssueSerializer(issue, fields=self.fields, expand=self.expand).data,
IssueSerializer(
issue,
fields=self.fields,
expand=self.expand,
).data,
status=status.HTTP_200_OK,
)
@@ -116,13 +126,17 @@ class IssueAPIEndpoint(BaseAPIView):
model = Issue
webhook_event = "issue"
permission_classes = [ProjectEntityPermission]
permission_classes = [
ProjectEntityPermission,
]
serializer_class = IssueSerializer
def get_queryset(self):
return (
Issue.issue_objects.annotate(
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
@@ -150,25 +164,41 @@ class IssueAPIEndpoint(BaseAPIView):
project_id=project_id,
)
return Response(
IssueSerializer(issue, fields=self.fields, expand=self.expand).data,
IssueSerializer(
issue,
fields=self.fields,
expand=self.expand,
).data,
status=status.HTTP_200_OK,
)
if pk:
issue = Issue.issue_objects.annotate(
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
).get(workspace__slug=slug, project_id=project_id, pk=pk)
return Response(
IssueSerializer(issue, fields=self.fields, expand=self.expand).data,
IssueSerializer(
issue,
fields=self.fields,
expand=self.expand,
).data,
status=status.HTTP_200_OK,
)
# Custom ordering for priority and state
priority_order = ["urgent", "high", "medium", "low", "none"]
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
state_order = [
"backlog",
"unstarted",
"started",
"completed",
"cancelled",
]
order_by_param = request.GET.get("order_by", "-created_at")
@@ -201,7 +231,9 @@ class IssueAPIEndpoint(BaseAPIView):
# Priority Ordering
if order_by_param == "priority" or order_by_param == "-priority":
priority_order = (
priority_order if order_by_param == "priority" else priority_order[::-1]
priority_order
if order_by_param == "priority"
else priority_order[::-1]
)
issue_queryset = issue_queryset.annotate(
priority_order=Case(
@@ -249,7 +281,9 @@ class IssueAPIEndpoint(BaseAPIView):
else order_by_param
)
).order_by(
"-max_values" if order_by_param.startswith("-") else "max_values"
"-max_values"
if order_by_param.startswith("-")
else "max_values"
)
else:
issue_queryset = issue_queryset.order_by(order_by_param)
@@ -258,7 +292,10 @@ class IssueAPIEndpoint(BaseAPIView):
request=request,
queryset=(issue_queryset),
on_results=lambda issues: IssueSerializer(
issues, many=True, fields=self.fields, expand=self.expand
issues,
many=True,
fields=self.fields,
expand=self.expand,
).data,
)
@@ -302,16 +339,22 @@ class IssueAPIEndpoint(BaseAPIView):
serializer.save()
# Refetch the issue
issue = Issue.objects.filter(
workspace__slug=slug, project_id=project_id, pk=serializer.data["id"]
workspace__slug=slug,
project_id=project_id,
pk=serializer.data["id"],
).first()
issue.created_at = request.data.get("created_at", timezone.now())
issue.created_by_id = request.data.get("created_by", request.user.id)
issue.created_by_id = request.data.get(
"created_by", request.user.id
)
issue.save(update_fields=["created_at", "created_by"])
# Track the issue
issue_activity.delay(
type="issue.activity.created",
requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder),
requested_data=json.dumps(
self.request.data, cls=DjangoJSONEncoder
),
actor_id=str(request.user.id),
issue_id=str(serializer.data.get("id", None)),
project_id=str(project_id),
@@ -348,7 +391,9 @@ class IssueAPIEndpoint(BaseAPIView):
# Get the requested data, encode it as django object and pass it
# to serializer to validation
requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder)
requested_data = json.dumps(
self.request.data, cls=DjangoJSONEncoder
)
serializer = IssueSerializer(
issue,
data=request.data,
@@ -406,7 +451,9 @@ class IssueAPIEndpoint(BaseAPIView):
# If any of the created_at or created_by is present, update
# the issue with the provided data, else return with the
# default states given.
issue.created_at = request.data.get("created_at", timezone.now())
issue.created_at = request.data.get(
"created_at", timezone.now()
)
issue.created_by_id = request.data.get(
"created_by", request.user.id
)
@@ -423,8 +470,12 @@ class IssueAPIEndpoint(BaseAPIView):
current_instance=None,
epoch=int(timezone.now().timestamp()),
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
return Response(
serializer.data, status=status.HTTP_201_CREATED
)
return Response(
serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
else:
return Response(
{"error": "external_id and external_source are required"},
@@ -432,7 +483,9 @@ class IssueAPIEndpoint(BaseAPIView):
)
def patch(self, request, slug, project_id, pk=None):
issue = Issue.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
issue = Issue.objects.get(
workspace__slug=slug, project_id=project_id, pk=pk
)
project = Project.objects.get(pk=project_id)
current_instance = json.dumps(
IssueSerializer(issue).data, cls=DjangoJSONEncoder
@@ -441,7 +494,10 @@ class IssueAPIEndpoint(BaseAPIView):
serializer = IssueSerializer(
issue,
data=request.data,
context={"project_id": project_id, "workspace_id": project.workspace_id},
context={
"project_id": project_id,
"workspace_id": project.workspace_id,
},
partial=True,
)
if serializer.is_valid():
@@ -479,7 +535,9 @@ class IssueAPIEndpoint(BaseAPIView):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, slug, project_id, pk=None):
issue = Issue.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
issue = Issue.objects.get(
workspace__slug=slug, project_id=project_id, pk=pk
)
if issue.created_by_id != request.user.id and (
not ProjectMember.objects.filter(
workspace__slug=slug,
@@ -518,7 +576,9 @@ class LabelAPIEndpoint(BaseAPIView):
serializer_class = LabelSerializer
model = Label
permission_classes = [ProjectMemberPermission]
permission_classes = [
ProjectMemberPermission,
]
def get_queryset(self):
return (
@@ -565,8 +625,12 @@ class LabelAPIEndpoint(BaseAPIView):
)
serializer.save(project_id=project_id)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
return Response(
serializer.data, status=status.HTTP_201_CREATED
)
return Response(
serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
except IntegrityError:
label = Label.objects.filter(
workspace__slug=slug,
@@ -587,11 +651,18 @@ class LabelAPIEndpoint(BaseAPIView):
request=request,
queryset=(self.get_queryset()),
on_results=lambda labels: LabelSerializer(
labels, many=True, fields=self.fields, expand=self.expand
labels,
many=True,
fields=self.fields,
expand=self.expand,
).data,
)
label = self.get_queryset().get(pk=pk)
serializer = LabelSerializer(label, fields=self.fields, expand=self.expand)
serializer = LabelSerializer(
label,
fields=self.fields,
expand=self.expand,
)
return Response(serializer.data, status=status.HTTP_200_OK)
def patch(self, request, slug, project_id, pk=None):
@@ -634,7 +705,9 @@ class IssueLinkAPIEndpoint(BaseAPIView):
"""
permission_classes = [ProjectEntityPermission]
permission_classes = [
ProjectEntityPermission,
]
model = IssueLink
serializer_class = IssueLinkSerializer
@@ -657,32 +730,46 @@ class IssueLinkAPIEndpoint(BaseAPIView):
if pk is None:
issue_links = self.get_queryset()
serializer = IssueLinkSerializer(
issue_links, fields=self.fields, expand=self.expand
issue_links,
fields=self.fields,
expand=self.expand,
)
return self.paginate(
request=request,
queryset=(self.get_queryset()),
on_results=lambda issue_links: IssueLinkSerializer(
issue_links, many=True, fields=self.fields, expand=self.expand
issue_links,
many=True,
fields=self.fields,
expand=self.expand,
).data,
)
issue_link = self.get_queryset().get(pk=pk)
serializer = IssueLinkSerializer(
issue_link, fields=self.fields, expand=self.expand
issue_link,
fields=self.fields,
expand=self.expand,
)
return Response(serializer.data, status=status.HTTP_200_OK)
def post(self, request, slug, project_id, issue_id):
serializer = IssueLinkSerializer(data=request.data)
if serializer.is_valid():
serializer.save(project_id=project_id, issue_id=issue_id)
serializer.save(
project_id=project_id,
issue_id=issue_id,
)
link = IssueLink.objects.get(pk=serializer.data["id"])
link.created_by_id = request.data.get("created_by", request.user.id)
link.created_by_id = request.data.get(
"created_by", request.user.id
)
link.save(update_fields=["created_by"])
issue_activity.delay(
type="link.activity.created",
requested_data=json.dumps(serializer.data, cls=DjangoJSONEncoder),
requested_data=json.dumps(
serializer.data, cls=DjangoJSONEncoder
),
issue_id=str(self.kwargs.get("issue_id")),
project_id=str(self.kwargs.get("project_id")),
actor_id=str(link.created_by_id),
@@ -694,13 +781,19 @@ class IssueLinkAPIEndpoint(BaseAPIView):
def patch(self, request, slug, project_id, issue_id, pk):
issue_link = IssueLink.objects.get(
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
workspace__slug=slug,
project_id=project_id,
issue_id=issue_id,
pk=pk,
)
requested_data = json.dumps(request.data, cls=DjangoJSONEncoder)
current_instance = json.dumps(
IssueLinkSerializer(issue_link).data, cls=DjangoJSONEncoder
IssueLinkSerializer(issue_link).data,
cls=DjangoJSONEncoder,
)
serializer = IssueLinkSerializer(
issue_link, data=request.data, partial=True
)
serializer = IssueLinkSerializer(issue_link, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
issue_activity.delay(
@@ -717,10 +810,14 @@ class IssueLinkAPIEndpoint(BaseAPIView):
def delete(self, request, slug, project_id, issue_id, pk):
issue_link = IssueLink.objects.get(
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
workspace__slug=slug,
project_id=project_id,
issue_id=issue_id,
pk=pk,
)
current_instance = json.dumps(
IssueLinkSerializer(issue_link).data, cls=DjangoJSONEncoder
IssueLinkSerializer(issue_link).data,
cls=DjangoJSONEncoder,
)
issue_activity.delay(
type="link.activity.deleted",
@@ -745,11 +842,15 @@ class IssueCommentAPIEndpoint(BaseAPIView):
serializer_class = IssueCommentSerializer
model = IssueComment
webhook_event = "issue_comment"
permission_classes = [ProjectLitePermission]
permission_classes = [
ProjectLitePermission,
]
def get_queryset(self):
return (
IssueComment.objects.filter(workspace__slug=self.kwargs.get("slug"))
IssueComment.objects.filter(
workspace__slug=self.kwargs.get("slug")
)
.filter(project_id=self.kwargs.get("project_id"))
.filter(issue_id=self.kwargs.get("issue_id"))
.filter(
@@ -776,14 +877,19 @@ class IssueCommentAPIEndpoint(BaseAPIView):
if pk:
issue_comment = self.get_queryset().get(pk=pk)
serializer = IssueCommentSerializer(
issue_comment, fields=self.fields, expand=self.expand
issue_comment,
fields=self.fields,
expand=self.expand,
)
return Response(serializer.data, status=status.HTTP_200_OK)
return self.paginate(
request=request,
queryset=(self.get_queryset()),
on_results=lambda issue_comment: IssueCommentSerializer(
issue_comment, many=True, fields=self.fields, expand=self.expand
issue_comment,
many=True,
fields=self.fields,
expand=self.expand,
).data,
)
@@ -816,11 +922,17 @@ class IssueCommentAPIEndpoint(BaseAPIView):
serializer = IssueCommentSerializer(data=request.data)
if serializer.is_valid():
serializer.save(
project_id=project_id, issue_id=issue_id, actor=request.user
project_id=project_id,
issue_id=issue_id,
actor=request.user,
)
issue_comment = IssueComment.objects.get(
pk=serializer.data.get("id")
)
issue_comment = IssueComment.objects.get(pk=serializer.data.get("id"))
# Update the created_at and the created_by and save the comment
issue_comment.created_at = request.data.get("created_at", timezone.now())
issue_comment.created_at = request.data.get(
"created_at", timezone.now()
)
issue_comment.created_by_id = request.data.get(
"created_by", request.user.id
)
@@ -828,7 +940,9 @@ class IssueCommentAPIEndpoint(BaseAPIView):
issue_activity.delay(
type="comment.activity.created",
requested_data=json.dumps(serializer.data, cls=DjangoJSONEncoder),
requested_data=json.dumps(
serializer.data, cls=DjangoJSONEncoder
),
actor_id=str(issue_comment.created_by_id),
issue_id=str(self.kwargs.get("issue_id")),
project_id=str(self.kwargs.get("project_id")),
@@ -840,17 +954,24 @@ class IssueCommentAPIEndpoint(BaseAPIView):
def patch(self, request, slug, project_id, issue_id, pk):
issue_comment = IssueComment.objects.get(
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
workspace__slug=slug,
project_id=project_id,
issue_id=issue_id,
pk=pk,
)
requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder)
current_instance = json.dumps(
IssueCommentSerializer(issue_comment).data, cls=DjangoJSONEncoder
IssueCommentSerializer(issue_comment).data,
cls=DjangoJSONEncoder,
)
# Validation check if the issue already exists
if (
request.data.get("external_id")
and (issue_comment.external_id != str(request.data.get("external_id")))
and (
issue_comment.external_id
!= str(request.data.get("external_id"))
)
and IssueComment.objects.filter(
project_id=project_id,
workspace__slug=slug,
@@ -887,10 +1008,14 @@ class IssueCommentAPIEndpoint(BaseAPIView):
def delete(self, request, slug, project_id, issue_id, pk):
issue_comment = IssueComment.objects.get(
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
workspace__slug=slug,
project_id=project_id,
issue_id=issue_id,
pk=pk,
)
current_instance = json.dumps(
IssueCommentSerializer(issue_comment).data, cls=DjangoJSONEncoder
IssueCommentSerializer(issue_comment).data,
cls=DjangoJSONEncoder,
)
issue_comment.delete()
issue_activity.delay(
@@ -906,7 +1031,9 @@ class IssueCommentAPIEndpoint(BaseAPIView):
class IssueActivityAPIEndpoint(BaseAPIView):
permission_classes = [ProjectEntityPermission]
permission_classes = [
ProjectEntityPermission,
]
def get(self, request, slug, project_id, issue_id, pk=None):
issue_activities = (
@@ -931,14 +1058,19 @@ class IssueActivityAPIEndpoint(BaseAPIView):
request=request,
queryset=(issue_activities),
on_results=lambda issue_activity: IssueActivitySerializer(
issue_activity, many=True, fields=self.fields, expand=self.expand
issue_activity,
many=True,
fields=self.fields,
expand=self.expand,
).data,
)
class IssueAttachmentEndpoint(BaseAPIView):
serializer_class = IssueAttachmentSerializer
permission_classes = [ProjectEntityPermission]
permission_classes = [
ProjectEntityPermission,
]
model = FileAsset
parser_classes = (MultiPartParser, FormParser)
@@ -977,7 +1109,10 @@ class IssueAttachmentEndpoint(BaseAPIView):
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("issue_id", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=json.dumps(serializer.data, cls=DjangoJSONEncoder),
current_instance=json.dumps(
serializer.data,
cls=DjangoJSONEncoder,
),
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
+29 -8
View File
@@ -13,14 +13,24 @@ from rest_framework import status
# Module imports
from .base import BaseAPIView
from plane.api.serializers import UserLiteSerializer
from plane.db.models import User, Workspace, Project, WorkspaceMember, ProjectMember
from plane.db.models import (
User,
Workspace,
Project,
WorkspaceMember,
ProjectMember,
)
from plane.app.permissions import ProjectMemberPermission
from plane.app.permissions import (
ProjectMemberPermission,
)
# API endpoint to get and insert users inside the workspace
class ProjectMemberAPIEndpoint(BaseAPIView):
permission_classes = [ProjectMemberPermission]
permission_classes = [
ProjectMemberPermission,
]
# Get all the users that are present inside the workspace
def get(self, request, slug, project_id):
@@ -38,7 +48,10 @@ class ProjectMemberAPIEndpoint(BaseAPIView):
# Get all the users that are present inside the workspace
users = UserLiteSerializer(
User.objects.filter(id__in=project_members), many=True
User.objects.filter(
id__in=project_members,
),
many=True,
).data
return Response(users, status=status.HTTP_200_OK)
@@ -65,7 +78,8 @@ class ProjectMemberAPIEndpoint(BaseAPIView):
validate_email(email)
except ValidationError:
return Response(
{"error": "Invalid email provided"}, status=status.HTTP_400_BAD_REQUEST
{"error": "Invalid email provided"},
status=status.HTTP_400_BAD_REQUEST,
)
workspace = Workspace.objects.filter(slug=slug).first()
@@ -94,7 +108,9 @@ class ProjectMemberAPIEndpoint(BaseAPIView):
).first()
if project_member:
return Response(
{"error": "User is already part of the workspace and project"},
{
"error": "User is already part of the workspace and project"
},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -115,14 +131,18 @@ class ProjectMemberAPIEndpoint(BaseAPIView):
# Create a workspace member for the user if not already a member
if not workspace_member:
workspace_member = WorkspaceMember.objects.create(
workspace=workspace, member=user, role=request.data.get("role", 5)
workspace=workspace,
member=user,
role=request.data.get("role", 5),
)
workspace_member.save()
# Create a project member for the user if not already a member
if not project_member:
project_member = ProjectMember.objects.create(
project=project, member=user, role=request.data.get("role", 5)
project=project,
member=user,
role=request.data.get("role", 5),
)
project_member.save()
@@ -130,3 +150,4 @@ class ProjectMemberAPIEndpoint(BaseAPIView):
user_data = UserLiteSerializer(user).data
return Response(user_data, status=status.HTTP_201_CREATED)
+99 -31
View File
@@ -43,7 +43,9 @@ class ModuleAPIEndpoint(BaseAPIView):
"""
model = Module
permission_classes = [ProjectEntityPermission]
permission_classes = [
ProjectEntityPermission,
]
serializer_class = ModuleSerializer
webhook_event = "module"
@@ -58,7 +60,9 @@ class ModuleAPIEndpoint(BaseAPIView):
.prefetch_related(
Prefetch(
"link_module",
queryset=ModuleLink.objects.select_related("module", "created_by"),
queryset=ModuleLink.objects.select_related(
"module", "created_by"
),
)
)
.annotate(
@@ -70,7 +74,7 @@ class ModuleAPIEndpoint(BaseAPIView):
issue_module__deleted_at__isnull=True,
),
distinct=True,
)
),
)
.annotate(
completed_issues=Count(
@@ -139,7 +143,10 @@ class ModuleAPIEndpoint(BaseAPIView):
project = Project.objects.get(pk=project_id, workspace__slug=slug)
serializer = ModuleSerializer(
data=request.data,
context={"project_id": project_id, "workspace_id": project.workspace_id},
context={
"project_id": project_id,
"workspace_id": project.workspace_id,
},
)
if serializer.is_valid():
if (
@@ -182,7 +189,9 @@ class ModuleAPIEndpoint(BaseAPIView):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def patch(self, request, slug, project_id, pk):
module = Module.objects.get(pk=pk, project_id=project_id, workspace__slug=slug)
module = Module.objects.get(
pk=pk, project_id=project_id, workspace__slug=slug
)
current_instance = json.dumps(
ModuleSerializer(module).data, cls=DjangoJSONEncoder
@@ -194,7 +203,10 @@ class ModuleAPIEndpoint(BaseAPIView):
status=status.HTTP_400_BAD_REQUEST,
)
serializer = ModuleSerializer(
module, data=request.data, context={"project_id": project_id}, partial=True
module,
data=request.data,
context={"project_id": project_id},
partial=True,
)
if serializer.is_valid():
if (
@@ -234,21 +246,33 @@ class ModuleAPIEndpoint(BaseAPIView):
def get(self, request, slug, project_id, pk=None):
if pk:
queryset = self.get_queryset().filter(archived_at__isnull=True).get(pk=pk)
queryset = (
self.get_queryset().filter(archived_at__isnull=True).get(pk=pk)
)
data = ModuleSerializer(
queryset, fields=self.fields, expand=self.expand
queryset,
fields=self.fields,
expand=self.expand,
).data
return Response(data, status=status.HTTP_200_OK)
return Response(
data,
status=status.HTTP_200_OK,
)
return self.paginate(
request=request,
queryset=(self.get_queryset().filter(archived_at__isnull=True)),
on_results=lambda modules: ModuleSerializer(
modules, many=True, fields=self.fields, expand=self.expand
modules,
many=True,
fields=self.fields,
expand=self.expand,
).data,
)
def delete(self, request, slug, project_id, pk):
module = Module.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
module = Module.objects.get(
workspace__slug=slug, project_id=project_id, pk=pk
)
if module.created_by_id != request.user.id and (
not ProjectMember.objects.filter(
workspace__slug=slug,
@@ -264,7 +288,9 @@ class ModuleAPIEndpoint(BaseAPIView):
)
module_issues = list(
ModuleIssue.objects.filter(module_id=pk).values_list("issue", flat=True)
ModuleIssue.objects.filter(module_id=pk).values_list(
"issue", flat=True
)
)
issue_activity.delay(
type="module.activity.deleted",
@@ -278,15 +304,24 @@ class ModuleAPIEndpoint(BaseAPIView):
actor_id=str(request.user.id),
issue_id=None,
project_id=str(project_id),
current_instance=json.dumps({"module_name": str(module.name)}),
current_instance=json.dumps(
{
"module_name": str(module.name),
}
),
epoch=int(timezone.now().timestamp()),
)
module.delete()
# Delete the module issues
ModuleIssue.objects.filter(module=pk, project_id=project_id).delete()
ModuleIssue.objects.filter(
module=pk,
project_id=project_id,
).delete()
# Delete the user favorite module
UserFavorite.objects.filter(
entity_type="module", entity_identifier=pk, project_id=project_id
entity_type="module",
entity_identifier=pk,
project_id=project_id,
).delete()
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -303,12 +338,16 @@ class ModuleIssueAPIEndpoint(BaseAPIView):
webhook_event = "module_issue"
bulk = True
permission_classes = [ProjectEntityPermission]
permission_classes = [
ProjectEntityPermission,
]
def get_queryset(self):
return (
ModuleIssue.objects.annotate(
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("issue"))
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("issue")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
@@ -335,10 +374,13 @@ class ModuleIssueAPIEndpoint(BaseAPIView):
order_by = request.GET.get("order_by", "created_at")
issues = (
Issue.issue_objects.filter(
issue_module__module_id=module_id, issue_module__deleted_at__isnull=True
issue_module__module_id=module_id,
issue_module__deleted_at__isnull=True,
)
.annotate(
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
@@ -373,7 +415,10 @@ class ModuleIssueAPIEndpoint(BaseAPIView):
request=request,
queryset=(issues),
on_results=lambda issues: IssueSerializer(
issues, many=True, fields=self.fields, expand=self.expand
issues,
many=True,
fields=self.fields,
expand=self.expand,
).data,
)
@@ -381,7 +426,8 @@ class ModuleIssueAPIEndpoint(BaseAPIView):
issues = request.data.get("issues", [])
if not len(issues):
return Response(
{"error": "Issues are required"}, status=status.HTTP_400_BAD_REQUEST
{"error": "Issues are required"},
status=status.HTTP_400_BAD_REQUEST,
)
module = Module.objects.get(
workspace__slug=slug, project_id=project_id, pk=module_id
@@ -428,10 +474,16 @@ class ModuleIssueAPIEndpoint(BaseAPIView):
)
ModuleIssue.objects.bulk_create(
record_to_create, batch_size=10, ignore_conflicts=True
record_to_create,
batch_size=10,
ignore_conflicts=True,
)
ModuleIssue.objects.bulk_update(records_to_update, ["module"], batch_size=10)
ModuleIssue.objects.bulk_update(
records_to_update,
["module"],
batch_size=10,
)
# Capture Issue Activity
issue_activity.delay(
@@ -467,7 +519,10 @@ class ModuleIssueAPIEndpoint(BaseAPIView):
issue_activity.delay(
type="module.activity.deleted",
requested_data=json.dumps(
{"module_id": str(module_id), "issues": [str(module_issue.issue_id)]}
{
"module_id": str(module_id),
"issues": [str(module_issue.issue_id)],
}
),
actor_id=str(request.user.id),
issue_id=str(issue_id),
@@ -479,7 +534,9 @@ class ModuleIssueAPIEndpoint(BaseAPIView):
class ModuleArchiveUnarchiveAPIEndpoint(BaseAPIView):
permission_classes = [ProjectEntityPermission]
permission_classes = [
ProjectEntityPermission,
]
def get_queryset(self):
return (
@@ -493,7 +550,9 @@ class ModuleArchiveUnarchiveAPIEndpoint(BaseAPIView):
.prefetch_related(
Prefetch(
"link_module",
queryset=ModuleLink.objects.select_related("module", "created_by"),
queryset=ModuleLink.objects.select_related(
"module", "created_by"
),
)
)
.annotate(
@@ -505,7 +564,7 @@ class ModuleArchiveUnarchiveAPIEndpoint(BaseAPIView):
issue_module__deleted_at__isnull=True,
),
distinct=True,
)
),
)
.annotate(
completed_issues=Count(
@@ -575,15 +634,22 @@ class ModuleArchiveUnarchiveAPIEndpoint(BaseAPIView):
request=request,
queryset=(self.get_queryset()),
on_results=lambda modules: ModuleSerializer(
modules, many=True, fields=self.fields, expand=self.expand
modules,
many=True,
fields=self.fields,
expand=self.expand,
).data,
)
def post(self, request, slug, project_id, pk):
module = Module.objects.get(pk=pk, project_id=project_id, workspace__slug=slug)
module = Module.objects.get(
pk=pk, project_id=project_id, workspace__slug=slug
)
if module.status not in ["completed", "cancelled"]:
return Response(
{"error": "Only completed or cancelled modules can be archived"},
{
"error": "Only completed or cancelled modules can be archived"
},
status=status.HTTP_400_BAD_REQUEST,
)
module.archived_at = timezone.now()
@@ -597,7 +663,9 @@ class ModuleArchiveUnarchiveAPIEndpoint(BaseAPIView):
return Response(status=status.HTTP_204_NO_CONTENT)
def delete(self, request, slug, project_id, pk):
module = Module.objects.get(pk=pk, project_id=project_id, workspace__slug=slug)
module = Module.objects.get(
pk=pk, project_id=project_id, workspace__slug=slug
)
module.archived_at = None
module.save()
return Response(status=status.HTTP_204_NO_CONTENT)
+70 -21
View File
@@ -39,7 +39,9 @@ class ProjectAPIEndpoint(BaseAPIView):
model = Project
webhook_event = "project"
permission_classes = [ProjectBasePermission]
permission_classes = [
ProjectBasePermission,
]
def get_queryset(self):
return (
@@ -52,7 +54,10 @@ class ProjectAPIEndpoint(BaseAPIView):
| Q(network=2)
)
.select_related(
"workspace", "workspace__owner", "default_assignee", "project_lead"
"workspace",
"workspace__owner",
"default_assignee",
"project_lead",
)
.annotate(
is_member=Exists(
@@ -66,7 +71,9 @@ class ProjectAPIEndpoint(BaseAPIView):
)
.annotate(
total_members=ProjectMember.objects.filter(
project_id=OuterRef("id"), member__is_bot=False, is_active=True
project_id=OuterRef("id"),
member__is_bot=False,
is_active=True,
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
@@ -118,7 +125,8 @@ class ProjectAPIEndpoint(BaseAPIView):
Prefetch(
"project_projectmember",
queryset=ProjectMember.objects.filter(
workspace__slug=slug, is_active=True
workspace__slug=slug,
is_active=True,
).select_related("member"),
)
)
@@ -128,11 +136,18 @@ class ProjectAPIEndpoint(BaseAPIView):
request=request,
queryset=(projects),
on_results=lambda projects: ProjectSerializer(
projects, many=True, fields=self.fields, expand=self.expand
projects,
many=True,
fields=self.fields,
expand=self.expand,
).data,
)
project = self.get_queryset().get(workspace__slug=slug, pk=pk)
serializer = ProjectSerializer(project, fields=self.fields, expand=self.expand)
serializer = ProjectSerializer(
project,
fields=self.fields,
expand=self.expand,
)
return Response(serializer.data, status=status.HTTP_200_OK)
def post(self, request, slug):
@@ -146,11 +161,14 @@ class ProjectAPIEndpoint(BaseAPIView):
# Add the user as Administrator to the project
_ = ProjectMember.objects.create(
project_id=serializer.data["id"], member=request.user, role=20
project_id=serializer.data["id"],
member=request.user,
role=20,
)
# Also create the issue property for the user
_ = IssueUserProperty.objects.create(
project_id=serializer.data["id"], user=request.user
project_id=serializer.data["id"],
user=request.user,
)
if serializer.data["project_lead"] is not None and str(
@@ -218,7 +236,11 @@ class ProjectAPIEndpoint(BaseAPIView):
]
)
project = self.get_queryset().filter(pk=serializer.data["id"]).first()
project = (
self.get_queryset()
.filter(pk=serializer.data["id"])
.first()
)
# Model activity
model_activity.delay(
@@ -232,8 +254,13 @@ class ProjectAPIEndpoint(BaseAPIView):
)
serializer = ProjectSerializer(project)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
return Response(
serializer.data, status=status.HTTP_201_CREATED
)
return Response(
serializer.errors,
status=status.HTTP_400_BAD_REQUEST,
)
except IntegrityError as e:
if "already exists" in str(e):
return Response(
@@ -242,7 +269,8 @@ class ProjectAPIEndpoint(BaseAPIView):
)
except Workspace.DoesNotExist:
return Response(
{"error": "Workspace does not exist"}, status=status.HTTP_404_NOT_FOUND
{"error": "Workspace does not exist"},
status=status.HTTP_404_NOT_FOUND,
)
except ValidationError:
return Response(
@@ -258,7 +286,9 @@ class ProjectAPIEndpoint(BaseAPIView):
ProjectSerializer(project).data, cls=DjangoJSONEncoder
)
intake_view = request.data.get("inbox_view", project.intake_view)
intake_view = request.data.get(
"inbox_view", request.data.get("intake_view", False)
)
if project.archived_at:
return Response(
@@ -268,7 +298,10 @@ class ProjectAPIEndpoint(BaseAPIView):
serializer = ProjectSerializer(
project,
data={**request.data, "intake_view": intake_view},
data={
**request.data,
"intake_view": intake_view,
},
context={"workspace_id": workspace.id},
partial=True,
)
@@ -277,7 +310,8 @@ class ProjectAPIEndpoint(BaseAPIView):
serializer.save()
if serializer.data["intake_view"]:
intake = Intake.objects.filter(
project=project, is_default=True
project=project,
is_default=True,
).first()
if not intake:
Intake.objects.create(
@@ -296,7 +330,11 @@ class ProjectAPIEndpoint(BaseAPIView):
is_triage=True,
)
project = self.get_queryset().filter(pk=serializer.data["id"]).first()
project = (
self.get_queryset()
.filter(pk=serializer.data["id"])
.first()
)
model_activity.delay(
model_name="project",
@@ -310,7 +348,9 @@ class ProjectAPIEndpoint(BaseAPIView):
serializer = ProjectSerializer(project)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
return Response(
serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
except IntegrityError as e:
if "already exists" in str(e):
return Response(
@@ -319,7 +359,8 @@ class ProjectAPIEndpoint(BaseAPIView):
)
except (Project.DoesNotExist, Workspace.DoesNotExist):
return Response(
{"error": "Project does not exist"}, status=status.HTTP_404_NOT_FOUND
{"error": "Project does not exist"},
status=status.HTTP_404_NOT_FOUND,
)
except ValidationError:
return Response(
@@ -331,20 +372,28 @@ class ProjectAPIEndpoint(BaseAPIView):
project = Project.objects.get(pk=pk, workspace__slug=slug)
# Delete the user favorite cycle
UserFavorite.objects.filter(
entity_type="project", entity_identifier=pk, project_id=pk
entity_type="project",
entity_identifier=pk,
project_id=pk,
).delete()
project.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
class ProjectArchiveUnarchiveAPIEndpoint(BaseAPIView):
permission_classes = [ProjectBasePermission]
permission_classes = [
ProjectBasePermission,
]
def post(self, request, slug, project_id):
project = Project.objects.get(pk=project_id, workspace__slug=slug)
project.archived_at = timezone.now()
project.save()
UserFavorite.objects.filter(workspace__slug=slug, project=project_id).delete()
UserFavorite.objects.filter(
workspace__slug=slug,
project=project_id,
).delete()
return Response(status=status.HTTP_204_NO_CONTENT)
def delete(self, request, slug, project_id):
+17 -5
View File
@@ -16,7 +16,9 @@ from .base import BaseAPIView
class StateAPIEndpoint(BaseAPIView):
serializer_class = StateSerializer
model = State
permission_classes = [ProjectEntityPermission]
permission_classes = [
ProjectEntityPermission,
]
def get_queryset(self):
return (
@@ -65,7 +67,9 @@ class StateAPIEndpoint(BaseAPIView):
serializer.save(project_id=project_id)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
return Response(
serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
except IntegrityError:
state = State.objects.filter(
workspace__slug=slug,
@@ -92,13 +96,19 @@ class StateAPIEndpoint(BaseAPIView):
request=request,
queryset=(self.get_queryset()),
on_results=lambda states: StateSerializer(
states, many=True, fields=self.fields, expand=self.expand
states,
many=True,
fields=self.fields,
expand=self.expand,
).data,
)
def delete(self, request, slug, project_id, state_id):
state = State.objects.get(
is_triage=False, pk=state_id, project_id=project_id, workspace__slug=slug
is_triage=False,
pk=state_id,
project_id=project_id,
workspace__slug=slug,
)
if state.default:
@@ -112,7 +122,9 @@ class StateAPIEndpoint(BaseAPIView):
if issue_exist:
return Response(
{"error": "The state is not empty, only empty states can be deleted"},
{
"error": "The state is not empty, only empty states can be deleted"
},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -25,7 +25,10 @@ class APIKeyAuthentication(authentication.BaseAuthentication):
def validate_api_token(self, token):
try:
api_token = APIToken.objects.get(
Q(Q(expired_at__gt=timezone.now()) | Q(expired_at__isnull=True)),
Q(
Q(expired_at__gt=timezone.now())
| Q(expired_at__isnull=True)
),
token=token,
is_active=True,
)
+1 -1
View File
@@ -12,4 +12,4 @@ from .project import (
ProjectMemberPermission,
ProjectLitePermission,
)
from .base import allow_permission, ROLE
from .base import allow_permission, ROLE
+3 -2
View File
@@ -5,7 +5,6 @@ from rest_framework import status
from enum import Enum
class ROLE(Enum):
ADMIN = 20
MEMBER = 15
@@ -16,6 +15,7 @@ def allow_permission(allowed_roles, level="PROJECT", creator=False, model=None):
def decorator(view_func):
@wraps(view_func)
def _wrapped_view(instance, request, *args, **kwargs):
# Check for creator if required
if creator and model:
obj = model.objects.filter(
@@ -26,7 +26,8 @@ def allow_permission(allowed_roles, level="PROJECT", creator=False, model=None):
# Convert allowed_roles to their values if they are enum members
allowed_role_values = [
role.value if isinstance(role, ROLE) else role for role in allowed_roles
role.value if isinstance(role, ROLE) else role
for role in allowed_roles
]
# Check role permissions
+6 -2
View File
@@ -18,7 +18,9 @@ class ProjectBasePermission(BasePermission):
## Safe Methods -> Handle the filtering logic in queryset
if request.method in SAFE_METHODS:
return WorkspaceMember.objects.filter(
workspace__slug=view.workspace_slug, member=request.user, is_active=True
workspace__slug=view.workspace_slug,
member=request.user,
is_active=True,
).exists()
## Only workspace owners or admins can create the projects
@@ -48,7 +50,9 @@ class ProjectMemberPermission(BasePermission):
## Safe Methods -> Handle the filtering logic in queryset
if request.method in SAFE_METHODS:
return ProjectMember.objects.filter(
workspace__slug=view.workspace_slug, member=request.user, is_active=True
workspace__slug=view.workspace_slug,
member=request.user,
is_active=True,
).exists()
## Only workspace owners or admins can create the projects
if request.method == "POST":
+12 -4
View File
@@ -50,7 +50,9 @@ class WorkspaceOwnerPermission(BasePermission):
return False
return WorkspaceMember.objects.filter(
workspace__slug=view.workspace_slug, member=request.user, role=Admin
workspace__slug=view.workspace_slug,
member=request.user,
role=Admin,
).exists()
@@ -75,7 +77,9 @@ class WorkspaceEntityPermission(BasePermission):
## Safe Methods -> Handle the filtering logic in queryset
if request.method in SAFE_METHODS:
return WorkspaceMember.objects.filter(
workspace__slug=view.workspace_slug, member=request.user, is_active=True
workspace__slug=view.workspace_slug,
member=request.user,
is_active=True,
).exists()
return WorkspaceMember.objects.filter(
@@ -92,7 +96,9 @@ class WorkspaceViewerPermission(BasePermission):
return False
return WorkspaceMember.objects.filter(
member=request.user, workspace__slug=view.workspace_slug, is_active=True
member=request.user,
workspace__slug=view.workspace_slug,
is_active=True,
).exists()
@@ -102,5 +108,7 @@ class WorkspaceUserPermission(BasePermission):
return False
return WorkspaceMember.objects.filter(
member=request.user, workspace__slug=view.workspace_slug, is_active=True
member=request.user,
workspace__slug=view.workspace_slug,
is_active=True,
).exists()
+8 -2
View File
@@ -13,6 +13,7 @@ from .user import (
from .workspace import (
WorkSpaceSerializer,
WorkSpaceMemberSerializer,
TeamSerializer,
WorkSpaceMemberInviteSerializer,
WorkspaceLiteSerializer,
WorkspaceThemeSerializer,
@@ -35,7 +36,9 @@ from .project import (
ProjectMemberRoleSerializer,
)
from .state import StateSerializer, StateLiteSerializer
from .view import IssueViewSerializer
from .view import (
IssueViewSerializer,
)
from .cycle import (
CycleSerializer,
CycleIssueSerializer,
@@ -109,7 +112,10 @@ from .intake import (
from .analytic import AnalyticViewSerializer
from .notification import NotificationSerializer, UserNotificationPreferenceSerializer
from .notification import (
NotificationSerializer,
UserNotificationPreferenceSerializer,
)
from .exporter import ExporterHistorySerializer
+4 -1
View File
@@ -7,7 +7,10 @@ class AnalyticViewSerializer(BaseSerializer):
class Meta:
model = AnalyticView
fields = "__all__"
read_only_fields = ["workspace", "query"]
read_only_fields = [
"workspace",
"query",
]
def create(self, validated_data):
query_params = validated_data.get("query_dict", {})
+6 -1
View File
@@ -6,4 +6,9 @@ class FileAssetSerializer(BaseSerializer):
class Meta:
model = FileAsset
fields = "__all__"
read_only_fields = ["created_by", "updated_by", "created_at", "updated_at"]
read_only_fields = [
"created_by",
"updated_by",
"created_at",
"updated_at",
]
+12 -5
View File
@@ -178,10 +178,15 @@ class DynamicBaseSerializer(BaseSerializer):
response[expand] = exp_serializer.data
else:
# You might need to handle this case differently
response[expand] = getattr(instance, f"{expand}_id", None)
response[expand] = getattr(
instance, f"{expand}_id", None
)
# Check if issue_attachments is in fields or expand
if "issue_attachments" in self.fields or "issue_attachments" in self.expand:
if (
"issue_attachments" in self.fields
or "issue_attachments" in self.expand
):
# Import the model here to avoid circular imports
from plane.db.models import FileAsset
@@ -194,9 +199,11 @@ class DynamicBaseSerializer(BaseSerializer):
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
)
# Serialize issue_attachments and add them to the response
response["issue_attachments"] = IssueAttachmentLiteSerializer(
issue_attachments, many=True
).data
response["issue_attachments"] = (
IssueAttachmentLiteSerializer(
issue_attachments, many=True
).data
)
else:
response["issue_attachments"] = []
+23 -17
View File
@@ -4,8 +4,11 @@ from rest_framework import serializers
# Module imports
from .base import BaseSerializer
from .issue import IssueStateSerializer
from plane.db.models import Cycle, CycleIssue, CycleUserProperties
from plane.utils.timezone_converter import convert_to_utc
from plane.db.models import (
Cycle,
CycleIssue,
CycleUserProperties,
)
class CycleWriteSerializer(BaseSerializer):
@@ -15,24 +18,20 @@ class CycleWriteSerializer(BaseSerializer):
and data.get("end_date", None) is not None
and data.get("start_date", None) > data.get("end_date", None)
):
raise serializers.ValidationError("Start date cannot exceed end date")
if (
data.get("start_date", None) is not None
and data.get("end_date", None) is not None
):
project_id = self.initial_data.get("project_id") or self.instance.project_id
data["start_date"] = convert_to_utc(
str(data.get("start_date").date()), project_id, is_start_date=True
)
data["end_date"] = convert_to_utc(
str(data.get("end_date", None).date()), project_id
raise serializers.ValidationError(
"Start date cannot exceed end date"
)
return data
class Meta:
model = Cycle
fields = "__all__"
read_only_fields = ["workspace", "project", "owned_by", "archived_at"]
read_only_fields = [
"workspace",
"project",
"owned_by",
"archived_at",
]
class CycleSerializer(BaseSerializer):
@@ -88,11 +87,18 @@ class CycleIssueSerializer(BaseSerializer):
class Meta:
model = CycleIssue
fields = "__all__"
read_only_fields = ["workspace", "project", "cycle"]
read_only_fields = [
"workspace",
"project",
"cycle",
]
class CycleUserPropertiesSerializer(BaseSerializer):
class Meta:
model = CycleUserProperties
fields = "__all__"
read_only_fields = ["workspace", "project", "cycle" "user"]
read_only_fields = [
"workspace",
"project",
"cycle" "user",
]
+29 -8
View File
@@ -22,10 +22,16 @@ from plane.db.models import (
class DraftIssueCreateSerializer(BaseSerializer):
# ids
state_id = serializers.PrimaryKeyRelatedField(
source="state", queryset=State.objects.all(), required=False, allow_null=True
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
source="parent",
queryset=Issue.objects.all(),
required=False,
allow_null=True,
)
label_ids = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()),
@@ -63,7 +69,9 @@ class DraftIssueCreateSerializer(BaseSerializer):
and data.get("target_date", None) is not None
and data.get("start_date", None) > data.get("target_date", None)
):
raise serializers.ValidationError("Start date cannot exceed target date")
raise serializers.ValidationError(
"Start date cannot exceed target date"
)
return data
def create(self, validated_data):
@@ -78,7 +86,9 @@ class DraftIssueCreateSerializer(BaseSerializer):
# Create Issue
issue = DraftIssue.objects.create(
**validated_data, workspace_id=workspace_id, project_id=project_id
**validated_data,
workspace_id=workspace_id,
project_id=project_id,
)
# Issue Audit Users
@@ -229,11 +239,20 @@ class DraftIssueCreateSerializer(BaseSerializer):
class DraftIssueSerializer(BaseSerializer):
# ids
cycle_id = serializers.PrimaryKeyRelatedField(read_only=True)
module_ids = serializers.ListField(child=serializers.UUIDField(), required=False)
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)
label_ids = serializers.ListField(
child=serializers.UUIDField(),
required=False,
)
assignee_ids = serializers.ListField(
child=serializers.UUIDField(),
required=False,
)
class Meta:
model = DraftIssue
@@ -267,5 +286,7 @@ class DraftIssueDetailSerializer(DraftIssueSerializer):
description_html = serializers.CharField()
class Meta(DraftIssueSerializer.Meta):
fields = DraftIssueSerializer.Meta.fields + ["description_html"]
fields = DraftIssueSerializer.Meta.fields + [
"description_html",
]
read_only_fields = fields
+23 -5
View File
@@ -7,10 +7,14 @@ from rest_framework import serializers
class EstimateSerializer(BaseSerializer):
class Meta:
model = Estimate
fields = "__all__"
read_only_fields = ["workspace", "project"]
read_only_fields = [
"workspace",
"project",
]
class EstimatePointSerializer(BaseSerializer):
@@ -19,13 +23,19 @@ class EstimatePointSerializer(BaseSerializer):
raise serializers.ValidationError("Estimate points are required")
value = data.get("value")
if value and len(value) > 20:
raise serializers.ValidationError("Value can't be more than 20 characters")
raise serializers.ValidationError(
"Value can't be more than 20 characters"
)
return data
class Meta:
model = EstimatePoint
fields = "__all__"
read_only_fields = ["estimate", "workspace", "project"]
read_only_fields = [
"estimate",
"workspace",
"project",
]
class EstimateReadSerializer(BaseSerializer):
@@ -34,7 +44,11 @@ class EstimateReadSerializer(BaseSerializer):
class Meta:
model = Estimate
fields = "__all__"
read_only_fields = ["points", "name", "description"]
read_only_fields = [
"points",
"name",
"description",
]
class WorkspaceEstimateSerializer(BaseSerializer):
@@ -43,4 +57,8 @@ class WorkspaceEstimateSerializer(BaseSerializer):
class Meta:
model = Estimate
fields = "__all__"
read_only_fields = ["points", "name", "description"]
read_only_fields = [
"points",
"name",
"description",
]
+3 -1
View File
@@ -5,7 +5,9 @@ from .user import UserLiteSerializer
class ExporterHistorySerializer(BaseSerializer):
initiated_by_detail = UserLiteSerializer(source="initiated_by", read_only=True)
initiated_by_detail = UserLiteSerializer(
source="initiated_by", read_only=True
)
class Meta:
model = ExporterHistory
+16 -2
View File
@@ -1,9 +1,18 @@
from rest_framework import serializers
from plane.db.models import UserFavorite, Cycle, Module, Issue, IssueView, Page, Project
from plane.db.models import (
UserFavorite,
Cycle,
Module,
Issue,
IssueView,
Page,
Project,
)
class ProjectFavoriteLiteSerializer(serializers.ModelSerializer):
class Meta:
model = Project
fields = ["id", "name", "logo_props"]
@@ -24,18 +33,21 @@ class PageFavoriteLiteSerializer(serializers.ModelSerializer):
class CycleFavoriteLiteSerializer(serializers.ModelSerializer):
class Meta:
model = Cycle
fields = ["id", "name", "logo_props", "project_id"]
class ModuleFavoriteLiteSerializer(serializers.ModelSerializer):
class Meta:
model = Module
fields = ["id", "name", "logo_props", "project_id"]
class ViewFavoriteSerializer(serializers.ModelSerializer):
class Meta:
model = IssueView
fields = ["id", "name", "logo_props", "project_id"]
@@ -77,7 +89,9 @@ class UserFavoriteSerializer(serializers.ModelSerializer):
entity_type = obj.entity_type
entity_identifier = obj.entity_identifier
entity_model, entity_serializer = get_entity_model_and_serializer(entity_type)
entity_model, entity_serializer = get_entity_model_and_serializer(
entity_type
)
if entity_model and entity_serializer:
try:
entity = entity_model.objects.get(pk=entity_identifier)
+6 -2
View File
@@ -7,9 +7,13 @@ from plane.db.models import Importer
class ImporterSerializer(BaseSerializer):
initiated_by_detail = UserLiteSerializer(source="initiated_by", read_only=True)
initiated_by_detail = UserLiteSerializer(
source="initiated_by", read_only=True
)
project_detail = ProjectLiteSerializer(source="project", read_only=True)
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
workspace_detail = WorkspaceLiteSerializer(
source="workspace", read_only=True
)
class Meta:
model = Importer
+23 -6
View File
@@ -3,7 +3,11 @@ from rest_framework import serializers
# Module imports
from .base import BaseSerializer
from .issue import IssueIntakeSerializer, LabelLiteSerializer, IssueDetailSerializer
from .issue import (
IssueIntakeSerializer,
LabelLiteSerializer,
IssueDetailSerializer,
)
from .project import ProjectLiteSerializer
from .state import StateLiteSerializer
from .user import UserLiteSerializer
@@ -17,7 +21,10 @@ class IntakeSerializer(BaseSerializer):
class Meta:
model = Intake
fields = "__all__"
read_only_fields = ["project", "workspace"]
read_only_fields = [
"project",
"workspace",
]
class IntakeIssueSerializer(BaseSerializer):
@@ -34,7 +41,10 @@ class IntakeIssueSerializer(BaseSerializer):
"issue",
"created_by",
]
read_only_fields = ["project", "workspace"]
read_only_fields = [
"project",
"workspace",
]
def to_representation(self, instance):
# Pass the annotated fields to the Issue instance if they exist
@@ -60,7 +70,10 @@ class IntakeIssueDetailSerializer(BaseSerializer):
"source",
"issue",
]
read_only_fields = ["project", "workspace"]
read_only_fields = [
"project",
"workspace",
]
def to_representation(self, instance):
# Pass the annotated fields to the Issue instance if they exist
@@ -82,8 +95,12 @@ class IntakeIssueLiteSerializer(BaseSerializer):
class IssueStateIntakeSerializer(BaseSerializer):
state_detail = StateLiteSerializer(read_only=True, source="state")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
label_details = LabelLiteSerializer(read_only=True, source="labels", many=True)
assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
label_details = LabelLiteSerializer(
read_only=True, source="labels", many=True
)
assignee_details = UserLiteSerializer(
read_only=True, source="assignees", many=True
)
sub_issues_count = serializers.IntegerField(read_only=True)
issue_intake = IntakeIssueLiteSerializer(read_only=True, many=True)
+146 -32
View File
@@ -60,7 +60,12 @@ class IssueProjectLiteSerializer(BaseSerializer):
class Meta:
model = Issue
fields = ["id", "project_detail", "name", "sequence_id"]
fields = [
"id",
"project_detail",
"name",
"sequence_id",
]
read_only_fields = fields
@@ -69,10 +74,16 @@ class IssueProjectLiteSerializer(BaseSerializer):
class IssueCreateSerializer(BaseSerializer):
# ids
state_id = serializers.PrimaryKeyRelatedField(
source="state", queryset=State.objects.all(), required=False, allow_null=True
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
source="parent",
queryset=Issue.objects.all(),
required=False,
allow_null=True,
)
label_ids = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()),
@@ -113,7 +124,9 @@ class IssueCreateSerializer(BaseSerializer):
and data.get("target_date", None) is not None
and data.get("start_date", None) > data.get("target_date", None)
):
raise serializers.ValidationError("Start date cannot exceed target date")
raise serializers.ValidationError(
"Start date cannot exceed target date"
)
return data
def create(self, validated_data):
@@ -125,7 +138,10 @@ class IssueCreateSerializer(BaseSerializer):
default_assignee_id = self.context["default_assignee_id"]
# Create Issue
issue = Issue.objects.create(**validated_data, project_id=project_id)
issue = Issue.objects.create(
**validated_data,
project_id=project_id,
)
# Issue Audit Users
created_by_id = issue.created_by_id
@@ -229,7 +245,9 @@ class IssueActivitySerializer(BaseSerializer):
actor_detail = UserLiteSerializer(read_only=True, source="actor")
issue_detail = IssueFlatSerializer(read_only=True, source="issue")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
workspace_detail = WorkspaceLiteSerializer(
read_only=True, source="workspace"
)
class Meta:
model = IssueActivity
@@ -240,7 +258,11 @@ class IssueUserPropertySerializer(BaseSerializer):
class Meta:
model = IssueUserProperty
fields = "__all__"
read_only_fields = ["user", "workspace", "project"]
read_only_fields = [
"user",
"workspace",
"project",
]
class LabelSerializer(BaseSerializer):
@@ -255,20 +277,30 @@ class LabelSerializer(BaseSerializer):
"workspace_id",
"sort_order",
]
read_only_fields = ["workspace", "project"]
read_only_fields = [
"workspace",
"project",
]
class LabelLiteSerializer(BaseSerializer):
class Meta:
model = Label
fields = ["id", "name", "color"]
fields = [
"id",
"name",
"color",
]
class IssueLabelSerializer(BaseSerializer):
class Meta:
model = IssueLabel
fields = "__all__"
read_only_fields = ["workspace", "project"]
read_only_fields = [
"workspace",
"project",
]
class IssueRelationSerializer(BaseSerializer):
@@ -284,8 +316,17 @@ class IssueRelationSerializer(BaseSerializer):
class Meta:
model = IssueRelation
fields = ["id", "project_id", "sequence_id", "relation_type", "name"]
read_only_fields = ["workspace", "project"]
fields = [
"id",
"project_id",
"sequence_id",
"relation_type",
"name",
]
read_only_fields = [
"workspace",
"project",
]
class RelatedIssueSerializer(BaseSerializer):
@@ -293,14 +334,25 @@ class RelatedIssueSerializer(BaseSerializer):
project_id = serializers.PrimaryKeyRelatedField(
source="issue.project_id", read_only=True
)
sequence_id = serializers.IntegerField(source="issue.sequence_id", read_only=True)
sequence_id = serializers.IntegerField(
source="issue.sequence_id", read_only=True
)
name = serializers.CharField(source="issue.name", read_only=True)
relation_type = serializers.CharField(read_only=True)
class Meta:
model = IssueRelation
fields = ["id", "project_id", "sequence_id", "relation_type", "name"]
read_only_fields = ["workspace", "project"]
fields = [
"id",
"project_id",
"sequence_id",
"relation_type",
"name",
]
read_only_fields = [
"workspace",
"project",
]
class IssueAssigneeSerializer(BaseSerializer):
@@ -408,7 +460,8 @@ class IssueLinkSerializer(BaseSerializer):
# Validation if url already exists
def create(self, validated_data):
if IssueLink.objects.filter(
url=validated_data.get("url"), issue_id=validated_data.get("issue_id")
url=validated_data.get("url"),
issue_id=validated_data.get("issue_id"),
).exists():
raise serializers.ValidationError(
{"error": "URL already exists for this Issue"}
@@ -418,7 +471,8 @@ class IssueLinkSerializer(BaseSerializer):
def update(self, instance, validated_data):
if (
IssueLink.objects.filter(
url=validated_data.get("url"), issue_id=instance.issue_id
url=validated_data.get("url"),
issue_id=instance.issue_id,
)
.exclude(pk=instance.id)
.exists()
@@ -446,6 +500,7 @@ class IssueLinkLiteSerializer(BaseSerializer):
class IssueAttachmentSerializer(BaseSerializer):
asset_url = serializers.CharField(read_only=True)
class Meta:
@@ -483,20 +538,37 @@ class IssueReactionSerializer(BaseSerializer):
class Meta:
model = IssueReaction
fields = "__all__"
read_only_fields = ["workspace", "project", "issue", "actor", "deleted_at"]
read_only_fields = [
"workspace",
"project",
"issue",
"actor",
"deleted_at",
]
class IssueReactionLiteSerializer(DynamicBaseSerializer):
class Meta:
model = IssueReaction
fields = ["id", "actor", "issue", "reaction"]
fields = [
"id",
"actor",
"issue",
"reaction",
]
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):
@@ -504,7 +576,14 @@ class IssueVoteSerializer(BaseSerializer):
class Meta:
model = IssueVote
fields = ["issue", "vote", "workspace", "project", "actor", "actor_detail"]
fields = [
"issue",
"vote",
"workspace",
"project",
"actor",
"actor_detail",
]
read_only_fields = fields
@@ -512,7 +591,9 @@ class IssueCommentSerializer(BaseSerializer):
actor_detail = UserLiteSerializer(read_only=True, source="actor")
issue_detail = IssueFlatSerializer(read_only=True, source="issue")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
workspace_detail = WorkspaceLiteSerializer(
read_only=True, source="workspace"
)
comment_reactions = CommentReactionSerializer(read_only=True, many=True)
is_member = serializers.BooleanField(read_only=True)
@@ -536,15 +617,25 @@ class IssueStateFlatSerializer(BaseSerializer):
class Meta:
model = Issue
fields = ["id", "sequence_id", "name", "state_detail", "project_detail"]
fields = [
"id",
"sequence_id",
"name",
"state_detail",
"project_detail",
]
# Issue Serializer with state details
class IssueStateSerializer(DynamicBaseSerializer):
label_details = LabelLiteSerializer(read_only=True, source="labels", many=True)
label_details = LabelLiteSerializer(
read_only=True, source="labels", many=True
)
state_detail = StateLiteSerializer(read_only=True, source="state")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
assignee_details = UserLiteSerializer(
read_only=True, source="assignees", many=True
)
sub_issues_count = serializers.IntegerField(read_only=True)
attachment_count = serializers.IntegerField(read_only=True)
link_count = serializers.IntegerField(read_only=True)
@@ -555,7 +646,10 @@ class IssueStateSerializer(DynamicBaseSerializer):
class IssueIntakeSerializer(DynamicBaseSerializer):
label_ids = serializers.ListField(child=serializers.UUIDField(), required=False)
label_ids = serializers.ListField(
child=serializers.UUIDField(),
required=False,
)
class Meta:
model = Issue
@@ -575,11 +669,20 @@ class IssueIntakeSerializer(DynamicBaseSerializer):
class IssueSerializer(DynamicBaseSerializer):
# ids
cycle_id = serializers.PrimaryKeyRelatedField(read_only=True)
module_ids = serializers.ListField(child=serializers.UUIDField(), required=False)
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)
label_ids = serializers.ListField(
child=serializers.UUIDField(),
required=False,
)
assignee_ids = serializers.ListField(
child=serializers.UUIDField(),
required=False,
)
# Count items
sub_issues_count = serializers.IntegerField(read_only=True)
@@ -621,7 +724,11 @@ class IssueSerializer(DynamicBaseSerializer):
class IssueLiteSerializer(DynamicBaseSerializer):
class Meta:
model = Issue
fields = ["id", "sequence_id", "project_id"]
fields = [
"id",
"sequence_id",
"project_id",
]
read_only_fields = fields
@@ -630,7 +737,10 @@ class IssueDetailSerializer(IssueSerializer):
is_subscribed = serializers.BooleanField(read_only=True)
class Meta(IssueSerializer.Meta):
fields = IssueSerializer.Meta.fields + ["description_html", "is_subscribed"]
fields = IssueSerializer.Meta.fields + [
"description_html",
"is_subscribed",
]
read_only_fields = fields
@@ -666,4 +776,8 @@ class IssueSubscriberSerializer(BaseSerializer):
class Meta:
model = IssueSubscriber
fields = "__all__"
read_only_fields = ["workspace", "project", "issue"]
read_only_fields = [
"workspace",
"project",
"issue",
]
+20 -7
View File
@@ -21,7 +21,10 @@ from plane.db.models import (
class ModuleWriteSerializer(BaseSerializer):
lead_id = serializers.PrimaryKeyRelatedField(
source="lead", queryset=User.objects.all(), required=False, allow_null=True
source="lead",
queryset=User.objects.all(),
required=False,
allow_null=True,
)
member_ids = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
@@ -45,7 +48,9 @@ class ModuleWriteSerializer(BaseSerializer):
def to_representation(self, instance):
data = super().to_representation(instance)
data["member_ids"] = [str(member.id) for member in instance.members.all()]
data["member_ids"] = [
str(member.id) for member in instance.members.all()
]
return data
def validate(self, data):
@@ -54,7 +59,9 @@ class ModuleWriteSerializer(BaseSerializer):
and data.get("target_date", None) is not None
and data.get("start_date", None) > data.get("target_date", None)
):
raise serializers.ValidationError("Start date cannot exceed target date")
raise serializers.ValidationError(
"Start date cannot exceed target date"
)
return data
def create(self, validated_data):
@@ -64,7 +71,9 @@ class ModuleWriteSerializer(BaseSerializer):
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():
if Module.objects.filter(
name=module_name, project=project
).exists():
raise serializers.ValidationError(
{"error": "Module with this name already exists"}
)
@@ -95,7 +104,9 @@ class ModuleWriteSerializer(BaseSerializer):
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)
Module.objects.filter(
name=module_name, project=instance.project
)
.exclude(id=instance.id)
.exists()
):
@@ -192,7 +203,8 @@ class ModuleLinkSerializer(BaseSerializer):
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")
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)
@@ -201,7 +213,8 @@ class ModuleLinkSerializer(BaseSerializer):
validated_data["url"] = self.validate_url(validated_data.get("url"))
if (
ModuleLink.objects.filter(
url=validated_data.get("url"), module_id=instance.module_id
url=validated_data.get("url"),
module_id=instance.module_id,
)
.exclude(pk=instance.id)
.exists()
@@ -8,7 +8,9 @@ from rest_framework import serializers
class NotificationSerializer(BaseSerializer):
triggered_by_details = UserLiteSerializer(read_only=True, source="triggered_by")
triggered_by_details = UserLiteSerializer(
read_only=True, source="triggered_by"
)
is_inbox_issue = serializers.BooleanField(read_only=True)
is_intake_issue = serializers.BooleanField(read_only=True)
is_mentioned_notification = serializers.BooleanField(read_only=True)
+31 -8
View File
@@ -22,8 +22,14 @@ class PageSerializer(BaseSerializer):
required=False,
)
# Many to many
label_ids = serializers.ListField(child=serializers.UUIDField(), required=False)
project_ids = serializers.ListField(child=serializers.UUIDField(), required=False)
label_ids = serializers.ListField(
child=serializers.UUIDField(),
required=False,
)
project_ids = serializers.ListField(
child=serializers.UUIDField(),
required=False,
)
class Meta:
model = Page
@@ -48,7 +54,10 @@ class PageSerializer(BaseSerializer):
"label_ids",
"project_ids",
]
read_only_fields = ["workspace", "owned_by"]
read_only_fields = [
"workspace",
"owned_by",
]
def create(self, validated_data):
labels = validated_data.pop("labels", None)
@@ -118,7 +127,9 @@ class PageDetailSerializer(PageSerializer):
description_html = serializers.CharField()
class Meta(PageSerializer.Meta):
fields = PageSerializer.Meta.fields + ["description_html"]
fields = PageSerializer.Meta.fields + [
"description_html",
]
class SubPageSerializer(BaseSerializer):
@@ -127,7 +138,10 @@ class SubPageSerializer(BaseSerializer):
class Meta:
model = PageLog
fields = "__all__"
read_only_fields = ["workspace", "page"]
read_only_fields = [
"workspace",
"page",
]
def get_entity_details(self, obj):
entity_name = obj.entity_name
@@ -144,7 +158,10 @@ class PageLogSerializer(BaseSerializer):
class Meta:
model = PageLog
fields = "__all__"
read_only_fields = ["workspace", "page"]
read_only_fields = [
"workspace",
"page",
]
class PageVersionSerializer(BaseSerializer):
@@ -161,7 +178,10 @@ class PageVersionSerializer(BaseSerializer):
"created_by",
"updated_by",
]
read_only_fields = ["workspace", "page"]
read_only_fields = [
"workspace",
"page",
]
class PageVersionDetailSerializer(BaseSerializer):
@@ -181,4 +201,7 @@ class PageVersionDetailSerializer(BaseSerializer):
"created_by",
"updated_by",
]
read_only_fields = ["workspace", "page"]
read_only_fields = [
"workspace",
"page",
]
+33 -9
View File
@@ -4,7 +4,10 @@ from rest_framework import serializers
# Module imports
from .base import BaseSerializer, DynamicBaseSerializer
from plane.app.serializers.workspace import WorkspaceLiteSerializer
from plane.app.serializers.user import UserLiteSerializer, UserAdminLiteSerializer
from plane.app.serializers.user import (
UserLiteSerializer,
UserAdminLiteSerializer,
)
from plane.db.models import (
Project,
ProjectMember,
@@ -16,23 +19,32 @@ from plane.db.models import (
class ProjectSerializer(BaseSerializer):
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
workspace_detail = WorkspaceLiteSerializer(
source="workspace", read_only=True
)
inbox_view = serializers.BooleanField(read_only=True, source="intake_view")
class Meta:
model = Project
fields = "__all__"
read_only_fields = ["workspace", "deleted_at"]
read_only_fields = [
"workspace",
"deleted_at",
]
def create(self, validated_data):
identifier = validated_data.get("identifier", "").strip().upper()
if identifier == "":
raise serializers.ValidationError(detail="Project Identifier is required")
raise serializers.ValidationError(
detail="Project Identifier is required"
)
if ProjectIdentifier.objects.filter(
name=identifier, workspace_id=self.context["workspace_id"]
).exists():
raise serializers.ValidationError(detail="Project Identifier is taken")
raise serializers.ValidationError(
detail="Project Identifier is taken"
)
project = Project.objects.create(
**validated_data, workspace_id=self.context["workspace_id"]
)
@@ -71,7 +83,9 @@ class ProjectSerializer(BaseSerializer):
return project
# If not same fail update
raise serializers.ValidationError(detail="Project Identifier is already taken")
raise serializers.ValidationError(
detail="Project Identifier is already taken"
)
class ProjectLiteSerializer(BaseSerializer):
@@ -200,16 +214,26 @@ class ProjectMemberLiteSerializer(BaseSerializer):
class DeployBoardSerializer(BaseSerializer):
project_details = ProjectLiteSerializer(read_only=True, source="project")
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
workspace_detail = WorkspaceLiteSerializer(
read_only=True, source="workspace"
)
class Meta:
model = DeployBoard
fields = "__all__"
read_only_fields = ["workspace", "project", "anchor"]
read_only_fields = [
"workspace",
"project",
"anchor",
]
class ProjectPublicMemberSerializer(BaseSerializer):
class Meta:
model = ProjectPublicMember
fields = "__all__"
read_only_fields = ["workspace", "project", "member"]
read_only_fields = [
"workspace",
"project",
"member",
]
+10 -2
View File
@@ -19,11 +19,19 @@ class StateSerializer(BaseSerializer):
"description",
"sequence",
]
read_only_fields = ["workspace", "project"]
read_only_fields = [
"workspace",
"project",
]
class StateLiteSerializer(BaseSerializer):
class Meta:
model = State
fields = ["id", "name", "color", "group"]
fields = [
"id",
"name",
"color",
"group",
]
read_only_fields = fields
+43 -11
View File
@@ -2,7 +2,13 @@
from rest_framework import serializers
# Module import
from plane.db.models import Account, Profile, User, Workspace, WorkspaceMemberInvite
from plane.db.models import (
Account,
Profile,
User,
Workspace,
WorkspaceMemberInvite,
)
from .base import BaseSerializer
@@ -11,7 +17,11 @@ class UserSerializer(BaseSerializer):
class Meta:
model = User
# Exclude password field from the serializer
fields = [field.name for field in User._meta.fields if field.name != "password"]
fields = [
field.name
for field in User._meta.fields
if field.name != "password"
]
# Make all system fields and email read only
read_only_fields = [
"id",
@@ -46,6 +56,7 @@ class UserSerializer(BaseSerializer):
class UserMeSerializer(BaseSerializer):
class Meta:
model = User
fields = [
@@ -76,7 +87,11 @@ class UserMeSettingsSerializer(BaseSerializer):
class Meta:
model = User
fields = ["id", "email", "workspace"]
fields = [
"id",
"email",
"workspace",
]
read_only_fields = fields
def get_workspace(self, obj):
@@ -113,7 +128,8 @@ class UserMeSettingsSerializer(BaseSerializer):
else:
fallback_workspace = (
Workspace.objects.filter(
workspace_member__member_id=obj.id, workspace_member__is_active=True
workspace_member__member_id=obj.id,
workspace_member__is_active=True,
)
.order_by("created_at")
.first()
@@ -122,10 +138,14 @@ class UserMeSettingsSerializer(BaseSerializer):
"last_workspace_id": None,
"last_workspace_slug": None,
"fallback_workspace_id": (
fallback_workspace.id if fallback_workspace is not None else None
fallback_workspace.id
if fallback_workspace is not None
else None
),
"fallback_workspace_slug": (
fallback_workspace.slug if fallback_workspace is not None else None
fallback_workspace.slug
if fallback_workspace is not None
else None
),
"invites": workspace_invites,
}
@@ -143,7 +163,10 @@ class UserLiteSerializer(BaseSerializer):
"is_bot",
"display_name",
]
read_only_fields = ["id", "is_bot"]
read_only_fields = [
"id",
"is_bot",
]
class UserAdminLiteSerializer(BaseSerializer):
@@ -160,7 +183,10 @@ class UserAdminLiteSerializer(BaseSerializer):
"email",
"last_login_medium",
]
read_only_fields = ["id", "is_bot"]
read_only_fields = [
"id",
"is_bot",
]
class ChangePasswordSerializer(serializers.Serializer):
@@ -181,7 +207,9 @@ class ChangePasswordSerializer(serializers.Serializer):
if data.get("new_password") != data.get("confirm_password"):
raise serializers.ValidationError(
{"error": "Confirm password should be same as the new password."}
{
"error": "Confirm password should be same as the new password."
}
)
return data
@@ -199,11 +227,15 @@ class ProfileSerializer(BaseSerializer):
class Meta:
model = Profile
fields = "__all__"
read_only_fields = ["user"]
read_only_fields = [
"user",
]
class AccountSerializer(BaseSerializer):
class Meta:
model = Account
fields = "__all__"
read_only_fields = ["user"]
read_only_fields = [
"user",
]
+13 -4
View File
@@ -47,9 +47,13 @@ class WebhookSerializer(DynamicBaseSerializer):
# Additional validation for multiple request domains and their subdomains
request = self.context.get("request")
disallowed_domains = ["plane.so"] # Add your disallowed domains here
disallowed_domains = [
"plane.so",
] # Add your disallowed domains here
if request:
request_host = request.get_host().split(":")[0] # Remove port if present
request_host = request.get_host().split(":")[
0
] # Remove port if present
disallowed_domains.append(request_host)
# Check if hostname is a subdomain or exact match of any disallowed domain
@@ -95,7 +99,9 @@ class WebhookSerializer(DynamicBaseSerializer):
# Additional validation for multiple request domains and their subdomains
request = self.context.get("request")
disallowed_domains = ["plane.so"] # Add your disallowed domains here
disallowed_domains = [
"plane.so",
] # Add your disallowed domains here
if request:
request_host = request.get_host().split(":")[
0
@@ -116,7 +122,10 @@ class WebhookSerializer(DynamicBaseSerializer):
class Meta:
model = Webhook
fields = "__all__"
read_only_fields = ["workspace", "secret_key"]
read_only_fields = [
"workspace",
"secret_key",
]
class WebhookLogSerializer(DynamicBaseSerializer):
+66 -4
View File
@@ -6,8 +6,11 @@ from .base import BaseSerializer, DynamicBaseSerializer
from .user import UserLiteSerializer, UserAdminLiteSerializer
from plane.db.models import (
User,
Workspace,
WorkspaceMember,
Team,
TeamMember,
WorkspaceMemberInvite,
WorkspaceTheme,
WorkspaceUserProperties,
@@ -44,7 +47,11 @@ class WorkSpaceSerializer(DynamicBaseSerializer):
class WorkspaceLiteSerializer(BaseSerializer):
class Meta:
model = Workspace
fields = ["name", "slug", "id"]
fields = [
"name",
"slug",
"id",
]
read_only_fields = fields
@@ -59,7 +66,6 @@ class WorkSpaceMemberSerializer(DynamicBaseSerializer):
class WorkspaceMemberMeSerializer(BaseSerializer):
draft_issue_count = serializers.IntegerField(read_only=True)
class Meta:
model = WorkspaceMember
fields = "__all__"
@@ -94,15 +100,71 @@ class WorkSpaceMemberInviteSerializer(BaseSerializer):
]
class TeamSerializer(BaseSerializer):
members_detail = UserLiteSerializer(
read_only=True, source="members", many=True
)
members = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
write_only=True,
required=False,
)
class Meta:
model = Team
fields = "__all__"
read_only_fields = [
"workspace",
"created_by",
"updated_by",
"created_at",
"updated_at",
]
def create(self, validated_data, **kwargs):
if "members" in validated_data:
members = validated_data.pop("members")
workspace = self.context["workspace"]
team = Team.objects.create(**validated_data, workspace=workspace)
team_members = [
TeamMember(member=member, team=team, workspace=workspace)
for member in members
]
TeamMember.objects.bulk_create(team_members, batch_size=10)
return team
team = Team.objects.create(**validated_data)
return team
def update(self, instance, validated_data):
if "members" in validated_data:
members = validated_data.pop("members")
TeamMember.objects.filter(team=instance).delete()
team_members = [
TeamMember(
member=member, team=instance, workspace=instance.workspace
)
for member in members
]
TeamMember.objects.bulk_create(team_members, batch_size=10)
return super().update(instance, validated_data)
return super().update(instance, validated_data)
class WorkspaceThemeSerializer(BaseSerializer):
class Meta:
model = WorkspaceTheme
fields = "__all__"
read_only_fields = ["workspace", "actor"]
read_only_fields = [
"workspace",
"actor",
]
class WorkspaceUserPropertiesSerializer(BaseSerializer):
class Meta:
model = WorkspaceUserProperties
fields = "__all__"
read_only_fields = ["workspace", "user"]
read_only_fields = [
"workspace",
"user",
]
+10 -2
View File
@@ -26,7 +26,11 @@ urlpatterns = [
FileAssetEndpoint.as_view(),
name="file-assets",
),
path("users/file-assets/", UserAssetsEndpoint.as_view(), name="user-file-assets"),
path(
"users/file-assets/",
UserAssetsEndpoint.as_view(),
name="user-file-assets",
),
path(
"users/file-assets/<str:asset_key>/",
UserAssetsEndpoint.as_view(),
@@ -34,7 +38,11 @@ urlpatterns = [
),
path(
"workspaces/file-assets/<uuid:workspace_id>/<str:asset_key>/restore/",
FileAssetViewSet.as_view({"post": "restore"}),
FileAssetViewSet.as_view(
{
"post": "restore",
}
),
name="file-assets-restore",
),
# V2 Endpoints
+23 -4
View File
@@ -17,7 +17,12 @@ from plane.app.views import (
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/",
CycleViewSet.as_view({"get": "list", "post": "create"}),
CycleViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-cycle",
),
path(
@@ -34,7 +39,12 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/cycle-issues/",
CycleIssueViewSet.as_view({"get": "list", "post": "create"}),
CycleIssueViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-issue-cycle",
),
path(
@@ -56,12 +66,21 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-cycles/",
CycleFavoriteViewSet.as_view({"get": "list", "post": "create"}),
CycleFavoriteViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="user-favorite-cycle",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-cycles/<uuid:cycle_id>/",
CycleFavoriteViewSet.as_view({"delete": "destroy"}),
CycleFavoriteViewSet.as_view(
{
"delete": "destroy",
}
),
name="user-favorite-cycle",
),
path(
+22 -4
View File
@@ -16,24 +16,42 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/",
BulkEstimatePointEndpoint.as_view({"get": "list", "post": "create"}),
BulkEstimatePointEndpoint.as_view(
{
"get": "list",
"post": "create",
}
),
name="bulk-create-estimate-points",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/<uuid:estimate_id>/",
BulkEstimatePointEndpoint.as_view(
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="bulk-create-estimate-points",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/<uuid:estimate_id>/estimate-points/",
EstimatePointEndpoint.as_view({"post": "create"}),
EstimatePointEndpoint.as_view(
{
"post": "create",
}
),
name="estimate-points",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/<uuid:estimate_id>/estimate-points/<estimate_point_id>/",
EstimatePointEndpoint.as_view({"patch": "partial_update", "delete": "destroy"}),
EstimatePointEndpoint.as_view(
{
"patch": "partial_update",
"delete": "destroy",
}
),
name="estimate-points",
),
]
+6 -2
View File
@@ -6,13 +6,17 @@ from plane.app.views import GPTIntegrationEndpoint, WorkspaceGPTIntegrationEndpo
urlpatterns = [
path("unsplash/", UnsplashEndpoint.as_view(), name="unsplash"),
path(
"unsplash/",
UnsplashEndpoint.as_view(),
name="unsplash",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/ai-assistant/",
GPTIntegrationEndpoint.as_view(),
name="importer",
),
path(
path(
"workspaces/<str:slug>/ai-assistant/",
WorkspaceGPTIntegrationEndpoint.as_view(),
name="importer",
+48 -9
View File
@@ -1,55 +1,94 @@
from django.urls import path
from plane.app.views import IntakeViewSet, IntakeIssueViewSet
from plane.app.views import (
IntakeViewSet,
IntakeIssueViewSet,
)
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/intakes/",
IntakeViewSet.as_view({"get": "list", "post": "create"}),
IntakeViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="intake",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/intakes/<uuid:pk>/",
IntakeViewSet.as_view(
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="intake",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/intake-issues/",
IntakeIssueViewSet.as_view({"get": "list", "post": "create"}),
IntakeIssueViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="intake-issue",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/intake-issues/<uuid:pk>/",
IntakeIssueViewSet.as_view(
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="intake-issue",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/",
IntakeViewSet.as_view({"get": "list", "post": "create"}),
IntakeViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="inbox",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/<uuid:pk>/",
IntakeViewSet.as_view(
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="inbox",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/",
IntakeIssueViewSet.as_view({"get": "list", "post": "create"}),
IntakeIssueViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="inbox-issue",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/<uuid:pk>/",
IntakeIssueViewSet.as_view(
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="inbox-issue",
),
+78 -14
View File
@@ -34,7 +34,12 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/",
IssueViewSet.as_view({"get": "list", "post": "create"}),
IssueViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-issue",
),
path(
@@ -63,7 +68,12 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-labels/",
LabelViewSet.as_view({"get": "list", "post": "create"}),
LabelViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-issue-labels",
),
path(
@@ -101,7 +111,12 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-links/",
IssueLinkViewSet.as_view({"get": "list", "post": "create"}),
IssueLinkViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-issue-links",
),
path(
@@ -154,7 +169,12 @@ urlpatterns = [
## IssueComments
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/comments/",
IssueCommentViewSet.as_view({"get": "list", "post": "create"}),
IssueCommentViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-issue-comment",
),
path(
@@ -173,7 +193,12 @@ urlpatterns = [
# Issue Subscribers
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-subscribers/",
IssueSubscriberViewSet.as_view({"get": "list", "post": "create"}),
IssueSubscriberViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-issue-subscribers",
),
path(
@@ -184,7 +209,11 @@ urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/subscribe/",
IssueSubscriberViewSet.as_view(
{"get": "subscription_status", "post": "subscribe", "delete": "unsubscribe"}
{
"get": "subscription_status",
"post": "subscribe",
"delete": "unsubscribe",
}
),
name="project-issue-subscribers",
),
@@ -192,24 +221,42 @@ urlpatterns = [
# Issue Reactions
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/reactions/",
IssueReactionViewSet.as_view({"get": "list", "post": "create"}),
IssueReactionViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-issue-reactions",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/reactions/<str:reaction_code>/",
IssueReactionViewSet.as_view({"delete": "destroy"}),
IssueReactionViewSet.as_view(
{
"delete": "destroy",
}
),
name="project-issue-reactions",
),
## End Issue Reactions
# Comment Reactions
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/comments/<uuid:comment_id>/reactions/",
CommentReactionViewSet.as_view({"get": "list", "post": "create"}),
CommentReactionViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-issue-comment-reactions",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/comments/<uuid:comment_id>/reactions/<str:reaction_code>/",
CommentReactionViewSet.as_view({"delete": "destroy"}),
CommentReactionViewSet.as_view(
{
"delete": "destroy",
}
),
name="project-issue-comment-reactions",
),
## End Comment Reactions
@@ -223,13 +270,21 @@ urlpatterns = [
## Issue Archives
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-issues/",
IssueArchiveViewSet.as_view({"get": "list"}),
IssueArchiveViewSet.as_view(
{
"get": "list",
}
),
name="project-issue-archive",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:pk>/archive/",
IssueArchiveViewSet.as_view(
{"get": "retrieve", "post": "archive", "delete": "unarchive"}
{
"get": "retrieve",
"post": "archive",
"delete": "unarchive",
}
),
name="project-issue-archive-unarchive",
),
@@ -237,12 +292,21 @@ urlpatterns = [
## Issue Relation
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-relation/",
IssueRelationViewSet.as_view({"get": "list", "post": "create"}),
IssueRelationViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="issue-relation",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/remove-relation/",
IssueRelationViewSet.as_view({"post": "remove_relation"}),
IssueRelationViewSet.as_view(
{
"post": "remove_relation",
}
),
name="issue-relation",
),
## End Issue Relation
+34 -6
View File
@@ -14,7 +14,12 @@ from plane.app.views import (
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/",
ModuleViewSet.as_view({"get": "list", "post": "create"}),
ModuleViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-modules",
),
path(
@@ -31,12 +36,21 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/modules/",
ModuleIssueViewSet.as_view({"post": "create_issue_modules"}),
ModuleIssueViewSet.as_view(
{
"post": "create_issue_modules",
}
),
name="issue-module",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/issues/",
ModuleIssueViewSet.as_view({"post": "create_module_issues", "get": "list"}),
ModuleIssueViewSet.as_view(
{
"post": "create_module_issues",
"get": "list",
}
),
name="project-module-issues",
),
path(
@@ -53,7 +67,12 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/module-links/",
ModuleLinkViewSet.as_view({"get": "list", "post": "create"}),
ModuleLinkViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-issue-module-links",
),
path(
@@ -70,12 +89,21 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-modules/",
ModuleFavoriteViewSet.as_view({"get": "list", "post": "create"}),
ModuleFavoriteViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="user-favorite-module",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-modules/<uuid:module_id>/",
ModuleFavoriteViewSet.as_view({"delete": "destroy"}),
ModuleFavoriteViewSet.as_view(
{
"delete": "destroy",
}
),
name="user-favorite-module",
),
path(
+27 -5
View File
@@ -12,24 +12,42 @@ from plane.app.views import (
urlpatterns = [
path(
"workspaces/<str:slug>/users/notifications/",
NotificationViewSet.as_view({"get": "list"}),
NotificationViewSet.as_view(
{
"get": "list",
}
),
name="notifications",
),
path(
"workspaces/<str:slug>/users/notifications/<uuid:pk>/",
NotificationViewSet.as_view(
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="notifications",
),
path(
"workspaces/<str:slug>/users/notifications/<uuid:pk>/read/",
NotificationViewSet.as_view({"post": "mark_read", "delete": "mark_unread"}),
NotificationViewSet.as_view(
{
"post": "mark_read",
"delete": "mark_unread",
}
),
name="notifications",
),
path(
"workspaces/<str:slug>/users/notifications/<uuid:pk>/archive/",
NotificationViewSet.as_view({"post": "archive", "delete": "unarchive"}),
NotificationViewSet.as_view(
{
"post": "archive",
"delete": "unarchive",
}
),
name="notifications",
),
path(
@@ -39,7 +57,11 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/users/notifications/mark-all-read/",
MarkAllReadNotificationViewSet.as_view({"post": "create"}),
MarkAllReadNotificationViewSet.as_view(
{
"post": "create",
}
),
name="mark-all-read-notifications",
),
path(
+40 -7
View File
@@ -14,38 +14,66 @@ from plane.app.views import (
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/",
PageViewSet.as_view({"get": "list", "post": "create"}),
PageViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-pages",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/",
PageViewSet.as_view(
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="project-pages",
),
# favorite pages
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/favorite-pages/<uuid:pk>/",
PageFavoriteViewSet.as_view({"post": "create", "delete": "destroy"}),
PageFavoriteViewSet.as_view(
{
"post": "create",
"delete": "destroy",
}
),
name="user-favorite-pages",
),
# archived pages
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/archive/",
PageViewSet.as_view({"post": "archive", "delete": "unarchive"}),
PageViewSet.as_view(
{
"post": "archive",
"delete": "unarchive",
}
),
name="project-page-archive-unarchive",
),
# lock and unlock
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/lock/",
PageViewSet.as_view({"post": "lock", "delete": "unlock"}),
PageViewSet.as_view(
{
"post": "lock",
"delete": "unlock",
}
),
name="project-pages-lock-unlock",
),
# private and public page
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/access/",
PageViewSet.as_view({"post": "access"}),
PageViewSet.as_view(
{
"post": "access",
}
),
name="project-pages-access",
),
path(
@@ -65,7 +93,12 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/description/",
PagesDescriptionViewSet.as_view({"get": "retrieve", "patch": "partial_update"}),
PagesDescriptionViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
}
),
name="page-description",
),
path(
+68 -11
View File
@@ -7,6 +7,7 @@ from plane.app.views import (
ProjectMemberViewSet,
ProjectMemberUserEndpoint,
ProjectJoinEndpoint,
AddTeamToProjectEndpoint,
ProjectUserViewsEndpoint,
ProjectIdentifierEndpoint,
ProjectFavoritesViewSet,
@@ -20,7 +21,12 @@ from plane.app.views import (
urlpatterns = [
path(
"workspaces/<str:slug>/projects/",
ProjectViewSet.as_view({"get": "list", "post": "create"}),
ProjectViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project",
),
path(
@@ -42,17 +48,32 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/invitations/",
ProjectInvitationsViewset.as_view({"get": "list", "post": "create"}),
ProjectInvitationsViewset.as_view(
{
"get": "list",
"post": "create",
},
),
name="project-member-invite",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/invitations/<uuid:pk>/",
ProjectInvitationsViewset.as_view({"get": "retrieve", "delete": "destroy"}),
ProjectInvitationsViewset.as_view(
{
"get": "retrieve",
"delete": "destroy",
}
),
name="project-member-invite",
),
path(
"users/me/workspaces/<str:slug>/projects/invitations/",
UserProjectInvitationsViewset.as_view({"get": "list", "post": "create"}),
UserProjectInvitationsViewset.as_view(
{
"get": "list",
"post": "create",
},
),
name="user-project-invitations",
),
path(
@@ -67,21 +88,39 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/members/",
ProjectMemberViewSet.as_view({"get": "list", "post": "create"}),
ProjectMemberViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-member",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/members/<uuid:pk>/",
ProjectMemberViewSet.as_view(
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="project-member",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/members/leave/",
ProjectMemberViewSet.as_view({"post": "leave"}),
ProjectMemberViewSet.as_view(
{
"post": "leave",
}
),
name="project-member",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/team-invite/",
AddTeamToProjectEndpoint.as_view(),
name="projects",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/project-views/",
ProjectUserViewsEndpoint.as_view(),
@@ -94,12 +133,21 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/user-favorite-projects/",
ProjectFavoritesViewSet.as_view({"get": "list", "post": "create"}),
ProjectFavoritesViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-favorite",
),
path(
"workspaces/<str:slug>/user-favorite-projects/<uuid:project_id>/",
ProjectFavoritesViewSet.as_view({"delete": "destroy"}),
ProjectFavoritesViewSet.as_view(
{
"delete": "destroy",
}
),
name="project-favorite",
),
path(
@@ -109,13 +157,22 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/project-deploy-boards/",
DeployBoardViewSet.as_view({"get": "list", "post": "create"}),
DeployBoardViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-deploy-board",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/project-deploy-boards/<uuid:pk>/",
DeployBoardViewSet.as_view(
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="project-deploy-board",
),
+4 -1
View File
@@ -1,7 +1,10 @@
from django.urls import path
from plane.app.views import GlobalSearchEndpoint, IssueSearchEndpoint
from plane.app.views import (
GlobalSearchEndpoint,
IssueSearchEndpoint,
)
urlpatterns = [
+16 -3
View File
@@ -7,19 +7,32 @@ from plane.app.views import StateViewSet
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/states/",
StateViewSet.as_view({"get": "list", "post": "create"}),
StateViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-states",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/states/<uuid:pk>/",
StateViewSet.as_view(
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="project-state",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/states/<uuid:pk>/mark-default/",
StateViewSet.as_view({"post": "mark_as_default"}),
StateViewSet.as_view(
{
"post": "mark_as_default",
}
),
name="project-state",
),
]
+44 -10
View File
@@ -22,30 +22,60 @@ urlpatterns = [
path(
"users/me/",
UserEndpoint.as_view(
{"get": "retrieve", "patch": "partial_update", "delete": "deactivate"}
{
"get": "retrieve",
"patch": "partial_update",
"delete": "deactivate",
}
),
name="users",
),
path("users/session/", UserSessionEndpoint.as_view(), name="user-session"),
path(
"users/session/",
UserSessionEndpoint.as_view(),
name="user-session",
),
path(
"users/me/settings/",
UserEndpoint.as_view({"get": "retrieve_user_settings"}),
UserEndpoint.as_view(
{
"get": "retrieve_user_settings",
}
),
name="users",
),
# Profile
path("users/me/profile/", ProfileEndpoint.as_view(), name="accounts"),
path(
"users/me/profile/",
ProfileEndpoint.as_view(),
name="accounts",
),
# End profile
# Accounts
path("users/me/accounts/", AccountEndpoint.as_view(), name="accounts"),
path("users/me/accounts/<uuid:pk>/", AccountEndpoint.as_view(), name="accounts"),
path(
"users/me/accounts/",
AccountEndpoint.as_view(),
name="accounts",
),
path(
"users/me/accounts/<uuid:pk>/",
AccountEndpoint.as_view(),
name="accounts",
),
## End Accounts
path(
"users/me/instance-admin/",
UserEndpoint.as_view({"get": "retrieve_instance_admin"}),
UserEndpoint.as_view(
{
"get": "retrieve_instance_admin",
}
),
name="users",
),
path(
"users/me/onboard/", UpdateUserOnBoardedEndpoint.as_view(), name="user-onboard"
"users/me/onboard/",
UpdateUserOnBoardedEndpoint.as_view(),
name="user-onboard",
),
path(
"users/me/tour-completed/",
@@ -53,11 +83,15 @@ urlpatterns = [
name="user-tour",
),
path(
"users/me/activities/", UserActivityEndpoint.as_view(), name="user-activities"
"users/me/activities/",
UserActivityEndpoint.as_view(),
name="user-activities",
),
# user workspaces
path(
"users/me/workspaces/", UserWorkSpacesEndpoint.as_view(), name="user-workspace"
"users/me/workspaces/",
UserWorkSpacesEndpoint.as_view(),
name="user-workspace",
),
# User Graphs
path(
+28 -5
View File
@@ -12,7 +12,12 @@ from plane.app.views import (
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/views/",
IssueViewViewSet.as_view({"get": "list", "post": "create"}),
IssueViewViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-view",
),
path(
@@ -29,7 +34,12 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/views/",
WorkspaceViewViewSet.as_view({"get": "list", "post": "create"}),
WorkspaceViewViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="global-view",
),
path(
@@ -46,17 +56,30 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/issues/",
WorkspaceViewIssuesViewSet.as_view({"get": "list"}),
WorkspaceViewIssuesViewSet.as_view(
{
"get": "list",
}
),
name="global-view-issues",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-views/",
IssueViewFavoriteViewSet.as_view({"get": "list", "post": "create"}),
IssueViewFavoriteViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="user-favorite-view",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-views/<uuid:view_id>/",
IssueViewFavoriteViewSet.as_view({"delete": "destroy"}),
IssueViewFavoriteViewSet.as_view(
{
"delete": "destroy",
}
),
name="user-favorite-view",
),
]

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