Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 015f8cbfe5 | |||
| 42b38db131 |
@@ -35,10 +35,6 @@ on:
|
||||
- preview
|
||||
- canary
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
TARGET_BRANCH: ${{ github.ref_name }}
|
||||
ARM64_BUILD: ${{ github.event.inputs.arm64 }}
|
||||
@@ -272,14 +268,15 @@ jobs:
|
||||
if: ${{ needs.branch_build_setup.outputs.aio_build == 'true' }}
|
||||
name: Build-Push AIO Docker Image
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- branch_build_setup
|
||||
- branch_build_push_admin
|
||||
- branch_build_push_web
|
||||
- branch_build_push_space
|
||||
- branch_build_push_live
|
||||
- branch_build_push_api
|
||||
- branch_build_push_proxy
|
||||
needs: [
|
||||
branch_build_setup,
|
||||
branch_build_push_admin,
|
||||
branch_build_push_web,
|
||||
branch_build_push_space,
|
||||
branch_build_push_live,
|
||||
branch_build_push_api,
|
||||
branch_build_push_proxy
|
||||
]
|
||||
steps:
|
||||
- name: Checkout Files
|
||||
uses: actions/checkout@v4
|
||||
@@ -288,7 +285,7 @@ jobs:
|
||||
id: prepare_aio_assets
|
||||
run: |
|
||||
cd deployments/aio/community
|
||||
|
||||
|
||||
if [ "${{ needs.branch_build_setup.outputs.build_type }}" == "Release" ]; then
|
||||
aio_version=${{ needs.branch_build_setup.outputs.release_version }}
|
||||
else
|
||||
@@ -327,14 +324,7 @@ jobs:
|
||||
upload_build_assets:
|
||||
name: Upload Build Assets
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- branch_build_setup
|
||||
- branch_build_push_admin
|
||||
- branch_build_push_web
|
||||
- branch_build_push_space
|
||||
- branch_build_push_live
|
||||
- branch_build_push_api
|
||||
- branch_build_push_proxy
|
||||
needs: [branch_build_setup, branch_build_push_admin, branch_build_push_web, branch_build_push_space, branch_build_push_live, branch_build_push_api, branch_build_push_proxy]
|
||||
steps:
|
||||
- name: Checkout Files
|
||||
uses: actions/checkout@v4
|
||||
@@ -407,3 +397,4 @@ jobs:
|
||||
${{ github.workspace }}/deployments/cli/community/docker-compose.yml
|
||||
${{ github.workspace }}/deployments/cli/community/variables.env
|
||||
${{ github.workspace }}/deployments/swarm/community/swarm.sh
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@ jobs:
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18'
|
||||
|
||||
- name: Get PR Branch version
|
||||
run: echo "PR_VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_ENV
|
||||
|
||||
@@ -3,21 +3,11 @@ name: Build and lint API
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
branches:
|
||||
- "preview"
|
||||
types:
|
||||
- "opened"
|
||||
- "synchronize"
|
||||
- "ready_for_review"
|
||||
- "review_requested"
|
||||
- "reopened"
|
||||
branches: ["preview"]
|
||||
types: ["opened", "synchronize", "ready_for_review", "review_requested", "reopened"]
|
||||
paths:
|
||||
- "apps/api/**"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
lint-api:
|
||||
name: Lint API
|
||||
|
||||
@@ -3,18 +3,21 @@ name: Build and lint web apps
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
branches:
|
||||
- "preview"
|
||||
branches: ["preview"]
|
||||
types:
|
||||
- "opened"
|
||||
- "synchronize"
|
||||
- "ready_for_review"
|
||||
- "review_requested"
|
||||
- "reopened"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
[
|
||||
"opened",
|
||||
"synchronize",
|
||||
"ready_for_review",
|
||||
"review_requested",
|
||||
"reopened",
|
||||
]
|
||||
paths:
|
||||
- "**.tsx?"
|
||||
- "**.jsx?"
|
||||
- "**.css"
|
||||
- "**.json"
|
||||
- "!apps/api/**"
|
||||
|
||||
jobs:
|
||||
build-and-lint:
|
||||
@@ -24,18 +27,16 @@ jobs:
|
||||
if: |
|
||||
github.event.pull_request.draft == false &&
|
||||
github.event.pull_request.requested_reviewers != null
|
||||
env:
|
||||
TURBO_SCM_BASE: ${{ github.event.pull_request.base.sha }}
|
||||
TURBO_SCM_HEAD: ${{ github.sha }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 50
|
||||
filter: blob:none
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
|
||||
- name: Enable Corepack and pnpm
|
||||
run: corepack enable pnpm
|
||||
@@ -43,11 +44,11 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Lint Affected
|
||||
run: pnpm turbo run check:lint --affected
|
||||
- name: Lint web apps
|
||||
run: pnpm run check:lint
|
||||
|
||||
- name: Check Affected format
|
||||
run: pnpm turbo run check:format --affected
|
||||
- name: Check format
|
||||
run: pnpm run check:format
|
||||
|
||||
- name: Build Affected
|
||||
run: pnpm turbo run build --affected
|
||||
- name: Build apps
|
||||
run: pnpm run build
|
||||
|
||||
@@ -14,7 +14,7 @@ strict-peer-dependencies=false
|
||||
# Turbo occasionally performs postinstall tasks for optimal performance
|
||||
# moved to pnpm-workspace.yaml: onlyBuiltDependencies (e.g., allow turbo)
|
||||
|
||||
public-hoist-pattern[]=*eslint*
|
||||
public-hoist-pattern[]=eslint
|
||||
public-hoist-pattern[]=prettier
|
||||
public-hoist-pattern[]=typescript
|
||||
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
.next/*
|
||||
out/*
|
||||
public/*
|
||||
dist/*
|
||||
node_modules/*
|
||||
.turbo/*
|
||||
.env*
|
||||
.env
|
||||
.env.local
|
||||
.env.development
|
||||
.env.production
|
||||
.env.test
|
||||
@@ -1,4 +1,5 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["@plane/eslint-config/next.js"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
};
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useInstance } from "@/hooks/store";
|
||||
// components
|
||||
import { InstanceEmailForm } from "./email-config-form";
|
||||
|
||||
const InstanceEmailPage: React.FC = observer(() => {
|
||||
const InstanceEmailPage = observer(() => {
|
||||
// store
|
||||
const { fetchInstanceConfigurations, formattedConfig, disableEmail } = useInstance();
|
||||
|
||||
@@ -29,7 +29,7 @@ const InstanceEmailPage: React.FC = observer(() => {
|
||||
message: "Email feature has been disabled",
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
});
|
||||
} catch (_error) {
|
||||
} catch (error) {
|
||||
setToast({
|
||||
title: "Error disabling email",
|
||||
message: "Failed to disable email feature. Please try again.",
|
||||
|
||||
@@ -7,8 +7,7 @@ import { ExternalLink, FileText, HelpCircle, MoveLeft } from "lucide-react";
|
||||
import { Transition } from "@headlessui/react";
|
||||
// plane internal packages
|
||||
import { WEB_BASE_URL } from "@plane/constants";
|
||||
import { DiscordIcon, GithubIcon } from "@plane/propel/icons";
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
import { DiscordIcon, GithubIcon, Tooltip } from "@plane/ui";
|
||||
import { cn } from "@plane/utils";
|
||||
// hooks
|
||||
import { useTheme } from "@/hooks/store";
|
||||
|
||||
@@ -5,8 +5,7 @@ import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { Image, BrainCog, Cog, Lock, Mail } from "lucide-react";
|
||||
// plane internal packages
|
||||
import { WorkspaceIcon } from "@plane/propel/icons";
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
import { Tooltip, WorkspaceIcon } from "@plane/ui";
|
||||
import { cn } from "@plane/utils";
|
||||
// hooks
|
||||
import { useTheme } from "@/hooks/store";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { PlaneLockup } from "@plane/propel/icons";
|
||||
import { PlaneLockup } from "@plane/ui";
|
||||
|
||||
export const AuthHeader = () => (
|
||||
<div className="flex items-center justify-between gap-6 w-full flex-shrink-0 sticky top-0">
|
||||
|
||||
@@ -25,8 +25,9 @@ export const EmailCodesConfiguration: React.FC<Props> = observer((props) => {
|
||||
<ToggleSwitch
|
||||
value={Boolean(parseInt(enableMagicLogin))}
|
||||
onChange={() => {
|
||||
const newEnableMagicLogin = Boolean(parseInt(enableMagicLogin)) === true ? "0" : "1";
|
||||
updateConfig("ENABLE_MAGIC_LINK_LOGIN", newEnableMagicLogin);
|
||||
Boolean(parseInt(enableMagicLogin)) === true
|
||||
? updateConfig("ENABLE_MAGIC_LINK_LOGIN", "0")
|
||||
: updateConfig("ENABLE_MAGIC_LINK_LOGIN", "1");
|
||||
}}
|
||||
size="sm"
|
||||
disabled={disabled}
|
||||
|
||||
@@ -35,8 +35,9 @@ export const GithubConfiguration: React.FC<Props> = observer((props) => {
|
||||
<ToggleSwitch
|
||||
value={Boolean(parseInt(enableGithubConfig))}
|
||||
onChange={() => {
|
||||
const newEnableGithubConfig = Boolean(parseInt(enableGithubConfig)) === true ? "0" : "1";
|
||||
updateConfig("IS_GITHUB_ENABLED", newEnableGithubConfig);
|
||||
Boolean(parseInt(enableGithubConfig)) === true
|
||||
? updateConfig("IS_GITHUB_ENABLED", "0")
|
||||
: updateConfig("IS_GITHUB_ENABLED", "1");
|
||||
}}
|
||||
size="sm"
|
||||
disabled={disabled}
|
||||
|
||||
@@ -35,8 +35,9 @@ export const GitlabConfiguration: React.FC<Props> = observer((props) => {
|
||||
<ToggleSwitch
|
||||
value={Boolean(parseInt(enableGitlabConfig))}
|
||||
onChange={() => {
|
||||
const newEnableGitlabConfig = Boolean(parseInt(enableGitlabConfig)) === true ? "0" : "1";
|
||||
updateConfig("IS_GITLAB_ENABLED", newEnableGitlabConfig);
|
||||
Boolean(parseInt(enableGitlabConfig)) === true
|
||||
? updateConfig("IS_GITLAB_ENABLED", "0")
|
||||
: updateConfig("IS_GITLAB_ENABLED", "1");
|
||||
}}
|
||||
size="sm"
|
||||
disabled={disabled}
|
||||
|
||||
@@ -35,8 +35,9 @@ export const GoogleConfiguration: React.FC<Props> = observer((props) => {
|
||||
<ToggleSwitch
|
||||
value={Boolean(parseInt(enableGoogleConfig))}
|
||||
onChange={() => {
|
||||
const newEnableGoogleConfig = Boolean(parseInt(enableGoogleConfig)) === true ? "0" : "1";
|
||||
updateConfig("IS_GOOGLE_ENABLED", newEnableGoogleConfig);
|
||||
Boolean(parseInt(enableGoogleConfig)) === true
|
||||
? updateConfig("IS_GOOGLE_ENABLED", "0")
|
||||
: updateConfig("IS_GOOGLE_ENABLED", "1");
|
||||
}}
|
||||
size="sm"
|
||||
disabled={disabled}
|
||||
|
||||
@@ -25,8 +25,9 @@ export const PasswordLoginConfiguration: React.FC<Props> = observer((props) => {
|
||||
<ToggleSwitch
|
||||
value={Boolean(parseInt(enableEmailPassword))}
|
||||
onChange={() => {
|
||||
const newEnableEmailPassword = Boolean(parseInt(enableEmailPassword)) === true ? "0" : "1";
|
||||
updateConfig("ENABLE_EMAIL_PASSWORD", newEnableEmailPassword);
|
||||
Boolean(parseInt(enableEmailPassword)) === true
|
||||
? updateConfig("ENABLE_EMAIL_PASSWORD", "0")
|
||||
: updateConfig("ENABLE_EMAIL_PASSWORD", "1");
|
||||
}}
|
||||
size="sm"
|
||||
disabled={disabled}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
import { Tooltip } from "@plane/ui";
|
||||
|
||||
type Props = {
|
||||
label?: string;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { observer } from "mobx-react";
|
||||
import { ExternalLink } from "lucide-react";
|
||||
// plane internal packages
|
||||
import { WEB_BASE_URL } from "@plane/constants";
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
import { Tooltip } from "@plane/ui";
|
||||
import { getFileURL } from "@plane/utils";
|
||||
// hooks
|
||||
import { useWorkspace } from "@/hooks/store";
|
||||
|
||||
@@ -209,7 +209,7 @@ export class InstanceStore implements IInstanceStore {
|
||||
});
|
||||
});
|
||||
await this.instanceService.disableEmail();
|
||||
} catch (_error) {
|
||||
} catch (error) {
|
||||
console.error("Error disabling the email");
|
||||
this.instanceConfigurations = instanceConfigurations;
|
||||
}
|
||||
|
||||
+16
-16
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "admin",
|
||||
"description": "Admin UI for Plane",
|
||||
"version": "1.0.0",
|
||||
"version": "0.28.0",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
@@ -26,30 +26,30 @@
|
||||
"@plane/ui": "workspace:*",
|
||||
"@plane/utils": "workspace:*",
|
||||
"autoprefixer": "10.4.14",
|
||||
"axios": "catalog:",
|
||||
"lodash": "catalog:",
|
||||
"lucide-react": "catalog:",
|
||||
"mobx": "catalog:",
|
||||
"mobx-react": "catalog:",
|
||||
"next": "catalog:",
|
||||
"axios": "1.11.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.469.0",
|
||||
"mobx": "^6.12.0",
|
||||
"mobx-react": "^9.1.1",
|
||||
"next": "14.2.30",
|
||||
"next-themes": "^0.2.1",
|
||||
"postcss": "^8.4.49",
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "7.51.5",
|
||||
"sharp": "catalog:",
|
||||
"swr": "catalog:",
|
||||
"uuid": "catalog:"
|
||||
"sharp": "^0.33.5",
|
||||
"swr": "^2.2.4",
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@plane/eslint-config": "workspace:*",
|
||||
"@plane/tailwind-config": "workspace:*",
|
||||
"@plane/typescript-config": "workspace:*",
|
||||
"@types/lodash": "catalog:",
|
||||
"@types/lodash": "^4.17.6",
|
||||
"@types/node": "18.16.1",
|
||||
"@types/react": "catalog:",
|
||||
"@types/react-dom": "catalog:",
|
||||
"@types/react": "^18.3.11",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"typescript": "catalog:"
|
||||
"typescript": "5.8.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "plane-api",
|
||||
"version": "1.0.0",
|
||||
"version": "0.28.0",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"description": "API server powering Plane's backend"
|
||||
|
||||
@@ -29,8 +29,7 @@ class BaseSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Adjust the serializer's fields based on the provided 'fields' list.
|
||||
|
||||
:param fields: List or dictionary specifying which fields to include
|
||||
in the serializer.
|
||||
:param fields: List or dictionary specifying which fields to include in the serializer.
|
||||
:return: The updated fields for the serializer.
|
||||
"""
|
||||
# Check each field_name in the provided fields.
|
||||
@@ -92,7 +91,6 @@ class BaseSerializer(serializers.ModelSerializer):
|
||||
"project_lead": UserLiteSerializer,
|
||||
"state": StateLiteSerializer,
|
||||
"created_by": UserLiteSerializer,
|
||||
"updated_by": UserLiteSerializer,
|
||||
"issue": IssueSerializer,
|
||||
"actor": UserLiteSerializer,
|
||||
"owned_by": UserLiteSerializer,
|
||||
|
||||
@@ -24,6 +24,7 @@ from plane.db.models import (
|
||||
)
|
||||
from plane.utils.content_validator import (
|
||||
validate_html_content,
|
||||
validate_json_content,
|
||||
validate_binary_data,
|
||||
)
|
||||
|
||||
@@ -43,8 +44,7 @@ class IssueSerializer(BaseSerializer):
|
||||
Comprehensive work item serializer with full relationship management.
|
||||
|
||||
Handles complete work item lifecycle including assignees, labels, validation,
|
||||
and related model updates. Supports dynamic field expansion and HTML content
|
||||
processing.
|
||||
and related model updates. Supports dynamic field expansion and HTML content processing.
|
||||
"""
|
||||
|
||||
assignees = serializers.ListField(
|
||||
@@ -89,24 +89,20 @@ class IssueSerializer(BaseSerializer):
|
||||
raise serializers.ValidationError("Invalid HTML passed")
|
||||
|
||||
# Validate description content for security
|
||||
if data.get("description_html"):
|
||||
is_valid, error_msg, sanitized_html = validate_html_content(
|
||||
data["description_html"]
|
||||
)
|
||||
if data.get("description"):
|
||||
is_valid, error_msg = validate_json_content(data["description"])
|
||||
if not is_valid:
|
||||
raise serializers.ValidationError(
|
||||
{"error": "html content is not valid"}
|
||||
)
|
||||
# Update the data with sanitized HTML if available
|
||||
if sanitized_html is not None:
|
||||
data["description_html"] = sanitized_html
|
||||
raise serializers.ValidationError({"description": error_msg})
|
||||
|
||||
if data.get("description_html"):
|
||||
is_valid, error_msg = validate_html_content(data["description_html"])
|
||||
if not is_valid:
|
||||
raise serializers.ValidationError({"description_html": error_msg})
|
||||
|
||||
if data.get("description_binary"):
|
||||
is_valid, error_msg = validate_binary_data(data["description_binary"])
|
||||
if not is_valid:
|
||||
raise serializers.ValidationError(
|
||||
{"description_binary": "Invalid binary data"}
|
||||
)
|
||||
raise serializers.ValidationError({"description_binary": error_msg})
|
||||
|
||||
# Validate assignees are from project
|
||||
if data.get("assignees", []):
|
||||
|
||||
@@ -17,8 +17,7 @@ class ModuleCreateSerializer(BaseSerializer):
|
||||
"""
|
||||
Serializer for creating modules with member validation and date checking.
|
||||
|
||||
Handles module creation including member assignment validation, date range
|
||||
verification,
|
||||
Handles module creation including member assignment validation, date range verification,
|
||||
and duplicate name prevention for feature-based project organization setup.
|
||||
"""
|
||||
|
||||
@@ -106,8 +105,7 @@ class ModuleUpdateSerializer(ModuleCreateSerializer):
|
||||
"""
|
||||
Serializer for updating modules with enhanced validation and member management.
|
||||
|
||||
Extends module creation with update-specific validations including member
|
||||
reassignment,
|
||||
Extends module creation with update-specific validations including member reassignment,
|
||||
name conflict checking, and relationship management for module modifications.
|
||||
"""
|
||||
|
||||
@@ -157,8 +155,7 @@ class ModuleSerializer(BaseSerializer):
|
||||
"""
|
||||
Comprehensive module serializer with work item metrics and member management.
|
||||
|
||||
Provides complete module data including work item counts by status, member
|
||||
relationships,
|
||||
Provides complete module data including work item counts by status, member relationships,
|
||||
and progress tracking for feature-based project organization.
|
||||
"""
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ from plane.db.models import (
|
||||
|
||||
from plane.utils.content_validator import (
|
||||
validate_html_content,
|
||||
validate_json_content,
|
||||
)
|
||||
from .base import BaseSerializer
|
||||
|
||||
@@ -199,18 +200,27 @@ class ProjectSerializer(BaseSerializer):
|
||||
)
|
||||
|
||||
# Validate description content for security
|
||||
if "description" in data and data["description"]:
|
||||
# For Project, description might be text field, not JSON
|
||||
if isinstance(data["description"], dict):
|
||||
is_valid, error_msg = validate_json_content(data["description"])
|
||||
if not is_valid:
|
||||
raise serializers.ValidationError({"description": error_msg})
|
||||
|
||||
if "description_text" in data and data["description_text"]:
|
||||
is_valid, error_msg = validate_json_content(data["description_text"])
|
||||
if not is_valid:
|
||||
raise serializers.ValidationError({"description_text": error_msg})
|
||||
|
||||
if "description_html" in data and data["description_html"]:
|
||||
if isinstance(data["description_html"], dict):
|
||||
is_valid, error_msg, sanitized_html = validate_html_content(
|
||||
is_valid, error_msg = validate_json_content(data["description_html"])
|
||||
else:
|
||||
is_valid, error_msg = validate_html_content(
|
||||
str(data["description_html"])
|
||||
)
|
||||
# Update the data with sanitized HTML if available
|
||||
if sanitized_html is not None:
|
||||
data["description_html"] = sanitized_html
|
||||
if not is_valid:
|
||||
raise serializers.ValidationError(
|
||||
{"error": "html content is not valid"}
|
||||
)
|
||||
raise serializers.ValidationError({"description_html": error_msg})
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from django.conf import settings
|
||||
# Third party imports
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
from drf_spectacular.utils import OpenApiExample, OpenApiRequest
|
||||
from drf_spectacular.utils import OpenApiExample, OpenApiRequest, OpenApiTypes
|
||||
|
||||
# Module Imports
|
||||
from plane.bgtasks.storage_metadata_task import get_asset_object_metadata
|
||||
@@ -106,8 +106,7 @@ class UserAssetEndpoint(BaseAPIView):
|
||||
def post(self, request):
|
||||
"""Generate presigned URL for user asset upload.
|
||||
|
||||
Create a presigned URL for uploading user profile assets (avatar or cover
|
||||
image).
|
||||
Create a presigned URL for uploading user profile assets (avatar or cover image).
|
||||
This endpoint generates the necessary credentials for direct S3 upload.
|
||||
"""
|
||||
# get the asset key
|
||||
@@ -202,10 +201,8 @@ class UserAssetEndpoint(BaseAPIView):
|
||||
def patch(self, request, asset_id):
|
||||
"""Update user asset after upload completion.
|
||||
|
||||
Update the asset status and attributes after the file has been uploaded to
|
||||
S3.
|
||||
This endpoint should be called after completing the S3 upload to mark the
|
||||
asset as uploaded.
|
||||
Update the asset status and attributes after the file has been uploaded to S3.
|
||||
This endpoint should be called after completing the S3 upload to mark the asset as uploaded.
|
||||
"""
|
||||
# get the asset id
|
||||
asset = FileAsset.objects.get(id=asset_id, user_id=request.user.id)
|
||||
@@ -364,8 +361,7 @@ class UserServerAssetEndpoint(BaseAPIView):
|
||||
"""Update user server asset after upload completion.
|
||||
|
||||
Update the asset status and attributes after the file has been uploaded to S3 using server credentials.
|
||||
This endpoint should be called after completing the S3 upload to mark the
|
||||
asset as uploaded.
|
||||
This endpoint should be called after completing the S3 upload to mark the asset as uploaded.
|
||||
"""
|
||||
# get the asset id
|
||||
asset = FileAsset.objects.get(id=asset_id, user_id=request.user.id)
|
||||
|
||||
@@ -30,10 +30,12 @@ from rest_framework.response import Response
|
||||
# drf-spectacular imports
|
||||
from drf_spectacular.utils import (
|
||||
extend_schema,
|
||||
OpenApiParameter,
|
||||
OpenApiResponse,
|
||||
OpenApiExample,
|
||||
OpenApiRequest,
|
||||
)
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
|
||||
# Module imports
|
||||
from plane.api.serializers import (
|
||||
@@ -97,6 +99,7 @@ from plane.utils.openapi import (
|
||||
EXTERNAL_ID_PARAMETER,
|
||||
EXTERNAL_SOURCE_PARAMETER,
|
||||
ORDER_BY_PARAMETER,
|
||||
SEARCH_PARAMETER,
|
||||
SEARCH_PARAMETER_REQUIRED,
|
||||
LIMIT_PARAMETER,
|
||||
WORKSPACE_SEARCH_PARAMETER,
|
||||
|
||||
@@ -10,7 +10,7 @@ from django.core.serializers.json import DjangoJSONEncoder
|
||||
# Third party imports
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
from drf_spectacular.utils import OpenApiResponse, OpenApiRequest
|
||||
from drf_spectacular.utils import OpenApiResponse, OpenApiExample, OpenApiRequest
|
||||
|
||||
# Module imports
|
||||
from plane.api.serializers import (
|
||||
@@ -41,6 +41,8 @@ from plane.utils.host import base_host
|
||||
from plane.utils.openapi import (
|
||||
module_docs,
|
||||
module_issue_docs,
|
||||
WORKSPACE_SLUG_PARAMETER,
|
||||
PROJECT_ID_PARAMETER,
|
||||
MODULE_ID_PARAMETER,
|
||||
MODULE_PK_PARAMETER,
|
||||
ISSUE_ID_PARAMETER,
|
||||
|
||||
@@ -11,7 +11,7 @@ from django.core.serializers.json import DjangoJSONEncoder
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import ValidationError
|
||||
from drf_spectacular.utils import OpenApiResponse, OpenApiRequest
|
||||
from drf_spectacular.utils import OpenApiResponse, OpenApiExample, OpenApiRequest
|
||||
|
||||
|
||||
# Module imports
|
||||
|
||||
@@ -4,7 +4,7 @@ from django.db import IntegrityError
|
||||
# Third party imports
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
from drf_spectacular.utils import OpenApiResponse, OpenApiRequest
|
||||
from drf_spectacular.utils import OpenApiResponse, OpenApiExample, OpenApiRequest
|
||||
|
||||
# Module imports
|
||||
from plane.api.serializers import StateSerializer
|
||||
|
||||
@@ -39,31 +39,13 @@ def allow_permission(allowed_roles, level="PROJECT", creator=False, model=None):
|
||||
).exists():
|
||||
return view_func(instance, request, *args, **kwargs)
|
||||
else:
|
||||
is_user_has_allowed_role = ProjectMember.objects.filter(
|
||||
if ProjectMember.objects.filter(
|
||||
member=request.user,
|
||||
workspace__slug=kwargs["slug"],
|
||||
project_id=kwargs["project_id"],
|
||||
role__in=allowed_role_values,
|
||||
is_active=True,
|
||||
).exists()
|
||||
|
||||
# Return if the user has the allowed role else if they are workspace admin and part of the project regardless of the role
|
||||
if is_user_has_allowed_role:
|
||||
return view_func(instance, request, *args, **kwargs)
|
||||
elif (
|
||||
ProjectMember.objects.filter(
|
||||
member=request.user,
|
||||
workspace__slug=kwargs["slug"],
|
||||
project_id=kwargs["project_id"],
|
||||
is_active=True,
|
||||
).exists()
|
||||
and WorkspaceMember.objects.filter(
|
||||
member=request.user,
|
||||
workspace__slug=kwargs["slug"],
|
||||
role=ROLE.ADMIN.value,
|
||||
is_active=True,
|
||||
).exists()
|
||||
):
|
||||
).exists():
|
||||
return view_func(instance, request, *args, **kwargs)
|
||||
|
||||
# Return permission denied if no conditions are met
|
||||
|
||||
@@ -3,7 +3,11 @@ from rest_framework.permissions import SAFE_METHODS, BasePermission
|
||||
|
||||
# Module import
|
||||
from plane.db.models import ProjectMember, WorkspaceMember
|
||||
from plane.db.models.project import ROLE
|
||||
|
||||
# Permission Mappings
|
||||
Admin = 20
|
||||
Member = 15
|
||||
Guest = 5
|
||||
|
||||
|
||||
class ProjectBasePermission(BasePermission):
|
||||
@@ -22,31 +26,18 @@ class ProjectBasePermission(BasePermission):
|
||||
return WorkspaceMember.objects.filter(
|
||||
workspace__slug=view.workspace_slug,
|
||||
member=request.user,
|
||||
role__in=[ROLE.ADMIN.value, ROLE.MEMBER.value],
|
||||
role__in=[Admin, Member],
|
||||
is_active=True,
|
||||
).exists()
|
||||
|
||||
project_member_qs = ProjectMember.objects.filter(
|
||||
## Only Project Admins can update project attributes
|
||||
return ProjectMember.objects.filter(
|
||||
workspace__slug=view.workspace_slug,
|
||||
member=request.user,
|
||||
role=Admin,
|
||||
project_id=view.project_id,
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
## Only project admins or workspace admin who is part of the project can access
|
||||
|
||||
if project_member_qs.filter(role=ROLE.ADMIN.value).exists():
|
||||
return True
|
||||
else:
|
||||
return (
|
||||
project_member_qs.exists()
|
||||
and WorkspaceMember.objects.filter(
|
||||
member=request.user,
|
||||
workspace__slug=view.workspace_slug,
|
||||
role=ROLE.ADMIN.value,
|
||||
is_active=True,
|
||||
).exists()
|
||||
)
|
||||
).exists()
|
||||
|
||||
|
||||
class ProjectMemberPermission(BasePermission):
|
||||
@@ -64,7 +55,7 @@ class ProjectMemberPermission(BasePermission):
|
||||
return WorkspaceMember.objects.filter(
|
||||
workspace__slug=view.workspace_slug,
|
||||
member=request.user,
|
||||
role__in=[ROLE.ADMIN.value, ROLE.MEMBER.value],
|
||||
role__in=[Admin, Member],
|
||||
is_active=True,
|
||||
).exists()
|
||||
|
||||
@@ -72,7 +63,7 @@ class ProjectMemberPermission(BasePermission):
|
||||
return ProjectMember.objects.filter(
|
||||
workspace__slug=view.workspace_slug,
|
||||
member=request.user,
|
||||
role__in=[ROLE.ADMIN.value, ROLE.MEMBER.value],
|
||||
role__in=[Admin, Member],
|
||||
project_id=view.project_id,
|
||||
is_active=True,
|
||||
).exists()
|
||||
@@ -106,7 +97,7 @@ class ProjectEntityPermission(BasePermission):
|
||||
return ProjectMember.objects.filter(
|
||||
workspace__slug=view.workspace_slug,
|
||||
member=request.user,
|
||||
role__in=[ROLE.ADMIN.value, ROLE.MEMBER.value],
|
||||
role__in=[Admin, Member],
|
||||
project_id=view.project_id,
|
||||
is_active=True,
|
||||
).exists()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from lxml import html
|
||||
|
||||
# Django imports
|
||||
from django.utils import timezone
|
||||
@@ -22,6 +23,7 @@ from plane.db.models import (
|
||||
)
|
||||
from plane.utils.content_validator import (
|
||||
validate_html_content,
|
||||
validate_json_content,
|
||||
validate_binary_data,
|
||||
)
|
||||
from plane.app.permissions import ROLE
|
||||
@@ -74,24 +76,20 @@ class DraftIssueCreateSerializer(BaseSerializer):
|
||||
raise serializers.ValidationError("Start date cannot exceed target date")
|
||||
|
||||
# Validate description content for security
|
||||
if "description_html" in attrs and attrs["description_html"]:
|
||||
is_valid, error_msg, sanitized_html = validate_html_content(
|
||||
attrs["description_html"]
|
||||
)
|
||||
if "description" in attrs and attrs["description"]:
|
||||
is_valid, error_msg = validate_json_content(attrs["description"])
|
||||
if not is_valid:
|
||||
raise serializers.ValidationError(
|
||||
{"error": "html content is not valid"}
|
||||
)
|
||||
# Update the attrs with sanitized HTML if available
|
||||
if sanitized_html is not None:
|
||||
attrs["description_html"] = sanitized_html
|
||||
raise serializers.ValidationError({"description": error_msg})
|
||||
|
||||
if "description_html" in attrs and attrs["description_html"]:
|
||||
is_valid, error_msg = validate_html_content(attrs["description_html"])
|
||||
if not is_valid:
|
||||
raise serializers.ValidationError({"description_html": error_msg})
|
||||
|
||||
if "description_binary" in attrs and attrs["description_binary"]:
|
||||
is_valid, error_msg = validate_binary_data(attrs["description_binary"])
|
||||
if not is_valid:
|
||||
raise serializers.ValidationError(
|
||||
{"description_binary": "Invalid binary data"}
|
||||
)
|
||||
raise serializers.ValidationError({"description_binary": error_msg})
|
||||
|
||||
# Validate assignees are from project
|
||||
if attrs.get("assignee_ids", []):
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from lxml import html
|
||||
|
||||
# Django imports
|
||||
from django.utils import timezone
|
||||
@@ -42,6 +43,7 @@ from plane.db.models import (
|
||||
)
|
||||
from plane.utils.content_validator import (
|
||||
validate_html_content,
|
||||
validate_json_content,
|
||||
validate_binary_data,
|
||||
)
|
||||
|
||||
@@ -126,24 +128,20 @@ class IssueCreateSerializer(BaseSerializer):
|
||||
raise serializers.ValidationError("Start date cannot exceed target date")
|
||||
|
||||
# Validate description content for security
|
||||
if "description_html" in attrs and attrs["description_html"]:
|
||||
is_valid, error_msg, sanitized_html = validate_html_content(
|
||||
attrs["description_html"]
|
||||
)
|
||||
if "description" in attrs and attrs["description"]:
|
||||
is_valid, error_msg = validate_json_content(attrs["description"])
|
||||
if not is_valid:
|
||||
raise serializers.ValidationError(
|
||||
{"error": "html content is not valid"}
|
||||
)
|
||||
# Update the attrs with sanitized HTML if available
|
||||
if sanitized_html is not None:
|
||||
attrs["description_html"] = sanitized_html
|
||||
raise serializers.ValidationError({"description": error_msg})
|
||||
|
||||
if "description_html" in attrs and attrs["description_html"]:
|
||||
is_valid, error_msg = validate_html_content(attrs["description_html"])
|
||||
if not is_valid:
|
||||
raise serializers.ValidationError({"description_html": error_msg})
|
||||
|
||||
if "description_binary" in attrs and attrs["description_binary"]:
|
||||
is_valid, error_msg = validate_binary_data(attrs["description_binary"])
|
||||
if not is_valid:
|
||||
raise serializers.ValidationError(
|
||||
{"description_binary": "Invalid binary data"}
|
||||
)
|
||||
raise serializers.ValidationError({"description_binary": error_msg})
|
||||
|
||||
# Validate assignees are from project
|
||||
if attrs.get("assignee_ids", []):
|
||||
@@ -666,33 +664,16 @@ class IssueReactionSerializer(BaseSerializer):
|
||||
|
||||
|
||||
class IssueReactionLiteSerializer(DynamicBaseSerializer):
|
||||
display_name = serializers.CharField(source="actor.display_name", read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = IssueReaction
|
||||
fields = ["id", "actor", "issue", "reaction", "display_name"]
|
||||
fields = ["id", "actor", "issue", "reaction"]
|
||||
|
||||
|
||||
class CommentReactionSerializer(BaseSerializer):
|
||||
display_name = serializers.CharField(source="actor.display_name", read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = CommentReaction
|
||||
fields = [
|
||||
"id",
|
||||
"actor",
|
||||
"comment",
|
||||
"reaction",
|
||||
"display_name",
|
||||
"deleted_at",
|
||||
"workspace",
|
||||
"project",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
]
|
||||
read_only_fields = ["workspace", "project", "comment", "actor", "deleted_at", "created_by", "updated_by"]
|
||||
fields = "__all__"
|
||||
read_only_fields = ["workspace", "project", "comment", "actor", "deleted_at"]
|
||||
|
||||
|
||||
class IssueVoteSerializer(BaseSerializer):
|
||||
|
||||
@@ -7,6 +7,7 @@ from .base import BaseSerializer
|
||||
from plane.utils.content_validator import (
|
||||
validate_binary_data,
|
||||
validate_html_content,
|
||||
validate_json_content,
|
||||
)
|
||||
from plane.db.models import (
|
||||
Page,
|
||||
@@ -228,13 +229,23 @@ class PageBinaryUpdateSerializer(serializers.Serializer):
|
||||
return value
|
||||
|
||||
# Use the validation function from utils
|
||||
is_valid, error_message, sanitized_html = validate_html_content(value)
|
||||
is_valid, error_message = validate_html_content(value)
|
||||
if not is_valid:
|
||||
raise serializers.ValidationError(error_message)
|
||||
|
||||
# Return sanitized HTML if available, otherwise return original
|
||||
return sanitized_html if sanitized_html is not None else value
|
||||
return value
|
||||
|
||||
def validate_description(self, value):
|
||||
"""Validate the JSON description"""
|
||||
if not value:
|
||||
return value
|
||||
|
||||
# Use the validation function from utils
|
||||
is_valid, error_message = validate_json_content(value)
|
||||
if not is_valid:
|
||||
raise serializers.ValidationError(error_message)
|
||||
|
||||
return value
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
"""Update the page instance with validated data"""
|
||||
|
||||
@@ -15,6 +15,8 @@ from plane.db.models import (
|
||||
)
|
||||
from plane.utils.content_validator import (
|
||||
validate_html_content,
|
||||
validate_json_content,
|
||||
validate_binary_data,
|
||||
)
|
||||
|
||||
|
||||
@@ -63,18 +65,27 @@ class ProjectSerializer(BaseSerializer):
|
||||
|
||||
def validate(self, data):
|
||||
# Validate description content for security
|
||||
if "description_html" in data and data["description_html"]:
|
||||
is_valid, error_msg, sanitized_html = validate_html_content(
|
||||
str(data["description_html"])
|
||||
)
|
||||
# Update the data with sanitized HTML if available
|
||||
if sanitized_html is not None:
|
||||
data["description_html"] = sanitized_html
|
||||
if "description" in data and data["description"]:
|
||||
# For Project, description might be text field, not JSON
|
||||
if isinstance(data["description"], dict):
|
||||
is_valid, error_msg = validate_json_content(data["description"])
|
||||
if not is_valid:
|
||||
raise serializers.ValidationError({"description": error_msg})
|
||||
|
||||
if "description_text" in data and data["description_text"]:
|
||||
is_valid, error_msg = validate_json_content(data["description_text"])
|
||||
if not is_valid:
|
||||
raise serializers.ValidationError(
|
||||
{"error": "html content is not valid"}
|
||||
raise serializers.ValidationError({"description_text": error_msg})
|
||||
|
||||
if "description_html" in data and data["description_html"]:
|
||||
if isinstance(data["description_html"], dict):
|
||||
is_valid, error_msg = validate_json_content(data["description_html"])
|
||||
else:
|
||||
is_valid, error_msg = validate_html_content(
|
||||
str(data["description_html"])
|
||||
)
|
||||
if not is_valid:
|
||||
raise serializers.ValidationError({"description_html": error_msg})
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ from plane.utils.constants import RESTRICTED_WORKSPACE_SLUGS
|
||||
from plane.utils.url import contains_url
|
||||
from plane.utils.content_validator import (
|
||||
validate_html_content,
|
||||
validate_json_content,
|
||||
validate_binary_data,
|
||||
)
|
||||
|
||||
@@ -318,24 +319,20 @@ class StickySerializer(BaseSerializer):
|
||||
|
||||
def validate(self, data):
|
||||
# Validate description content for security
|
||||
if "description_html" in data and data["description_html"]:
|
||||
is_valid, error_msg, sanitized_html = validate_html_content(
|
||||
data["description_html"]
|
||||
)
|
||||
if "description" in data and data["description"]:
|
||||
is_valid, error_msg = validate_json_content(data["description"])
|
||||
if not is_valid:
|
||||
raise serializers.ValidationError(
|
||||
{"error": "html content is not valid"}
|
||||
)
|
||||
# Update the data with sanitized HTML if available
|
||||
if sanitized_html is not None:
|
||||
data["description_html"] = sanitized_html
|
||||
raise serializers.ValidationError({"description": error_msg})
|
||||
|
||||
if "description_html" in data and data["description_html"]:
|
||||
is_valid, error_msg = validate_html_content(data["description_html"])
|
||||
if not is_valid:
|
||||
raise serializers.ValidationError({"description_html": error_msg})
|
||||
|
||||
if "description_binary" in data and data["description_binary"]:
|
||||
is_valid, error_msg = validate_binary_data(data["description_binary"])
|
||||
if not is_valid:
|
||||
raise serializers.ValidationError(
|
||||
{"description_binary": "Invalid binary data"}
|
||||
)
|
||||
raise serializers.ValidationError({"description_binary": error_msg})
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@@ -441,11 +441,7 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
|
||||
# Get the presigned URL
|
||||
storage = S3Storage(request=request)
|
||||
# Generate a presigned URL to share an S3 object
|
||||
signed_url = storage.generate_presigned_url(
|
||||
object_name=asset.asset.name,
|
||||
disposition="attachment",
|
||||
filename=asset.attributes.get("name"),
|
||||
)
|
||||
signed_url = storage.generate_presigned_url(object_name=asset.asset.name)
|
||||
# Redirect to the signed URL
|
||||
return HttpResponseRedirect(signed_url)
|
||||
|
||||
@@ -645,11 +641,7 @@ class ProjectAssetEndpoint(BaseAPIView):
|
||||
# Get the presigned URL
|
||||
storage = S3Storage(request=request)
|
||||
# Generate a presigned URL to share an S3 object
|
||||
signed_url = storage.generate_presigned_url(
|
||||
object_name=asset.asset.name,
|
||||
disposition="attachment",
|
||||
filename=asset.attributes.get("name"),
|
||||
)
|
||||
signed_url = storage.generate_presigned_url(object_name=asset.asset.name)
|
||||
# Redirect to the signed URL
|
||||
return HttpResponseRedirect(signed_url)
|
||||
|
||||
|
||||
@@ -369,13 +369,11 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
assignees__avatar_asset__isnull=False,
|
||||
then=Concat(
|
||||
Value("/api/assets/v2/static/"),
|
||||
"assignees__avatar_asset", # Assuming avatar_asset has an id or
|
||||
# relevant field
|
||||
"assignees__avatar_asset", # Assuming avatar_asset has an id or relevant field
|
||||
Value("/"),
|
||||
),
|
||||
),
|
||||
# If `avatar_asset` is None, fall back to using `avatar` field
|
||||
# directly
|
||||
# If `avatar_asset` is None, fall back to using `avatar` field directly
|
||||
When(
|
||||
assignees__avatar_asset__isnull=True,
|
||||
then="assignees__avatar",
|
||||
@@ -482,13 +480,11 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
assignees__avatar_asset__isnull=False,
|
||||
then=Concat(
|
||||
Value("/api/assets/v2/static/"),
|
||||
"assignees__avatar_asset", # Assuming avatar_asset has an id or
|
||||
# relevant field
|
||||
"assignees__avatar_asset", # Assuming avatar_asset has an id or relevant field
|
||||
Value("/"),
|
||||
),
|
||||
),
|
||||
# If `avatar_asset` is None, fall back to using `avatar` field
|
||||
# directly
|
||||
# If `avatar_asset` is None, fall back to using `avatar` field directly
|
||||
When(
|
||||
assignees__avatar_asset__isnull=True,
|
||||
then="assignees__avatar",
|
||||
|
||||
@@ -485,13 +485,11 @@ class ModuleViewSet(BaseViewSet):
|
||||
assignees__avatar_asset__isnull=False,
|
||||
then=Concat(
|
||||
Value("/api/assets/v2/static/"),
|
||||
"assignees__avatar_asset", # Assuming avatar_asset has an id or
|
||||
# relevant field
|
||||
"assignees__avatar_asset", # Assuming avatar_asset has an id or relevant field
|
||||
Value("/"),
|
||||
),
|
||||
),
|
||||
# If `avatar_asset` is None, fall back to using `avatar` field
|
||||
# directly
|
||||
# If `avatar_asset` is None, fall back to using `avatar` field directly
|
||||
When(
|
||||
assignees__avatar_asset__isnull=True,
|
||||
then="assignees__avatar",
|
||||
@@ -599,8 +597,7 @@ class ModuleViewSet(BaseViewSet):
|
||||
assignees__avatar_asset__isnull=False,
|
||||
then=Concat(
|
||||
Value("/api/assets/v2/static/"),
|
||||
"assignees__avatar_asset", # Assuming avatar_asset has an id or
|
||||
# relevant field
|
||||
"assignees__avatar_asset", # Assuming avatar_asset has an id or relevant field
|
||||
Value("/"),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -112,8 +112,7 @@ class ModuleIssueViewSet(BaseViewSet):
|
||||
if group_by == sub_group_by:
|
||||
return Response(
|
||||
{
|
||||
"error": "Group by and sub group by cannot have same "
|
||||
"parameters"
|
||||
"error": "Group by and sub group by cannot have same parameters"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# Python imports
|
||||
import json
|
||||
import base64
|
||||
from datetime import datetime
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
|
||||
@@ -163,8 +164,7 @@ class PageViewSet(BaseViewSet):
|
||||
):
|
||||
return Response(
|
||||
{
|
||||
"error": "Access cannot be updated since this page is owned by "
|
||||
"someone else"
|
||||
"error": "Access cannot be updated since this page is owned by someone else"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
@@ -198,7 +198,6 @@ class PageViewSet(BaseViewSet):
|
||||
def retrieve(self, request, slug, project_id, pk=None):
|
||||
page = self.get_queryset().filter(pk=pk).first()
|
||||
project = Project.objects.get(pk=project_id)
|
||||
track_visit = request.query_params.get("track_visit", "true").lower() == "true"
|
||||
|
||||
"""
|
||||
if the role is guest and guest_view_all_features is false and owned by is not
|
||||
@@ -231,14 +230,13 @@ class PageViewSet(BaseViewSet):
|
||||
).values_list("entity_identifier", flat=True)
|
||||
data = PageDetailSerializer(page).data
|
||||
data["issue_ids"] = issue_ids
|
||||
if track_visit:
|
||||
recent_visited_task.delay(
|
||||
slug=slug,
|
||||
entity_name="page",
|
||||
entity_identifier=pk,
|
||||
user_id=request.user.id,
|
||||
project_id=project_id,
|
||||
)
|
||||
recent_visited_task.delay(
|
||||
slug=slug,
|
||||
entity_name="page",
|
||||
entity_identifier=pk,
|
||||
user_id=request.user.id,
|
||||
project_id=project_id,
|
||||
)
|
||||
return Response(data, status=status.HTTP_200_OK)
|
||||
|
||||
@allow_permission([ROLE.ADMIN], model=Page, creator=True)
|
||||
@@ -346,8 +344,7 @@ class PageViewSet(BaseViewSet):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# if parent page is archived then the page will be un archived breaking
|
||||
# the hierarchy
|
||||
# if parent page is archived then the page will be un archived breaking the hierarchy
|
||||
if page.parent_id and page.parent.archived_at:
|
||||
page.parent = None
|
||||
page.save(update_fields=["parent"])
|
||||
|
||||
@@ -5,12 +5,13 @@ from django.utils import timezone
|
||||
import json
|
||||
|
||||
# Django imports
|
||||
from django.db import IntegrityError
|
||||
from django.db.models import Exists, F, OuterRef, Prefetch, Q, Subquery
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
|
||||
# Third Party imports
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from rest_framework import serializers, status
|
||||
from rest_framework.permissions import AllowAny
|
||||
|
||||
# Module imports
|
||||
@@ -105,10 +106,7 @@ class ProjectViewSet(BaseViewSet):
|
||||
fields = [field for field in request.GET.get("fields", "").split(",") if field]
|
||||
projects = self.get_queryset().order_by("sort_order", "name")
|
||||
if WorkspaceMember.objects.filter(
|
||||
member=request.user,
|
||||
workspace__slug=slug,
|
||||
is_active=True,
|
||||
role=ROLE.GUEST.value,
|
||||
member=request.user, workspace__slug=slug, is_active=True, role=5
|
||||
).exists():
|
||||
projects = projects.filter(
|
||||
project_projectmember__member=self.request.user,
|
||||
@@ -116,10 +114,7 @@ class ProjectViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
if WorkspaceMember.objects.filter(
|
||||
member=request.user,
|
||||
workspace__slug=slug,
|
||||
is_active=True,
|
||||
role=ROLE.MEMBER.value,
|
||||
member=request.user, workspace__slug=slug, is_active=True, role=15
|
||||
).exists():
|
||||
projects = projects.filter(
|
||||
Q(
|
||||
@@ -194,10 +189,7 @@ class ProjectViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
if WorkspaceMember.objects.filter(
|
||||
member=request.user,
|
||||
workspace__slug=slug,
|
||||
is_active=True,
|
||||
role=ROLE.GUEST.value,
|
||||
member=request.user, workspace__slug=slug, is_active=True, role=5
|
||||
).exists():
|
||||
projects = projects.filter(
|
||||
project_projectmember__member=self.request.user,
|
||||
@@ -205,10 +197,7 @@ class ProjectViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
if WorkspaceMember.objects.filter(
|
||||
member=request.user,
|
||||
workspace__slug=slug,
|
||||
is_active=True,
|
||||
role=ROLE.MEMBER.value,
|
||||
member=request.user, workspace__slug=slug, is_active=True, role=15
|
||||
).exists():
|
||||
projects = projects.filter(
|
||||
Q(
|
||||
@@ -261,9 +250,7 @@ class ProjectViewSet(BaseViewSet):
|
||||
|
||||
# Add the user as Administrator to the project
|
||||
_ = ProjectMember.objects.create(
|
||||
project_id=serializer.data["id"],
|
||||
member=request.user,
|
||||
role=ROLE.ADMIN.value,
|
||||
project_id=serializer.data["id"], member=request.user, role=20
|
||||
)
|
||||
# Also create the issue property for the user
|
||||
_ = IssueUserProperty.objects.create(
|
||||
@@ -276,7 +263,7 @@ class ProjectViewSet(BaseViewSet):
|
||||
ProjectMember.objects.create(
|
||||
project_id=serializer.data["id"],
|
||||
member_id=serializer.data["project_lead"],
|
||||
role=ROLE.ADMIN.value,
|
||||
role=20,
|
||||
)
|
||||
# Also create the issue property for the user
|
||||
IssueUserProperty.objects.create(
|
||||
@@ -354,23 +341,13 @@ class ProjectViewSet(BaseViewSet):
|
||||
|
||||
def partial_update(self, request, slug, pk=None):
|
||||
# try:
|
||||
is_workspace_admin = WorkspaceMember.objects.filter(
|
||||
member=request.user,
|
||||
workspace__slug=slug,
|
||||
is_active=True,
|
||||
role=ROLE.ADMIN.value,
|
||||
).exists()
|
||||
|
||||
is_project_admin = ProjectMember.objects.filter(
|
||||
if not ProjectMember.objects.filter(
|
||||
member=request.user,
|
||||
workspace__slug=slug,
|
||||
project_id=pk,
|
||||
role=ROLE.ADMIN.value,
|
||||
role=20,
|
||||
is_active=True,
|
||||
).exists()
|
||||
|
||||
# Return error for if the user is neither workspace admin nor project admin
|
||||
if not is_project_admin and not is_workspace_admin:
|
||||
).exists():
|
||||
return Response(
|
||||
{"error": "You don't have the required permissions."},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
@@ -425,16 +402,13 @@ class ProjectViewSet(BaseViewSet):
|
||||
def destroy(self, request, slug, pk):
|
||||
if (
|
||||
WorkspaceMember.objects.filter(
|
||||
member=request.user,
|
||||
workspace__slug=slug,
|
||||
is_active=True,
|
||||
role=ROLE.ADMIN.value,
|
||||
member=request.user, workspace__slug=slug, is_active=True, role=20
|
||||
).exists()
|
||||
or ProjectMember.objects.filter(
|
||||
member=request.user,
|
||||
workspace__slug=slug,
|
||||
project_id=pk,
|
||||
role=ROLE.ADMIN.value,
|
||||
role=20,
|
||||
is_active=True,
|
||||
).exists()
|
||||
):
|
||||
|
||||
@@ -64,8 +64,7 @@ class ProjectInvitationsViewset(BaseViewSet):
|
||||
if workspace_role in [5, 20] and workspace_role != email.get("role", 5):
|
||||
return Response(
|
||||
{
|
||||
"error": "You cannot invite a user with different role than "
|
||||
"workspace role"
|
||||
"error": "You cannot invite a user with different role than workspace role"
|
||||
}
|
||||
)
|
||||
|
||||
@@ -92,8 +91,7 @@ class ProjectInvitationsViewset(BaseViewSet):
|
||||
except ValidationError:
|
||||
return Response(
|
||||
{
|
||||
"error": f"Invalid email - {email} provided a valid email address "
|
||||
f"is required to send the invite"
|
||||
"error": f"Invalid email - {email} provided a valid email address is required to send the invite"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
@@ -236,8 +234,7 @@ class ProjectJoinEndpoint(BaseAPIView):
|
||||
workspace_member.is_active = True
|
||||
workspace_member.save()
|
||||
|
||||
# Check if the user was already a member of project then activate
|
||||
# the user
|
||||
# Check if the user was already a member of project then activate the user
|
||||
project_member = ProjectMember.objects.filter(
|
||||
workspace_id=project_invite.workspace_id, member=user
|
||||
).first()
|
||||
|
||||
@@ -39,8 +39,7 @@ class ProjectMemberViewSet(BaseViewSet):
|
||||
|
||||
@allow_permission([ROLE.ADMIN])
|
||||
def create(self, request, slug, project_id):
|
||||
# Get the list of members to be added to the project and their roles
|
||||
# i.e. the user_id and the role
|
||||
# Get the list of members to be added to the project and their roles i.e. the user_id and the role
|
||||
members = request.data.get("members", [])
|
||||
|
||||
# get the project
|
||||
@@ -70,8 +69,7 @@ class ProjectMemberViewSet(BaseViewSet):
|
||||
if workspace_member_role in [20] and member_roles.get(member) in [5, 15]:
|
||||
return Response(
|
||||
{
|
||||
"error": "You cannot add a user with role lower than the "
|
||||
"workspace role"
|
||||
"error": "You cannot add a user with role lower than the workspace role"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
@@ -79,14 +77,12 @@ class ProjectMemberViewSet(BaseViewSet):
|
||||
if workspace_member_role in [5] and member_roles.get(member) in [15, 20]:
|
||||
return Response(
|
||||
{
|
||||
"error": "You cannot add a user with role higher than the "
|
||||
"workspace role"
|
||||
"error": "You cannot add a user with role higher than the workspace role"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Update roles in the members array based on the member_roles dictionary
|
||||
# and set is_active to True
|
||||
# Update roles in the members array based on the member_roles dictionary and set is_active to True
|
||||
for project_member in ProjectMember.objects.filter(
|
||||
project_id=project_id,
|
||||
member_id__in=[member.get("member_id") for member in members],
|
||||
@@ -257,8 +253,7 @@ class ProjectMemberViewSet(BaseViewSet):
|
||||
if str(project_member.id) == str(requesting_project_member.id):
|
||||
return Response(
|
||||
{
|
||||
"error": "You cannot remove yourself from the workspace. Please use "
|
||||
"leave workspace"
|
||||
"error": "You cannot remove yourself from the workspace. Please use leave workspace"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
@@ -292,9 +287,7 @@ class ProjectMemberViewSet(BaseViewSet):
|
||||
):
|
||||
return Response(
|
||||
{
|
||||
"error": "You cannot leave the project as your the only admin of the "
|
||||
"project you will have to either delete the project or create an "
|
||||
"another admin"
|
||||
"error": "You cannot leave the project as your the only admin of the project you will have to either delete the project or create an another admin"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@@ -79,8 +79,7 @@ class UserEndpoint(BaseViewSet):
|
||||
if InstanceAdmin.objects.filter(user=user).exists():
|
||||
return Response(
|
||||
{
|
||||
"error": "You cannot deactivate your account since you are an "
|
||||
"instance admin"
|
||||
"error": "You cannot deactivate your account since you are an instance admin"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
@@ -108,8 +107,7 @@ class UserEndpoint(BaseViewSet):
|
||||
else:
|
||||
return Response(
|
||||
{
|
||||
"error": "You cannot deactivate account as you are the only admin "
|
||||
"in some projects."
|
||||
"error": "You cannot deactivate account as you are the only admin in some projects."
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
@@ -134,8 +132,7 @@ class UserEndpoint(BaseViewSet):
|
||||
else:
|
||||
return Response(
|
||||
{
|
||||
"error": "You cannot deactivate account as you are the only admin "
|
||||
"in some workspaces."
|
||||
"error": "You cannot deactivate account as you are the only admin in some workspaces."
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@@ -147,8 +147,7 @@ class WorkspaceViewViewSet(BaseViewSet):
|
||||
class WorkspaceViewIssuesViewSet(BaseViewSet):
|
||||
def _get_project_permission_filters(self):
|
||||
"""
|
||||
Get common project permission filters for guest users and role-based
|
||||
access control.
|
||||
Get common project permission filters for guest users and role-based access control.
|
||||
Returns Q object for filtering issues based on user role and project settings.
|
||||
"""
|
||||
return Q(
|
||||
|
||||
@@ -39,6 +39,9 @@ from plane.db.models import (
|
||||
Profile,
|
||||
)
|
||||
from plane.app.permissions import ROLE, allow_permission
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.cache import cache_control
|
||||
from django.views.decorators.vary import vary_on_cookie
|
||||
from plane.utils.constants import RESTRICTED_WORKSPACE_SLUGS
|
||||
from plane.license.utils.instance_value import get_configuration_value
|
||||
from plane.bgtasks.workspace_seed_task import workspace_seed
|
||||
@@ -390,8 +393,7 @@ class ExportWorkspaceUserActivityEndpoint(BaseAPIView):
|
||||
rows = [
|
||||
(
|
||||
activity.actor.display_name,
|
||||
f"{activity.project.identifier} - "
|
||||
f"{activity.issue.sequence_id if activity.issue else ''}",
|
||||
f"{activity.project.identifier} - {activity.issue.sequence_id if activity.issue else ''}",
|
||||
activity.project.name,
|
||||
activity.created_at,
|
||||
activity.updated_at,
|
||||
|
||||
@@ -172,14 +172,12 @@ class WorkspaceDraftIssueViewSet(BaseViewSet):
|
||||
{"error": "Issue not found"}, status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
project_id = request.data.get("project_id", issue.project_id)
|
||||
|
||||
serializer = DraftIssueCreateSerializer(
|
||||
issue,
|
||||
data=request.data,
|
||||
partial=True,
|
||||
context={
|
||||
"project_id": project_id,
|
||||
"project_id": request.data.get("project_id", None),
|
||||
"cycle_id": request.data.get("cycle_id", "not_provided"),
|
||||
},
|
||||
)
|
||||
|
||||
@@ -113,8 +113,7 @@ class WorkspaceInvitationsViewset(BaseViewSet):
|
||||
except ValidationError:
|
||||
return Response(
|
||||
{
|
||||
"error": f"Invalid email - {email} provided a valid email address "
|
||||
f"is required to send the invite"
|
||||
"error": f"Invalid email - {email} provided a valid email address is required to send the invite"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
@@ -186,8 +185,7 @@ class WorkspaceJoinEndpoint(BaseAPIView):
|
||||
|
||||
# If the user is present then create the workspace member
|
||||
if user is not None:
|
||||
# Check if the user was already a member of workspace then
|
||||
# activate the user
|
||||
# Check if the user was already a member of workspace then activate the user
|
||||
workspace_member = WorkspaceMember.objects.filter(
|
||||
workspace=workspace_invite.workspace, member=user
|
||||
).first()
|
||||
@@ -264,8 +262,7 @@ class UserWorkspaceInvitationsViewSet(BaseViewSet):
|
||||
pk__in=invitations, email=request.user.email
|
||||
).order_by("-created_at")
|
||||
|
||||
# If the user is already a member of workspace and was deactivated then
|
||||
# activate the user
|
||||
# If the user is already a member of workspace and was deactivated then activate the user
|
||||
for invitation in workspace_invitations:
|
||||
invalidate_cache_directly(
|
||||
path=f"/api/workspaces/{invitation.workspace.slug}/members/",
|
||||
|
||||
@@ -99,8 +99,7 @@ class WorkSpaceMemberViewSet(BaseViewSet):
|
||||
if str(workspace_member.id) == str(requesting_workspace_member.id):
|
||||
return Response(
|
||||
{
|
||||
"error": "You cannot remove yourself from the workspace. Please use "
|
||||
"leave workspace"
|
||||
"error": "You cannot remove yourself from the workspace. Please use leave workspace"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
@@ -127,9 +126,7 @@ class WorkSpaceMemberViewSet(BaseViewSet):
|
||||
):
|
||||
return Response(
|
||||
{
|
||||
"error": "User is a part of some projects where they are the only "
|
||||
"admin, they should either leave that project or promote another "
|
||||
"user to admin."
|
||||
"error": "User is a part of some projects where they are the only admin, they should either leave that project or promote another user to admin."
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
@@ -169,9 +166,7 @@ class WorkSpaceMemberViewSet(BaseViewSet):
|
||||
):
|
||||
return Response(
|
||||
{
|
||||
"error": "You cannot leave the workspace as you are the only admin of "
|
||||
"the workspace you will have to either delete the workspace or "
|
||||
"promote another user to admin."
|
||||
"error": "You cannot leave the workspace as you are the only admin of the workspace you will have to either delete the workspace or promote another user to admin."
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
@@ -192,9 +187,7 @@ class WorkSpaceMemberViewSet(BaseViewSet):
|
||||
):
|
||||
return Response(
|
||||
{
|
||||
"error": "You are a part of some projects where you are the only "
|
||||
"admin, you should either leave the project or promote another "
|
||||
"user to admin."
|
||||
"error": "You are a part of some projects where you are the only admin, you should either leave the project or promote another user to admin."
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@@ -7,6 +7,7 @@ from plane.app.serializers import StateSerializer
|
||||
from plane.app.views.base import BaseAPIView
|
||||
from plane.db.models import State
|
||||
from plane.app.permissions import WorkspaceEntityPermission
|
||||
from plane.utils.cache import cache_response
|
||||
from collections import defaultdict
|
||||
|
||||
|
||||
@@ -14,6 +15,7 @@ class WorkspaceStatesEndpoint(BaseAPIView):
|
||||
permission_classes = [WorkspaceEntityPermission]
|
||||
use_read_replica = True
|
||||
|
||||
@cache_response(60 * 60 * 2)
|
||||
def get(self, request, slug):
|
||||
states = State.objects.filter(
|
||||
workspace__slug=slug,
|
||||
|
||||
@@ -157,8 +157,7 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
|
||||
if group_by == sub_group_by:
|
||||
return Response(
|
||||
{
|
||||
"error": "Group by and sub group by cannot have same "
|
||||
"parameters"
|
||||
"error": "Group by and sub group by cannot have same parameters"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@@ -8,8 +8,7 @@ from plane.utils.cache import invalidate_cache_directly
|
||||
|
||||
|
||||
def process_workspace_project_invitations(user):
|
||||
"""This function takes in User and adds him to all workspace and projects
|
||||
that the user has accepted invited of"""
|
||||
"""This function takes in User and adds him to all workspace and projects that the user has accepted invited of"""
|
||||
|
||||
# Check if user has any accepted invites for workspace and add them to workspace
|
||||
workspace_member_invites = WorkspaceMemberInvite.objects.filter(
|
||||
|
||||
@@ -107,8 +107,7 @@ class MagicSignInEndpoint(View):
|
||||
# Login the user and record his device info
|
||||
user_login(request=request, user=user, is_app=True)
|
||||
if user.is_password_autoset and profile.is_onboarded:
|
||||
# Redirect to the home page
|
||||
path = "/"
|
||||
path = "accounts/set-password"
|
||||
else:
|
||||
# Get the redirection path
|
||||
path = (
|
||||
|
||||
@@ -108,8 +108,7 @@ class SetUserPasswordEndpoint(APIView):
|
||||
error_code=AUTHENTICATION_ERROR_CODES["PASSWORD_ALREADY_SET"],
|
||||
error_message="PASSWORD_ALREADY_SET",
|
||||
payload={
|
||||
"error": "Your password is already set please change your password "
|
||||
"from profile"
|
||||
"error": "Your password is already set please change your password from profile"
|
||||
},
|
||||
)
|
||||
return Response(exc.get_error_dict(), status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@@ -95,8 +95,7 @@ class SignInAuthSpaceEndpoint(View):
|
||||
# Login the user and record his device info
|
||||
user_login(request=request, user=user, is_space=True)
|
||||
# redirect to next path
|
||||
base_url = base_host(request=request, is_space=True)
|
||||
url = f"{base_url}{str(next_path) if next_path else ''}"
|
||||
url = f"{base_host(request=request, is_space=True)}{str(next_path) if next_path else ''}"
|
||||
return HttpResponseRedirect(url)
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
@@ -180,8 +179,7 @@ class SignUpAuthSpaceEndpoint(View):
|
||||
# Login the user and record his device info
|
||||
user_login(request=request, user=user, is_space=True)
|
||||
# redirect to referer path
|
||||
base_url = base_host(request=request, is_space=True)
|
||||
url = f"{base_url}{str(next_path) if next_path else ''}"
|
||||
url = f"{base_host(request=request, is_space=True)}{str(next_path) if next_path else ''}"
|
||||
return HttpResponseRedirect(url)
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
|
||||
@@ -89,8 +89,7 @@ class GitHubCallbackSpaceEndpoint(View):
|
||||
user_login(request=request, user=user, is_space=True)
|
||||
# Process workspace and project invitations
|
||||
# redirect to referer path
|
||||
base_url = base_host(request=request, is_space=True)
|
||||
url = f"{base_url}{str(next_path) if next_path else ''}"
|
||||
url = f"{base_host(request=request, is_space=True)}{str(next_path) if next_path else ''}"
|
||||
return HttpResponseRedirect(url)
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
|
||||
@@ -89,8 +89,7 @@ class GitLabCallbackSpaceEndpoint(View):
|
||||
user_login(request=request, user=user, is_space=True)
|
||||
# Process workspace and project invitations
|
||||
# redirect to referer path
|
||||
base_url = base_host(request=request, is_space=True)
|
||||
url = f"{base_url}{str(next_path) if next_path else ''}"
|
||||
url = f"{base_host(request=request, is_space=True)}{str(next_path) if next_path else ''}"
|
||||
return HttpResponseRedirect(url)
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
|
||||
@@ -85,8 +85,7 @@ class GoogleCallbackSpaceEndpoint(View):
|
||||
# Login the user and record his device info
|
||||
user_login(request=request, user=user, is_space=True)
|
||||
# redirect to referer path
|
||||
base_url = base_host(request=request, is_space=True)
|
||||
url = f"{base_url}{str(next_path) if next_path else ''}"
|
||||
url = f"{base_host(request=request, is_space=True)}{str(next_path) if next_path else ''}"
|
||||
return HttpResponseRedirect(url)
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
|
||||
@@ -146,8 +146,7 @@ class MagicSignUpSpaceEndpoint(View):
|
||||
# Login the user and record his device info
|
||||
user_login(request=request, user=user, is_space=True)
|
||||
# redirect to referer path
|
||||
base_url = base_host(request=request, is_space=True)
|
||||
url = f"{base_url}{str(next_path) if next_path else ''}"
|
||||
url = f"{base_host(request=request, is_space=True)}{str(next_path) if next_path else ''}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
except AuthenticationException as e:
|
||||
|
||||
@@ -120,8 +120,7 @@ class ResetPasswordSpaceEndpoint(View):
|
||||
error_message="INVALID_PASSWORD_TOKEN",
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
base_url = base_host(request=request, is_space=True)
|
||||
url = f"{base_url}/accounts/reset-password/?{urlencode(params)}"
|
||||
url = f"{base_host(request=request, is_space=True)}/accounts/reset-password/?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
password = request.POST.get("password", False)
|
||||
@@ -131,8 +130,7 @@ class ResetPasswordSpaceEndpoint(View):
|
||||
error_code=AUTHENTICATION_ERROR_CODES["INVALID_PASSWORD"],
|
||||
error_message="INVALID_PASSWORD",
|
||||
)
|
||||
base_url = base_host(request=request, is_space=True)
|
||||
url = f"{base_url}/accounts/reset-password/?{urlencode(exc.get_error_dict())}"
|
||||
url = f"{base_host(request=request, is_space=True)}/accounts/reset-password/?{urlencode(exc.get_error_dict())}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
# Check the password complexity
|
||||
@@ -142,8 +140,7 @@ class ResetPasswordSpaceEndpoint(View):
|
||||
error_code=AUTHENTICATION_ERROR_CODES["INVALID_PASSWORD"],
|
||||
error_message="INVALID_PASSWORD",
|
||||
)
|
||||
base_url = base_host(request=request, is_space=True)
|
||||
url = f"{base_url}/accounts/reset-password/?{urlencode(exc.get_error_dict())}"
|
||||
url = f"{base_host(request=request, is_space=True)}/accounts/reset-password/?{urlencode(exc.get_error_dict())}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
# set_password also hashes the password that the user will get
|
||||
|
||||
@@ -22,10 +22,8 @@ class SignOutAuthSpaceEndpoint(View):
|
||||
user.save()
|
||||
# Log the user out
|
||||
logout(request)
|
||||
base_url = base_host(request=request, is_space=True)
|
||||
url = f"{base_url}{str(validate_next_path(next_path)) if next_path else ''}"
|
||||
url = f"{base_host(request=request, is_space=True)}{str(validate_next_path(next_path)) if next_path else ''}"
|
||||
return HttpResponseRedirect(url)
|
||||
except Exception:
|
||||
base_url = base_host(request=request, is_space=True)
|
||||
url = f"{base_url}{str(validate_next_path(next_path)) if next_path else ''}"
|
||||
url = f"{base_host(request=request, is_space=True)}{str(validate_next_path(next_path)) if next_path else ''}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -67,7 +67,7 @@ def flush_to_mongo_and_delete(
|
||||
mongo_archival_failed = False
|
||||
|
||||
# Try to insert into MongoDB if available
|
||||
if mongo_collection is not None and mongo_available:
|
||||
if mongo_collection and mongo_available:
|
||||
try:
|
||||
mongo_collection.bulk_write([InsertOne(doc) for doc in buffer])
|
||||
except BulkWriteError as bwe:
|
||||
@@ -166,9 +166,9 @@ def process_cleanup_task(
|
||||
def transform_api_log(record: Dict) -> Dict:
|
||||
"""Transform API activity log record."""
|
||||
return {
|
||||
"id": str(record["id"]),
|
||||
"id": record["id"],
|
||||
"created_at": str(record["created_at"]) if record.get("created_at") else None,
|
||||
"token_identifier": str(record["token_identifier"]),
|
||||
"token_identifier": record["token_identifier"],
|
||||
"path": record["path"],
|
||||
"method": record["method"],
|
||||
"query_params": record.get("query_params"),
|
||||
@@ -178,18 +178,18 @@ def transform_api_log(record: Dict) -> Dict:
|
||||
"response_body": record["response_body"],
|
||||
"ip_address": record["ip_address"],
|
||||
"user_agent": record["user_agent"],
|
||||
"created_by_id": str(record["created_by_id"]),
|
||||
"created_by_id": record["created_by_id"],
|
||||
}
|
||||
|
||||
|
||||
def transform_email_log(record: Dict) -> Dict:
|
||||
"""Transform email notification log record."""
|
||||
return {
|
||||
"id": str(record["id"]),
|
||||
"id": record["id"],
|
||||
"created_at": str(record["created_at"]) if record.get("created_at") else None,
|
||||
"receiver_id": str(record["receiver_id"]),
|
||||
"triggered_by_id": str(record["triggered_by_id"]),
|
||||
"entity_identifier": str(record["entity_identifier"]),
|
||||
"receiver_id": record["receiver_id"],
|
||||
"triggered_by_id": record["triggered_by_id"],
|
||||
"entity_identifier": record["entity_identifier"],
|
||||
"entity_name": record["entity_name"],
|
||||
"data": record["data"],
|
||||
"processed_at": (
|
||||
@@ -197,27 +197,27 @@ def transform_email_log(record: Dict) -> Dict:
|
||||
),
|
||||
"sent_at": str(record["sent_at"]) if record.get("sent_at") else None,
|
||||
"entity": record["entity"],
|
||||
"old_value": str(record["old_value"]),
|
||||
"new_value": str(record["new_value"]),
|
||||
"created_by_id": str(record["created_by_id"]),
|
||||
"old_value": record["old_value"],
|
||||
"new_value": record["new_value"],
|
||||
"created_by_id": record["created_by_id"],
|
||||
}
|
||||
|
||||
|
||||
def transform_page_version(record: Dict) -> Dict:
|
||||
"""Transform page version record."""
|
||||
return {
|
||||
"id": str(record["id"]),
|
||||
"id": record["id"],
|
||||
"created_at": str(record["created_at"]) if record.get("created_at") else None,
|
||||
"page_id": str(record["page_id"]),
|
||||
"workspace_id": str(record["workspace_id"]),
|
||||
"owned_by_id": str(record["owned_by_id"]),
|
||||
"page_id": record["page_id"],
|
||||
"workspace_id": record["workspace_id"],
|
||||
"owned_by_id": record["owned_by_id"],
|
||||
"description_html": record["description_html"],
|
||||
"description_binary": record["description_binary"],
|
||||
"description_stripped": record["description_stripped"],
|
||||
"description_json": record["description_json"],
|
||||
"sub_pages_data": record["sub_pages_data"],
|
||||
"created_by_id": str(record["created_by_id"]),
|
||||
"updated_by_id": str(record["updated_by_id"]),
|
||||
"created_by_id": record["created_by_id"],
|
||||
"updated_by_id": record["updated_by_id"],
|
||||
"deleted_at": str(record["deleted_at"]) if record.get("deleted_at") else None,
|
||||
"last_saved_at": (
|
||||
str(record["last_saved_at"]) if record.get("last_saved_at") else None
|
||||
@@ -228,14 +228,14 @@ def transform_page_version(record: Dict) -> Dict:
|
||||
def transform_issue_description_version(record: Dict) -> Dict:
|
||||
"""Transform issue description version record."""
|
||||
return {
|
||||
"id": str(record["id"]),
|
||||
"id": record["id"],
|
||||
"created_at": str(record["created_at"]) if record.get("created_at") else None,
|
||||
"issue_id": str(record["issue_id"]),
|
||||
"workspace_id": str(record["workspace_id"]),
|
||||
"project_id": str(record["project_id"]),
|
||||
"created_by_id": str(record["created_by_id"]),
|
||||
"updated_by_id": str(record["updated_by_id"]),
|
||||
"owned_by_id": str(record["owned_by_id"]),
|
||||
"issue_id": record["issue_id"],
|
||||
"workspace_id": record["workspace_id"],
|
||||
"project_id": record["project_id"],
|
||||
"created_by_id": record["created_by_id"],
|
||||
"updated_by_id": record["updated_by_id"],
|
||||
"owned_by_id": record["owned_by_id"],
|
||||
"last_saved_at": (
|
||||
str(record["last_saved_at"]) if record.get("last_saved_at") else None
|
||||
),
|
||||
|
||||
@@ -30,8 +30,6 @@ def page_version(page_id, existing_instance, user_id):
|
||||
description_binary=page.description_binary,
|
||||
owned_by_id=user_id,
|
||||
last_saved_at=page.updated_at,
|
||||
description_json=page.description,
|
||||
description_stripped=page.description_stripped,
|
||||
)
|
||||
|
||||
# If page versions are greater than 20 delete the oldest one
|
||||
|
||||
@@ -436,7 +436,7 @@ def webhook_activity(
|
||||
def model_activity(
|
||||
model_name, model_id, requested_data, current_instance, actor_id, slug, origin=None
|
||||
):
|
||||
"""Function takes in two json and computes differences between keys of both the json."""
|
||||
"""Function takes in two json and computes differences between keys of both the json"""
|
||||
if current_instance is None:
|
||||
webhook_activity.delay(
|
||||
event=model_name,
|
||||
@@ -458,8 +458,7 @@ def model_activity(
|
||||
json.loads(current_instance) if current_instance is not None else None
|
||||
)
|
||||
|
||||
# Loop through all keys in requested data and check the current value and
|
||||
# requested value
|
||||
# Loop through all keys in requested data and check the current value and requested value
|
||||
for key in requested_data:
|
||||
# Check if key is present in current instance or not
|
||||
if key in current_instance:
|
||||
|
||||
@@ -76,8 +76,7 @@ def create_project_and_member(workspace: Workspace) -> Dict[int, uuid.UUID]:
|
||||
|
||||
if not project_seeds:
|
||||
logger.warning(
|
||||
"Task: workspace_seed_task -> No project seeds found. "
|
||||
"Skipping project creation."
|
||||
"Task: workspace_seed_task -> No project seeds found. Skipping project creation."
|
||||
)
|
||||
return projects_map
|
||||
|
||||
@@ -93,10 +92,6 @@ def create_project_and_member(workspace: Workspace) -> Dict[int, uuid.UUID]:
|
||||
name=workspace.name, # Use workspace name
|
||||
identifier=project_identifier,
|
||||
created_by_id=workspace.created_by_id,
|
||||
# Enable all views in seed data
|
||||
cycle_view=True,
|
||||
module_view=True,
|
||||
issue_views_view=True,
|
||||
)
|
||||
|
||||
# Create project members
|
||||
@@ -236,8 +231,7 @@ def create_project_issues(
|
||||
for field in required_fields:
|
||||
if field not in issue_seed:
|
||||
logger.error(
|
||||
f"Task: workspace_seed_task -> Required field '{field}' "
|
||||
f"missing in issue seed"
|
||||
f"Task: workspace_seed_task -> Required field '{field}' missing in issue seed"
|
||||
)
|
||||
continue
|
||||
|
||||
@@ -320,7 +314,6 @@ def workspace_seed(workspace_id: uuid.UUID) -> None:
|
||||
return
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Task: workspace_seed_task -> Failed to seed workspace "
|
||||
f"{workspace_id}: {str(e)}"
|
||||
f"Task: workspace_seed_task -> Failed to seed workspace {workspace_id}: {str(e)}"
|
||||
)
|
||||
raise e
|
||||
|
||||
@@ -62,8 +62,7 @@ class Command(BaseCommand):
|
||||
# Access to the bucket is forbidden
|
||||
self.stdout.write(
|
||||
self.style.ERROR(
|
||||
f"Access to the bucket '{bucket_name}' is forbidden. "
|
||||
f"Check permissions."
|
||||
f"Access to the bucket '{bucket_name}' is forbidden. Check permissions."
|
||||
)
|
||||
)
|
||||
else:
|
||||
|
||||
@@ -61,8 +61,7 @@ class Command(BaseCommand):
|
||||
)
|
||||
)
|
||||
with transaction.atomic():
|
||||
# This ensures only one transaction per project can execute this
|
||||
# code at a time
|
||||
# This ensures only one transaction per project can execute this code at a time
|
||||
lock_key = convert_uuid_to_integer(project.id)
|
||||
|
||||
# Acquire an exclusive lock using the project ID as the lock key
|
||||
|
||||
@@ -37,8 +37,7 @@ class Command(BaseCommand):
|
||||
if workspace.deleted_at is None:
|
||||
self.stdout.write(
|
||||
self.style.WARNING(
|
||||
f"Workspace '{workspace.name}' (slug: {workspace.slug}) "
|
||||
f"is not deleted."
|
||||
f"Workspace '{workspace.name}' (slug: {workspace.slug}) is not deleted."
|
||||
)
|
||||
)
|
||||
return
|
||||
@@ -47,8 +46,7 @@ class Command(BaseCommand):
|
||||
if "__" in workspace.slug and workspace.slug.split("__")[-1].isdigit():
|
||||
self.stdout.write(
|
||||
self.style.WARNING(
|
||||
f"Workspace '{workspace.name}' (slug: {workspace.slug}) "
|
||||
f"already has a timestamp appended."
|
||||
f"Workspace '{workspace.name}' (slug: {workspace.slug}) already has a timestamp appended."
|
||||
)
|
||||
)
|
||||
return
|
||||
@@ -61,8 +59,7 @@ class Command(BaseCommand):
|
||||
|
||||
if dry_run:
|
||||
self.stdout.write(
|
||||
f"Would update workspace '{workspace.name}' slug from "
|
||||
f"'{workspace.slug}' to '{new_slug}'"
|
||||
f"Would update workspace '{workspace.name}' slug from '{workspace.slug}' to '{new_slug}'"
|
||||
)
|
||||
else:
|
||||
try:
|
||||
@@ -71,8 +68,7 @@ class Command(BaseCommand):
|
||||
workspace.save(update_fields=["slug"])
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f"Updated workspace '{workspace.name}' slug from "
|
||||
f"'{workspace.slug}' to '{new_slug}'"
|
||||
f"Updated workspace '{workspace.name}' slug from '{workspace.slug}' to '{new_slug}'"
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
# Generated by Django 4.2.22 on 2025-08-29 11:31
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("db", "0101_description_descriptionversion"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="page",
|
||||
name="sort_order",
|
||||
field=models.FloatField(default=65535),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="pagelog",
|
||||
name="entity_type",
|
||||
field=models.CharField(
|
||||
blank=True, max_length=30, null=True, verbose_name="Entity Type"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="pagelog",
|
||||
name="entity_identifier",
|
||||
field=models.UUIDField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
@@ -1,75 +0,0 @@
|
||||
# Generated by Django 4.2.22 on 2025-09-01 14:33
|
||||
|
||||
from django.db import migrations, models
|
||||
from django.contrib.postgres.operations import AddIndexConcurrently
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
atomic = False
|
||||
|
||||
dependencies = [
|
||||
('db', '0102_page_sort_order_pagelog_entity_type_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
AddIndexConcurrently(
|
||||
model_name='fileasset',
|
||||
index=models.Index(fields=['entity_type'], name='asset_entity_type_idx'),
|
||||
),
|
||||
AddIndexConcurrently(
|
||||
model_name='fileasset',
|
||||
index=models.Index(fields=['entity_identifier'], name='asset_entity_identifier_idx'),
|
||||
),
|
||||
AddIndexConcurrently(
|
||||
model_name='fileasset',
|
||||
index=models.Index(fields=['entity_type', 'entity_identifier'], name='asset_entity_idx'),
|
||||
),
|
||||
AddIndexConcurrently(
|
||||
model_name='notification',
|
||||
index=models.Index(fields=['entity_identifier'], name='notif_entity_identifier_idx'),
|
||||
),
|
||||
AddIndexConcurrently(
|
||||
model_name='notification',
|
||||
index=models.Index(fields=['entity_name'], name='notif_entity_name_idx'),
|
||||
),
|
||||
AddIndexConcurrently(
|
||||
model_name='notification',
|
||||
index=models.Index(fields=['read_at'], name='notif_read_at_idx'),
|
||||
),
|
||||
AddIndexConcurrently(
|
||||
model_name='notification',
|
||||
index=models.Index(fields=['receiver', 'read_at'], name='notif_entity_idx'),
|
||||
),
|
||||
AddIndexConcurrently(
|
||||
model_name='pagelog',
|
||||
index=models.Index(fields=['entity_type'], name='pagelog_entity_type_idx'),
|
||||
),
|
||||
AddIndexConcurrently(
|
||||
model_name='pagelog',
|
||||
index=models.Index(fields=['entity_identifier'], name='pagelog_entity_id_idx'),
|
||||
),
|
||||
AddIndexConcurrently(
|
||||
model_name='pagelog',
|
||||
index=models.Index(fields=['entity_name'], name='pagelog_entity_name_idx'),
|
||||
),
|
||||
AddIndexConcurrently(
|
||||
model_name='pagelog',
|
||||
index=models.Index(fields=['entity_type', 'entity_identifier'], name='pagelog_type_id_idx'),
|
||||
),
|
||||
AddIndexConcurrently(
|
||||
model_name='pagelog',
|
||||
index=models.Index(fields=['entity_name', 'entity_identifier'], name='pagelog_name_id_idx'),
|
||||
),
|
||||
AddIndexConcurrently(
|
||||
model_name='userfavorite',
|
||||
index=models.Index(fields=['entity_type'], name='fav_entity_type_idx'),
|
||||
),
|
||||
AddIndexConcurrently(
|
||||
model_name='userfavorite',
|
||||
index=models.Index(fields=['entity_identifier'], name='fav_entity_identifier_idx'),
|
||||
),
|
||||
AddIndexConcurrently(
|
||||
model_name='userfavorite',
|
||||
index=models.Index(fields=['entity_type', 'entity_identifier'], name='fav_entity_idx'),
|
||||
),
|
||||
]
|
||||
@@ -1,43 +0,0 @@
|
||||
# Generated by Django 4.2.22 on 2025-09-03 05:18
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0103_fileasset_asset_entity_type_idx_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='cycleuserproperties',
|
||||
name='rich_filters',
|
||||
field=models.JSONField(default=dict),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='exporterhistory',
|
||||
name='rich_filters',
|
||||
field=models.JSONField(blank=True, default=dict, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='issueuserproperty',
|
||||
name='rich_filters',
|
||||
field=models.JSONField(default=dict),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='issueview',
|
||||
name='rich_filters',
|
||||
field=models.JSONField(default=dict),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='moduleuserproperties',
|
||||
name='rich_filters',
|
||||
field=models.JSONField(default=dict),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='workspaceuserproperties',
|
||||
name='rich_filters',
|
||||
field=models.JSONField(default=dict),
|
||||
),
|
||||
]
|
||||
@@ -1,33 +0,0 @@
|
||||
# Generated by Django 4.2.22 on 2025-09-10 09:54
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("db", "0104_cycleuserproperties_rich_filters_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="project",
|
||||
name="cycle_view",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="project",
|
||||
name="issue_views_view",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="project",
|
||||
name="module_view",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="session",
|
||||
name="user_id",
|
||||
field=models.CharField(db_index=True, max_length=50, null=True),
|
||||
),
|
||||
]
|
||||
@@ -76,15 +76,6 @@ class FileAsset(BaseModel):
|
||||
verbose_name_plural = "File Assets"
|
||||
db_table = "file_assets"
|
||||
ordering = ("-created_at",)
|
||||
indexes = [
|
||||
models.Index(fields=["entity_type"], name="asset_entity_type_idx"),
|
||||
models.Index(
|
||||
fields=["entity_identifier"], name="asset_entity_identifier_idx"
|
||||
),
|
||||
models.Index(
|
||||
fields=["entity_type", "entity_identifier"], name="asset_entity_idx"
|
||||
),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return str(self.asset)
|
||||
@@ -100,10 +91,7 @@ class FileAsset(BaseModel):
|
||||
return f"/api/assets/v2/static/{self.id}/"
|
||||
|
||||
if self.entity_type == self.EntityTypeContext.ISSUE_ATTACHMENT:
|
||||
return (
|
||||
f"/api/assets/v2/workspaces/{self.workspace.slug}/projects/"
|
||||
f"{self.project_id}/issues/{self.issue_id}/attachments/{self.id}/"
|
||||
)
|
||||
return f"/api/assets/v2/workspaces/{self.workspace.slug}/projects/{self.project_id}/issues/{self.issue_id}/attachments/{self.id}/"
|
||||
|
||||
if self.entity_type in [
|
||||
self.EntityTypeContext.ISSUE_DESCRIPTION,
|
||||
@@ -111,9 +99,6 @@ class FileAsset(BaseModel):
|
||||
self.EntityTypeContext.PAGE_DESCRIPTION,
|
||||
self.EntityTypeContext.DRAFT_ISSUE_DESCRIPTION,
|
||||
]:
|
||||
return (
|
||||
f"/api/assets/v2/workspaces/{self.workspace.slug}/projects/"
|
||||
f"{self.project_id}/{self.id}/"
|
||||
)
|
||||
return f"/api/assets/v2/workspaces/{self.workspace.slug}/projects/{self.project_id}/{self.id}/"
|
||||
|
||||
return None
|
||||
|
||||
@@ -139,7 +139,6 @@ class CycleUserProperties(ProjectBaseModel):
|
||||
filters = models.JSONField(default=get_default_filters)
|
||||
display_filters = models.JSONField(default=get_default_display_filters)
|
||||
display_properties = models.JSONField(default=get_default_display_properties)
|
||||
rich_filters = models.JSONField(default=dict)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["cycle", "user", "deleted_at"]
|
||||
|
||||
@@ -56,7 +56,6 @@ class ExporterHistory(BaseModel):
|
||||
related_name="workspace_exporters",
|
||||
)
|
||||
filters = models.JSONField(blank=True, null=True)
|
||||
rich_filters = models.JSONField(default=dict, blank=True, null=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Exporter"
|
||||
|
||||
@@ -41,15 +41,6 @@ class UserFavorite(WorkspaceBaseModel):
|
||||
verbose_name_plural = "User Favorites"
|
||||
db_table = "user_favorites"
|
||||
ordering = ("-created_at",)
|
||||
indexes = [
|
||||
models.Index(fields=["entity_type"], name="fav_entity_type_idx"),
|
||||
models.Index(
|
||||
fields=["entity_identifier"], name="fav_entity_identifier_idx"
|
||||
),
|
||||
models.Index(
|
||||
fields=["entity_type", "entity_identifier"], name="fav_entity_idx"
|
||||
),
|
||||
]
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self._state.adding:
|
||||
|
||||
@@ -210,8 +210,7 @@ class Issue(ProjectBaseModel):
|
||||
if self._state.adding:
|
||||
with transaction.atomic():
|
||||
# Create a lock for this specific project using an advisory lock
|
||||
# This ensures only one transaction per project can execute this
|
||||
# code at a time
|
||||
# This ensures only one transaction per project can execute this code at a time
|
||||
lock_key = convert_uuid_to_integer(self.project.id)
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
@@ -510,7 +509,6 @@ class IssueUserProperty(ProjectBaseModel):
|
||||
filters = models.JSONField(default=get_default_filters)
|
||||
display_filters = models.JSONField(default=get_default_display_filters)
|
||||
display_properties = models.JSONField(default=get_default_display_properties)
|
||||
rich_filters = models.JSONField(default=dict)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Issue User Property"
|
||||
@@ -554,8 +552,7 @@ class IssueSequence(ProjectBaseModel):
|
||||
Issue,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="issue_sequence",
|
||||
null=True, # This is set to null because we want to keep the sequence
|
||||
# even if the issue is deleted
|
||||
null=True, # This is set to null because we want to keep the sequence even if the issue is deleted
|
||||
)
|
||||
sequence = models.PositiveBigIntegerField(default=1, db_index=True)
|
||||
deleted = models.BooleanField(default=False)
|
||||
|
||||
@@ -27,8 +27,7 @@ class Label(WorkspaceBaseModel):
|
||||
condition=Q(project__isnull=True, deleted_at__isnull=True),
|
||||
name="unique_name_when_project_null_and_not_deleted",
|
||||
),
|
||||
# Enforce uniqueness of project and name when project is not NULL
|
||||
# and deleted_at is NULL
|
||||
# Enforce uniqueness of project and name when project is not NULL and deleted_at is NULL
|
||||
models.UniqueConstraint(
|
||||
fields=["project", "name"],
|
||||
condition=Q(project__isnull=False, deleted_at__isnull=True),
|
||||
|
||||
@@ -207,7 +207,6 @@ class ModuleUserProperties(ProjectBaseModel):
|
||||
filters = models.JSONField(default=get_default_filters)
|
||||
display_filters = models.JSONField(default=get_default_display_filters)
|
||||
display_properties = models.JSONField(default=get_default_display_properties)
|
||||
rich_filters = models.JSONField(default=dict)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["module", "user", "deleted_at"]
|
||||
|
||||
@@ -39,14 +39,6 @@ class Notification(BaseModel):
|
||||
verbose_name_plural = "Notifications"
|
||||
db_table = "notifications"
|
||||
ordering = ("-created_at",)
|
||||
indexes = [
|
||||
models.Index(
|
||||
fields=["entity_identifier"], name="notif_entity_identifier_idx"
|
||||
),
|
||||
models.Index(fields=["entity_name"], name="notif_entity_name_idx"),
|
||||
models.Index(fields=["read_at"], name="notif_read_at_idx"),
|
||||
models.Index(fields=["receiver", "read_at"], name="notif_entity_idx"),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
"""Return name of the notifications"""
|
||||
|
||||
@@ -57,7 +57,6 @@ class Page(BaseModel):
|
||||
)
|
||||
moved_to_page = models.UUIDField(null=True, blank=True)
|
||||
moved_to_project = models.UUIDField(null=True, blank=True)
|
||||
sort_order = models.FloatField(default=65535)
|
||||
|
||||
external_id = models.CharField(max_length=255, null=True, blank=True)
|
||||
external_source = models.CharField(max_length=255, null=True, blank=True)
|
||||
@@ -99,11 +98,8 @@ class PageLog(BaseModel):
|
||||
)
|
||||
transaction = models.UUIDField(default=uuid.uuid4)
|
||||
page = models.ForeignKey(Page, related_name="page_log", on_delete=models.CASCADE)
|
||||
entity_identifier = models.UUIDField(null=True, blank=True)
|
||||
entity_identifier = models.UUIDField(null=True)
|
||||
entity_name = models.CharField(max_length=30, verbose_name="Transaction Type")
|
||||
entity_type = models.CharField(
|
||||
max_length=30, verbose_name="Entity Type", null=True, blank=True
|
||||
)
|
||||
workspace = models.ForeignKey(
|
||||
"db.Workspace", on_delete=models.CASCADE, related_name="workspace_page_log"
|
||||
)
|
||||
@@ -114,17 +110,6 @@ class PageLog(BaseModel):
|
||||
verbose_name_plural = "Page Logs"
|
||||
db_table = "page_logs"
|
||||
ordering = ("-created_at",)
|
||||
indexes = [
|
||||
models.Index(fields=["entity_type"], name="pagelog_entity_type_idx"),
|
||||
models.Index(fields=["entity_identifier"], name="pagelog_entity_id_idx"),
|
||||
models.Index(fields=["entity_name"], name="pagelog_entity_name_idx"),
|
||||
models.Index(
|
||||
fields=["entity_type", "entity_identifier"], name="pagelog_type_id_idx"
|
||||
),
|
||||
models.Index(
|
||||
fields=["entity_name", "entity_identifier"], name="pagelog_name_id_idx"
|
||||
),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.page.name} {self.entity_name}"
|
||||
|
||||
@@ -18,12 +18,6 @@ from .base import BaseModel
|
||||
ROLE_CHOICES = ((20, "Admin"), (15, "Member"), (5, "Guest"))
|
||||
|
||||
|
||||
class ROLE(Enum):
|
||||
ADMIN = 20
|
||||
MEMBER = 15
|
||||
GUEST = 5
|
||||
|
||||
|
||||
class ProjectNetwork(Enum):
|
||||
SECRET = 0
|
||||
PUBLIC = 2
|
||||
@@ -95,9 +89,9 @@ class Project(BaseModel):
|
||||
)
|
||||
emoji = models.CharField(max_length=255, null=True, blank=True)
|
||||
icon_prop = models.JSONField(null=True)
|
||||
module_view = models.BooleanField(default=False)
|
||||
cycle_view = models.BooleanField(default=False)
|
||||
issue_views_view = models.BooleanField(default=False)
|
||||
module_view = models.BooleanField(default=True)
|
||||
cycle_view = models.BooleanField(default=True)
|
||||
issue_views_view = models.BooleanField(default=True)
|
||||
page_view = models.BooleanField(default=True)
|
||||
intake_view = models.BooleanField(default=False)
|
||||
is_time_tracking_enabled = models.BooleanField(default=False)
|
||||
|
||||
@@ -13,7 +13,7 @@ VALID_KEY_CHARS = string.ascii_lowercase + string.digits
|
||||
class Session(AbstractBaseSession):
|
||||
device_info = models.JSONField(null=True, blank=True, default=None)
|
||||
session_key = models.CharField(max_length=128, primary_key=True)
|
||||
user_id = models.CharField(null=True, max_length=50, db_index=True)
|
||||
user_id = models.CharField(null=True, max_length=50)
|
||||
|
||||
@classmethod
|
||||
def get_session_store_class(cls):
|
||||
|
||||
@@ -58,7 +58,6 @@ class IssueView(WorkspaceBaseModel):
|
||||
filters = models.JSONField(default=dict)
|
||||
display_filters = models.JSONField(default=get_default_display_filters)
|
||||
display_properties = models.JSONField(default=get_default_display_properties)
|
||||
rich_filters = models.JSONField(default=dict)
|
||||
access = models.PositiveSmallIntegerField(
|
||||
default=1, choices=((0, "Private"), (1, "Public"))
|
||||
)
|
||||
|
||||
@@ -155,8 +155,7 @@ class Workspace(BaseModel):
|
||||
self, using: Optional[str] = None, soft: bool = True, *args: Any, **kwargs: Any
|
||||
):
|
||||
"""
|
||||
Override the delete method to append epoch timestamp to the slug when
|
||||
soft deleting.
|
||||
Override the delete method to append epoch timestamp to the slug when soft deleting.
|
||||
|
||||
Args:
|
||||
using: The database alias to use for the deletion.
|
||||
@@ -333,7 +332,6 @@ class WorkspaceUserProperties(BaseModel):
|
||||
filters = models.JSONField(default=get_default_filters)
|
||||
display_filters = models.JSONField(default=get_default_display_filters)
|
||||
display_properties = models.JSONField(default=get_default_display_properties)
|
||||
rich_filters = models.JSONField(default=dict)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["workspace", "user", "deleted_at"]
|
||||
|
||||
@@ -98,8 +98,7 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
|
||||
from django.db import connection
|
||||
|
||||
print(
|
||||
f"{request.method} - {request.get_full_path()} of Queries: "
|
||||
f"{len(connection.queries)}"
|
||||
f"{request.method} - {request.get_full_path()} of Queries: {len(connection.queries)}"
|
||||
)
|
||||
return response
|
||||
|
||||
|
||||
@@ -167,8 +167,7 @@ class EmailCredentialCheckEndpoint(BaseAPIView):
|
||||
except ConnectionError:
|
||||
return Response(
|
||||
{
|
||||
"error": "Network connection error. Please check your "
|
||||
"internet connection."
|
||||
"error": "Network connection error. Please check your internet connection."
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 4.2.22 on 2025-09-11 08:40
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("license", "0005_rename_product_instance_edition_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="instance",
|
||||
name="is_current_version_deprecated",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -38,8 +38,6 @@ class Instance(BaseModel):
|
||||
is_signup_screen_visited = models.BooleanField(default=False)
|
||||
is_verified = models.BooleanField(default=False)
|
||||
is_test = models.BooleanField(default=False)
|
||||
# field for validating if the current version is deprecated
|
||||
is_current_version_deprecated = models.BooleanField(default=False)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Instance"
|
||||
|
||||
@@ -155,8 +155,7 @@ class ReadReplicaRoutingMiddleware:
|
||||
# provides extra protection specifically for view exceptions
|
||||
clear_read_replica_context()
|
||||
logger.debug(
|
||||
f"Cleaned up read replica context due to exception: "
|
||||
f"{type(exception).__name__}"
|
||||
f"Cleaned up read replica context due to exception: {type(exception).__name__}"
|
||||
)
|
||||
|
||||
# Return None to let the exception continue propagating
|
||||
|
||||
@@ -22,8 +22,7 @@ class RequestLoggerMiddleware:
|
||||
|
||||
def _should_log_route(self, request: Request | HttpRequest) -> bool:
|
||||
"""
|
||||
Determines whether a route should be logged based on the request and
|
||||
status code.
|
||||
Determines whether a route should be logged based on the request and status code.
|
||||
"""
|
||||
# Don't log health checks
|
||||
if request.path == "/" and request.method == "GET":
|
||||
@@ -137,7 +136,6 @@ class APITokenLogMiddleware:
|
||||
|
||||
except Exception as e:
|
||||
api_logger.exception(e)
|
||||
# If the token does not exist, you can decide whether to log
|
||||
# this as an invalid attempt
|
||||
# If the token does not exist, you can decide whether to log this as an invalid attempt
|
||||
|
||||
return None
|
||||
|
||||
@@ -74,9 +74,7 @@ REST_FRAMEWORK = {
|
||||
"DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",),
|
||||
"DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",),
|
||||
"DEFAULT_FILTER_BACKENDS": ("django_filters.rest_framework.DjangoFilterBackend",),
|
||||
"EXCEPTION_HANDLER": (
|
||||
"plane.authentication.adapter.exception.auth_exception_handler"
|
||||
),
|
||||
"EXCEPTION_HANDLER": "plane.authentication.adapter.exception.auth_exception_handler",
|
||||
# Preserve original Django URL parameter names (pk) instead of converting to 'id'
|
||||
"SCHEMA_COERCE_PATH_PK": False,
|
||||
}
|
||||
@@ -201,9 +199,7 @@ else:
|
||||
# Password validations
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
"NAME": (
|
||||
"django.contrib.auth.password_validation.UserAttributeSimilarityValidator"
|
||||
)
|
||||
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"
|
||||
},
|
||||
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
|
||||
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
|
||||
@@ -469,7 +465,3 @@ if ENABLE_DRF_SPECTACULAR:
|
||||
REST_FRAMEWORK["DEFAULT_SCHEMA_CLASS"] = "drf_spectacular.openapi.AutoSchema"
|
||||
INSTALLED_APPS.append("drf_spectacular")
|
||||
from .openapi import SPECTACULAR_SETTINGS # noqa: F401
|
||||
|
||||
# MongoDB Settings
|
||||
MONGO_DB_URL = os.environ.get("MONGO_DB_URL", False)
|
||||
MONGO_DB_DATABASE = os.environ.get("MONGO_DB_DATABASE", False)
|
||||
|
||||
@@ -19,8 +19,7 @@ class MongoConnection:
|
||||
"""
|
||||
A singleton class that manages MongoDB connections.
|
||||
|
||||
This class ensures only one MongoDB connection is maintained throughout the
|
||||
application.
|
||||
This class ensures only one MongoDB connection is maintained throughout the application.
|
||||
It provides methods to access the MongoDB client, database, and collections.
|
||||
|
||||
Attributes:
|
||||
@@ -48,8 +47,7 @@ class MongoConnection:
|
||||
|
||||
if not mongo_url or not mongo_db_database:
|
||||
logger.warning(
|
||||
"MongoDB connection parameters not configured. "
|
||||
"MongoDB functionality will be disabled."
|
||||
"MongoDB connection parameters not configured. MongoDB functionality will be disabled."
|
||||
)
|
||||
return cls._instance
|
||||
|
||||
@@ -61,8 +59,7 @@ class MongoConnection:
|
||||
logger.info("MongoDB connection established successfully")
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Failed to initialize MongoDB connection: {str(e)}. "
|
||||
f"MongoDB functionality will be disabled."
|
||||
f"Failed to initialize MongoDB connection: {str(e)}. MongoDB functionality will be disabled."
|
||||
)
|
||||
return cls._instance
|
||||
|
||||
@@ -99,15 +96,13 @@ class MongoConnection:
|
||||
collection_name (str): The name of the collection to retrieve
|
||||
|
||||
Returns:
|
||||
Optional[Collection]: The MongoDB collection instance or None if
|
||||
not configured
|
||||
Optional[Collection]: The MongoDB collection instance or None if not configured
|
||||
"""
|
||||
try:
|
||||
db = cls.get_db()
|
||||
if db is None:
|
||||
logger.warning(
|
||||
f"Cannot access collection '{collection_name}': "
|
||||
f"MongoDB not configured"
|
||||
f"Cannot access collection '{collection_name}': MongoDB not configured"
|
||||
)
|
||||
return None
|
||||
return db[collection_name]
|
||||
@@ -123,7 +118,4 @@ class MongoConnection:
|
||||
Returns:
|
||||
bool: True if MongoDB is configured and connected, False otherwise
|
||||
"""
|
||||
|
||||
if cls._client is None:
|
||||
cls._instance = cls()
|
||||
return cls._client is not None and cls._db is not None
|
||||
|
||||
@@ -52,16 +52,14 @@ SPECTACULAR_SETTINGS = {
|
||||
"name": "Assets",
|
||||
"description": (
|
||||
"**File Upload & Presigned URLs**\n\n"
|
||||
"Generate presigned URLs for direct file uploads to cloud storage. "
|
||||
"Handle user avatars, cover images, and generic project assets with "
|
||||
"secure upload workflows.\n\n"
|
||||
"Generate presigned URLs for direct file uploads to cloud storage. Handle user avatars, "
|
||||
"cover images, and generic project assets with secure upload workflows.\n\n"
|
||||
"*Key Features:*\n"
|
||||
"- Generate presigned URLs for S3 uploads\n"
|
||||
"- Support for user avatars and cover images\n"
|
||||
"- Generic asset upload for projects\n"
|
||||
"- File validation and size limits\n\n"
|
||||
"*Use Cases:* User profile images, project file uploads, "
|
||||
"secure direct-to-cloud uploads."
|
||||
"*Use Cases:* User profile images, project file uploads, secure direct-to-cloud uploads."
|
||||
),
|
||||
},
|
||||
# Project Organization
|
||||
@@ -69,16 +67,14 @@ SPECTACULAR_SETTINGS = {
|
||||
"name": "Cycles",
|
||||
"description": (
|
||||
"**Sprint & Development Cycles**\n\n"
|
||||
"Create and manage development cycles (sprints) to organize work into "
|
||||
"time-boxed iterations. Track progress, assign work items, and monitor "
|
||||
"team velocity.\n\n"
|
||||
"Create and manage development cycles (sprints) to organize work into time-boxed iterations. "
|
||||
"Track progress, assign work items, and monitor team velocity.\n\n"
|
||||
"*Key Features:*\n"
|
||||
"- Create and configure development cycles\n"
|
||||
"- Assign work items to cycles\n"
|
||||
"- Track cycle progress and completion\n"
|
||||
"- Generate cycle analytics and reports\n\n"
|
||||
"*Use Cases:* Sprint planning, iterative development, "
|
||||
"progress tracking, team velocity."
|
||||
"*Use Cases:* Sprint planning, iterative development, progress tracking, team velocity."
|
||||
),
|
||||
},
|
||||
# System Features
|
||||
@@ -86,16 +82,14 @@ SPECTACULAR_SETTINGS = {
|
||||
"name": "Intake",
|
||||
"description": (
|
||||
"**Work Item Intake Queue**\n\n"
|
||||
"Manage incoming work items through a dedicated intake queue "
|
||||
"for triage and review. Submit, update, and process work items "
|
||||
"before they enter the main project workflow.\n\n"
|
||||
"Manage incoming work items through a dedicated intake queue for triage and review. "
|
||||
"Submit, update, and process work items before they enter the main project workflow.\n\n"
|
||||
"*Key Features:*\n"
|
||||
"- Submit work items to intake queue\n"
|
||||
"- Review and triage incoming work items\n"
|
||||
"- Update intake work item status and properties\n"
|
||||
"- Accept, reject, or modify work items before approval\n\n"
|
||||
"*Use Cases:* Work item triage, external submissions, quality review, "
|
||||
"approval workflows."
|
||||
"*Use Cases:* Work item triage, external submissions, quality review, approval workflows."
|
||||
),
|
||||
},
|
||||
# Project Organization
|
||||
@@ -103,16 +97,14 @@ SPECTACULAR_SETTINGS = {
|
||||
"name": "Labels",
|
||||
"description": (
|
||||
"**Labels & Tags**\n\n"
|
||||
"Create and manage labels to categorize and organize work items. "
|
||||
"Use color-coded labels for easy identification, filtering, and "
|
||||
"project organization.\n\n"
|
||||
"Create and manage labels to categorize and organize work items. Use color-coded labels "
|
||||
"for easy identification, filtering, and project organization.\n\n"
|
||||
"*Key Features:*\n"
|
||||
"- Create custom labels with colors and descriptions\n"
|
||||
"- Apply labels to work items for categorization\n"
|
||||
"- Filter and search by labels\n"
|
||||
"- Organize labels across projects\n\n"
|
||||
"*Use Cases:* Priority marking, feature categorization, "
|
||||
"bug classification, team organization."
|
||||
"*Use Cases:* Priority marking, feature categorization, bug classification, team organization."
|
||||
),
|
||||
},
|
||||
# Team & User Management
|
||||
@@ -120,16 +112,14 @@ SPECTACULAR_SETTINGS = {
|
||||
"name": "Members",
|
||||
"description": (
|
||||
"**Team Member Management**\n\n"
|
||||
"Manage team members, roles, and permissions within projects "
|
||||
"and workspaces. Control access levels and track member "
|
||||
"participation.\n\n"
|
||||
"Manage team members, roles, and permissions within projects and workspaces. "
|
||||
"Control access levels and track member participation.\n\n"
|
||||
"*Key Features:*\n"
|
||||
"- Invite and manage team members\n"
|
||||
"- Assign roles and permissions\n"
|
||||
"- Control project and workspace access\n"
|
||||
"- Track member activity and participation\n\n"
|
||||
"*Use Cases:* Team setup, access control, role management, "
|
||||
"collaboration."
|
||||
"*Use Cases:* Team setup, access control, role management, collaboration."
|
||||
),
|
||||
},
|
||||
# Project Organization
|
||||
@@ -137,16 +127,14 @@ SPECTACULAR_SETTINGS = {
|
||||
"name": "Modules",
|
||||
"description": (
|
||||
"**Feature Modules**\n\n"
|
||||
"Group related work items into modules for better organization "
|
||||
"and tracking. Plan features, track progress, and manage "
|
||||
"deliverables at a higher level.\n\n"
|
||||
"Group related work items into modules for better organization and tracking. "
|
||||
"Plan features, track progress, and manage deliverables at a higher level.\n\n"
|
||||
"*Key Features:*\n"
|
||||
"- Create and organize feature modules\n"
|
||||
"- Group work items by module\n"
|
||||
"- Track module progress and completion\n"
|
||||
"- Manage module leads and assignments\n\n"
|
||||
"*Use Cases:* Feature planning, release organization, "
|
||||
"progress tracking, team coordination."
|
||||
"*Use Cases:* Feature planning, release organization, progress tracking, team coordination."
|
||||
),
|
||||
},
|
||||
# Core Project Management
|
||||
@@ -154,16 +142,14 @@ SPECTACULAR_SETTINGS = {
|
||||
"name": "Projects",
|
||||
"description": (
|
||||
"**Project Management**\n\n"
|
||||
"Create and manage projects to organize your development work. "
|
||||
"Configure project settings, manage team access, and control "
|
||||
"project visibility.\n\n"
|
||||
"Create and manage projects to organize your development work. Configure project settings, "
|
||||
"manage team access, and control project visibility.\n\n"
|
||||
"*Key Features:*\n"
|
||||
"- Create, update, and delete projects\n"
|
||||
"- Configure project settings and preferences\n"
|
||||
"- Manage team access and permissions\n"
|
||||
"- Control project visibility and sharing\n\n"
|
||||
"*Use Cases:* Project setup, team collaboration, access "
|
||||
"control, project configuration."
|
||||
"*Use Cases:* Project setup, team collaboration, access control, project configuration."
|
||||
),
|
||||
},
|
||||
# Project Organization
|
||||
@@ -171,16 +157,14 @@ SPECTACULAR_SETTINGS = {
|
||||
"name": "States",
|
||||
"description": (
|
||||
"**Workflow States**\n\n"
|
||||
"Define custom workflow states for work items to match your "
|
||||
"team's process. Configure state transitions and track work "
|
||||
"item progress through different stages.\n\n"
|
||||
"Define custom workflow states for work items to match your team's process. "
|
||||
"Configure state transitions and track work item progress through different stages.\n\n"
|
||||
"*Key Features:*\n"
|
||||
"- Create custom workflow states\n"
|
||||
"- Configure state transitions and rules\n"
|
||||
"- Track work item progress through states\n"
|
||||
"- Set state-based permissions and automation\n\n"
|
||||
"*Use Cases:* Custom workflows, status tracking, process "
|
||||
"automation, progress monitoring."
|
||||
"*Use Cases:* Custom workflows, status tracking, process automation, progress monitoring."
|
||||
),
|
||||
},
|
||||
# Team & User Management
|
||||
@@ -188,15 +172,14 @@ SPECTACULAR_SETTINGS = {
|
||||
"name": "Users",
|
||||
"description": (
|
||||
"**Current User Information**\n\n"
|
||||
"Get information about the currently authenticated user "
|
||||
"including profile details and account settings.\n\n"
|
||||
"Get information about the currently authenticated user including profile details "
|
||||
"and account settings.\n\n"
|
||||
"*Key Features:*\n"
|
||||
"- Retrieve current user profile\n"
|
||||
"- Access user account information\n"
|
||||
"- View user preferences and settings\n"
|
||||
"- Get authentication context\n\n"
|
||||
"*Use Cases:* Profile display, user context, account "
|
||||
"information, authentication status."
|
||||
"*Use Cases:* Profile display, user context, account information, authentication status."
|
||||
),
|
||||
},
|
||||
# Work Item Management
|
||||
@@ -204,80 +187,70 @@ SPECTACULAR_SETTINGS = {
|
||||
"name": "Work Item Activity",
|
||||
"description": (
|
||||
"**Activity History & Search**\n\n"
|
||||
"View activity history and search for work items across the "
|
||||
"workspace. Get detailed activity logs and find work items "
|
||||
"using text search.\n\n"
|
||||
"View activity history and search for work items across the workspace. "
|
||||
"Get detailed activity logs and find work items using text search.\n\n"
|
||||
"*Key Features:*\n"
|
||||
"- View work item activity history\n"
|
||||
"- Search work items across workspace\n"
|
||||
"- Track changes and modifications\n"
|
||||
"- Filter search results by project\n\n"
|
||||
"*Use Cases:* Activity tracking, work item discovery, change "
|
||||
"history, workspace search."
|
||||
"*Use Cases:* Activity tracking, work item discovery, change history, workspace search."
|
||||
),
|
||||
},
|
||||
{
|
||||
"name": "Work Item Attachments",
|
||||
"description": (
|
||||
"**Work Item File Attachments**\n\n"
|
||||
"Generate presigned URLs for uploading files directly to "
|
||||
"specific work items. Upload and manage attachments "
|
||||
"associated with work items.\n\n"
|
||||
"Generate presigned URLs for uploading files directly to specific work items. "
|
||||
"Upload and manage attachments associated with work items.\n\n"
|
||||
"*Key Features:*\n"
|
||||
"- Generate presigned URLs for work item attachments\n"
|
||||
"- Upload files directly to work items\n"
|
||||
"- Retrieve and manage attachment metadata\n"
|
||||
"- Delete attachments from work items\n\n"
|
||||
"*Use Cases:* Screenshots, error logs, design files, "
|
||||
"supporting documents."
|
||||
"*Use Cases:* Screenshots, error logs, design files, supporting documents."
|
||||
),
|
||||
},
|
||||
{
|
||||
"name": "Work Item Comments",
|
||||
"description": (
|
||||
"**Comments & Discussions**\n\n"
|
||||
"Add comments and discussions to work items for team "
|
||||
"collaboration. Support threaded conversations, mentions, and "
|
||||
"rich text formatting.\n\n"
|
||||
"Add comments and discussions to work items for team collaboration. "
|
||||
"Support threaded conversations, mentions, and rich text formatting.\n\n"
|
||||
"*Key Features:*\n"
|
||||
"- Add comments to work items\n"
|
||||
"- Thread conversations and replies\n"
|
||||
"- Mention users and trigger notifications\n"
|
||||
"- Rich text and markdown support\n\n"
|
||||
"*Use Cases:* Team discussions, progress updates, code "
|
||||
"reviews, decision tracking."
|
||||
"*Use Cases:* Team discussions, progress updates, code reviews, decision tracking."
|
||||
),
|
||||
},
|
||||
{
|
||||
"name": "Work Item Links",
|
||||
"description": (
|
||||
"**External Links & References**\n\n"
|
||||
"Link work items to external resources like documentation, "
|
||||
"repositories, or design files. Maintain connections between "
|
||||
"work items and external systems.\n\n"
|
||||
"Link work items to external resources like documentation, repositories, or design files. "
|
||||
"Maintain connections between work items and external systems.\n\n"
|
||||
"*Key Features:*\n"
|
||||
"- Add external URL links to work items\n"
|
||||
"- Validate and preview linked resources\n"
|
||||
"- Organize links by type and category\n"
|
||||
"- Track link usage and access\n\n"
|
||||
"*Use Cases:* Documentation links, repository connections, "
|
||||
"design references, external tools."
|
||||
"*Use Cases:* Documentation links, repository connections, design references, external tools."
|
||||
),
|
||||
},
|
||||
{
|
||||
"name": "Work Items",
|
||||
"description": (
|
||||
"**Work Items & Tasks**\n\n"
|
||||
"Create and manage work items like tasks, bugs, features, and "
|
||||
"user stories. The core entities for tracking work in your "
|
||||
"projects.\n\n"
|
||||
"Create and manage work items like tasks, bugs, features, and user stories. "
|
||||
"The core entities for tracking work in your projects.\n\n"
|
||||
"*Key Features:*\n"
|
||||
"- Create, update, and manage work items\n"
|
||||
"- Assign to team members and set priorities\n"
|
||||
"- Track progress through workflow states\n"
|
||||
"- Set due dates, estimates, and relationships\n\n"
|
||||
"*Use Cases:* Bug tracking, task management, feature "
|
||||
"development, sprint planning."
|
||||
"*Use Cases:* Bug tracking, task management, feature development, sprint planning."
|
||||
),
|
||||
},
|
||||
],
|
||||
|
||||
@@ -22,8 +22,7 @@ class DynamicBaseSerializer(BaseSerializer):
|
||||
"""
|
||||
Adjust the serializer's fields based on the provided 'fields' list.
|
||||
|
||||
:param fields: List or dictionary specifying which fields to include
|
||||
in the serializer.
|
||||
:param fields: List or dictionary specifying which fields to include in the serializer.
|
||||
:return: The updated fields for the serializer.
|
||||
"""
|
||||
# Check each field_name in the provided fields.
|
||||
|
||||
@@ -30,6 +30,7 @@ from plane.db.models import (
|
||||
)
|
||||
from plane.utils.content_validator import (
|
||||
validate_html_content,
|
||||
validate_json_content,
|
||||
validate_binary_data,
|
||||
)
|
||||
|
||||
@@ -289,24 +290,20 @@ class IssueCreateSerializer(BaseSerializer):
|
||||
raise serializers.ValidationError("Start date cannot exceed target date")
|
||||
|
||||
# Validate description content for security
|
||||
if "description_html" in data and data["description_html"]:
|
||||
is_valid, error_msg, sanitized_html = validate_html_content(
|
||||
data["description_html"]
|
||||
)
|
||||
if "description" in data and data["description"]:
|
||||
is_valid, error_msg = validate_json_content(data["description"])
|
||||
if not is_valid:
|
||||
raise serializers.ValidationError(
|
||||
{"error": "html content is not valid"}
|
||||
)
|
||||
# Update the data with sanitized HTML if available
|
||||
if sanitized_html is not None:
|
||||
data["description_html"] = sanitized_html
|
||||
raise serializers.ValidationError({"description": error_msg})
|
||||
|
||||
if "description_html" in data and data["description_html"]:
|
||||
is_valid, error_msg = validate_html_content(data["description_html"])
|
||||
if not is_valid:
|
||||
raise serializers.ValidationError({"description_html": error_msg})
|
||||
|
||||
if "description_binary" in data and data["description_binary"]:
|
||||
is_valid, error_msg = validate_binary_data(data["description_binary"])
|
||||
if not is_valid:
|
||||
raise serializers.ValidationError(
|
||||
{"description_binary": "Invalid binary data"}
|
||||
)
|
||||
raise serializers.ValidationError({"description_binary": error_msg})
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@@ -96,8 +96,7 @@ class EntityAssetEndpoint(BaseAPIView):
|
||||
if type not in allowed_types:
|
||||
return Response(
|
||||
{
|
||||
"error": "Invalid file type. Only JPEG, PNG, WebP, JPG and "
|
||||
"GIF files are allowed.",
|
||||
"error": "Invalid file type. Only JPEG, PNG, WebP, JPG and GIF files are allowed.",
|
||||
"status": False,
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user