Compare commits

..

2 Commits

Author SHA1 Message Date
Aaryan Khandelwal 015f8cbfe5 chore: split editor ref 2025-08-26 16:56:29 +05:30
Aaryan Khandelwal 42b38db131 chore: implement code splitting for editor 2025-08-26 14:37:17 +05:30
865 changed files with 12419 additions and 18265 deletions
+12 -21
View File
@@ -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
+2
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -0,0 +1 @@
lts/jod
-12
View File
@@ -1,12 +0,0 @@
.next/*
out/*
public/*
dist/*
node_modules/*
.turbo/*
.env*
.env
.env.local
.env.development
.env.production
.env.test
+1
View File
@@ -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 -1
View File
@@ -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";
+1 -1
View File
@@ -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
View File
@@ -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 -1
View File
@@ -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"
+1 -3
View File
@@ -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,
+11 -15
View File
@@ -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", []):
+3 -6
View File
@@ -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.
"""
+17 -7
View File
@@ -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
+5 -9
View File
@@ -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)
+3
View File
@@ -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,
+3 -1
View File
@@ -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,
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+2 -20
View File
@@ -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
+13 -22
View File
@@ -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()
+11 -13
View File
@@ -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", []):
+14 -33
View File
@@ -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):
+14 -3
View File
@@ -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"""
+20 -9
View File
@@ -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
+10 -13
View File
@@ -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
+2 -10
View File
@@ -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)
+4 -8
View File
@@ -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",
+3 -6
View File
@@ -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("/"),
),
),
+1 -2
View File
@@ -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,
)
+10 -13
View File
@@ -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"])
+13 -39
View File
@@ -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()
):
+3 -6
View File
@@ -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()
+6 -13
View File
@@ -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,
)
+3 -6
View File
@@ -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,
)
+1 -2
View File
@@ -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(
+4 -2
View File
@@ -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,
+1 -3
View File
@@ -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"),
},
)
+3 -6
View File
@@ -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/",
+4 -11
View File
@@ -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,
+1 -2
View File
@@ -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)
+24 -24
View File
@@ -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
+2 -3
View File
@@ -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:
+3 -10
View File
@@ -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),
),
]
+2 -17
View File
@@ -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
-1
View File
@@ -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"]
-1
View File
@@ -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"
-9
View File
@@ -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:
+2 -5
View File
@@ -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)
+1 -2
View File
@@ -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),
-1
View File
@@ -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"]
-8
View File
@@ -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"""
+1 -16
View File
@@ -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}"
+3 -9
View File
@@ -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)
+1 -1
View File
@@ -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):
-1
View File
@@ -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"))
)
+1 -3
View File
@@ -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"]
+1 -2
View File
@@ -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"
+1 -2
View File
@@ -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
+2 -4
View File
@@ -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
+2 -10
View File
@@ -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)
+5 -13
View File
@@ -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
+42 -69
View File
@@ -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."
),
},
],
+1 -2
View File
@@ -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.
+10 -13
View File
@@ -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
+1 -2
View File
@@ -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