Compare commits

...

1 Commits

Author SHA1 Message Date
sriramveeraghanta d2110d3318 chore: api lint errors 2025-09-16 20:20:34 +05:30
68 changed files with 347 additions and 183 deletions
+2 -1
View File
@@ -29,7 +29,8 @@ 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.
+2 -1
View File
@@ -43,7 +43,8 @@ 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(
+6 -3
View File
@@ -17,7 +17,8 @@ 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.
"""
@@ -105,7 +106,8 @@ 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.
"""
@@ -155,7 +157,8 @@ 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.
"""
+9 -5
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, OpenApiTypes
from drf_spectacular.utils import OpenApiExample, OpenApiRequest
# Module Imports
from plane.bgtasks.storage_metadata_task import get_asset_object_metadata
@@ -106,7 +106,8 @@ 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
@@ -201,8 +202,10 @@ 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)
@@ -361,7 +364,8 @@ 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,12 +30,10 @@ 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 (
@@ -99,7 +97,6 @@ from plane.utils.openapi import (
EXTERNAL_ID_PARAMETER,
EXTERNAL_SOURCE_PARAMETER,
ORDER_BY_PARAMETER,
SEARCH_PARAMETER,
SEARCH_PARAMETER_REQUIRED,
LIMIT_PARAMETER,
WORKSPACE_SEARCH_PARAMETER,
+1 -3
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, OpenApiExample, OpenApiRequest
from drf_spectacular.utils import OpenApiResponse, OpenApiRequest
# Module imports
from plane.api.serializers import (
@@ -41,8 +41,6 @@ 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, OpenApiExample, OpenApiRequest
from drf_spectacular.utils import OpenApiResponse, 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, OpenApiExample, OpenApiRequest
from drf_spectacular.utils import OpenApiResponse, OpenApiRequest
# Module imports
from plane.api.serializers import StateSerializer
-1
View File
@@ -1,4 +1,3 @@
from lxml import html
# Django imports
from django.utils import timezone
-1
View File
@@ -1,4 +1,3 @@
from lxml import html
# Django imports
from django.utils import timezone
+8 -4
View File
@@ -369,11 +369,13 @@ 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",
@@ -480,11 +482,13 @@ 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",
+6 -3
View File
@@ -485,11 +485,13 @@ 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",
@@ -597,7 +599,8 @@ 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("/"),
),
),
+2 -1
View File
@@ -112,7 +112,8 @@ 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,
)
+4 -3
View File
@@ -1,6 +1,5 @@
# Python imports
import json
import base64
from datetime import datetime
from django.core.serializers.json import DjangoJSONEncoder
@@ -164,7 +163,8 @@ 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,
)
@@ -346,7 +346,8 @@ 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"])
+6 -3
View File
@@ -64,7 +64,8 @@ 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"
}
)
@@ -91,7 +92,8 @@ class ProjectInvitationsViewset(BaseViewSet):
except ValidationError:
return Response(
{
"error": f"Invalid email - {email} provided a valid email address is required to send the invite"
"error": f"Invalid email - {email} provided a valid email address "
f"is required to send the invite"
},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -234,7 +236,8 @@ 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()
+13 -6
View File
@@ -39,7 +39,8 @@ 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
@@ -69,7 +70,8 @@ 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,
)
@@ -77,12 +79,14 @@ 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],
@@ -253,7 +257,8 @@ 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,
)
@@ -287,7 +292,9 @@ 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,
)
+6 -3
View File
@@ -79,7 +79,8 @@ 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,
)
@@ -107,7 +108,8 @@ 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,
)
@@ -132,7 +134,8 @@ 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,
)
+2 -1
View File
@@ -147,7 +147,8 @@ 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(
+2 -4
View File
@@ -39,9 +39,6 @@ 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
@@ -393,7 +390,8 @@ class ExportWorkspaceUserActivityEndpoint(BaseAPIView):
rows = [
(
activity.actor.display_name,
f"{activity.project.identifier} - {activity.issue.sequence_id if activity.issue else ''}",
f"{activity.project.identifier} - "
f"{activity.issue.sequence_id if activity.issue else ''}",
activity.project.name,
activity.created_at,
activity.updated_at,
+6 -3
View File
@@ -113,7 +113,8 @@ class WorkspaceInvitationsViewset(BaseViewSet):
except ValidationError:
return Response(
{
"error": f"Invalid email - {email} provided a valid email address is required to send the invite"
"error": f"Invalid email - {email} provided a valid email address "
f"is required to send the invite"
},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -185,7 +186,8 @@ 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()
@@ -262,7 +264,8 @@ 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/",
+11 -4
View File
@@ -99,7 +99,8 @@ 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,
)
@@ -126,7 +127,9 @@ 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,
)
@@ -166,7 +169,9 @@ 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,
)
@@ -187,7 +192,9 @@ 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,
)
+2 -1
View File
@@ -157,7 +157,8 @@ 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,7 +8,8 @@ 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(
@@ -108,7 +108,8 @@ 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,7 +95,8 @@ class SignInAuthSpaceEndpoint(View):
# Login the user and record his device info
user_login(request=request, user=user, is_space=True)
# redirect to next path
url = f"{base_host(request=request, is_space=True)}{str(next_path) if next_path else ''}"
base_url = base_host(request=request, is_space=True)
url = f"{base_url}{str(next_path) if next_path else ''}"
return HttpResponseRedirect(url)
except AuthenticationException as e:
params = e.get_error_dict()
@@ -179,7 +180,8 @@ class SignUpAuthSpaceEndpoint(View):
# Login the user and record his device info
user_login(request=request, user=user, is_space=True)
# redirect to referer path
url = f"{base_host(request=request, is_space=True)}{str(next_path) if next_path else ''}"
base_url = base_host(request=request, is_space=True)
url = f"{base_url}{str(next_path) if next_path else ''}"
return HttpResponseRedirect(url)
except AuthenticationException as e:
params = e.get_error_dict()
@@ -89,7 +89,8 @@ class GitHubCallbackSpaceEndpoint(View):
user_login(request=request, user=user, is_space=True)
# Process workspace and project invitations
# redirect to referer path
url = f"{base_host(request=request, is_space=True)}{str(next_path) if next_path else ''}"
base_url = base_host(request=request, is_space=True)
url = f"{base_url}{str(next_path) if next_path else ''}"
return HttpResponseRedirect(url)
except AuthenticationException as e:
params = e.get_error_dict()
@@ -89,7 +89,8 @@ class GitLabCallbackSpaceEndpoint(View):
user_login(request=request, user=user, is_space=True)
# Process workspace and project invitations
# redirect to referer path
url = f"{base_host(request=request, is_space=True)}{str(next_path) if next_path else ''}"
base_url = base_host(request=request, is_space=True)
url = f"{base_url}{str(next_path) if next_path else ''}"
return HttpResponseRedirect(url)
except AuthenticationException as e:
params = e.get_error_dict()
@@ -85,7 +85,8 @@ class GoogleCallbackSpaceEndpoint(View):
# Login the user and record his device info
user_login(request=request, user=user, is_space=True)
# redirect to referer path
url = f"{base_host(request=request, is_space=True)}{str(next_path) if next_path else ''}"
base_url = base_host(request=request, is_space=True)
url = f"{base_url}{str(next_path) if next_path else ''}"
return HttpResponseRedirect(url)
except AuthenticationException as e:
params = e.get_error_dict()
@@ -146,7 +146,8 @@ class MagicSignUpSpaceEndpoint(View):
# Login the user and record his device info
user_login(request=request, user=user, is_space=True)
# redirect to referer path
url = f"{base_host(request=request, is_space=True)}{str(next_path) if next_path else ''}"
base_url = base_host(request=request, is_space=True)
url = f"{base_url}{str(next_path) if next_path else ''}"
return HttpResponseRedirect(url)
except AuthenticationException as e:
@@ -120,7 +120,8 @@ class ResetPasswordSpaceEndpoint(View):
error_message="INVALID_PASSWORD_TOKEN",
)
params = exc.get_error_dict()
url = f"{base_host(request=request, is_space=True)}/accounts/reset-password/?{urlencode(params)}"
base_url = base_host(request=request, is_space=True)
url = f"{base_url}/accounts/reset-password/?{urlencode(params)}"
return HttpResponseRedirect(url)
password = request.POST.get("password", False)
@@ -130,7 +131,8 @@ class ResetPasswordSpaceEndpoint(View):
error_code=AUTHENTICATION_ERROR_CODES["INVALID_PASSWORD"],
error_message="INVALID_PASSWORD",
)
url = f"{base_host(request=request, is_space=True)}/accounts/reset-password/?{urlencode(exc.get_error_dict())}"
base_url = base_host(request=request, is_space=True)
url = f"{base_url}/accounts/reset-password/?{urlencode(exc.get_error_dict())}"
return HttpResponseRedirect(url)
# Check the password complexity
@@ -140,7 +142,8 @@ class ResetPasswordSpaceEndpoint(View):
error_code=AUTHENTICATION_ERROR_CODES["INVALID_PASSWORD"],
error_message="INVALID_PASSWORD",
)
url = f"{base_host(request=request, is_space=True)}/accounts/reset-password/?{urlencode(exc.get_error_dict())}"
base_url = base_host(request=request, is_space=True)
url = f"{base_url}/accounts/reset-password/?{urlencode(exc.get_error_dict())}"
return HttpResponseRedirect(url)
# set_password also hashes the password that the user will get
@@ -22,8 +22,10 @@ class SignOutAuthSpaceEndpoint(View):
user.save()
# Log the user out
logout(request)
url = f"{base_host(request=request, is_space=True)}{str(validate_next_path(next_path)) if next_path else ''}"
base_url = base_host(request=request, is_space=True)
url = f"{base_url}{str(validate_next_path(next_path)) if next_path else ''}"
return HttpResponseRedirect(url)
except Exception:
url = f"{base_host(request=request, is_space=True)}{str(validate_next_path(next_path)) if next_path else ''}"
base_url = base_host(request=request, is_space=True)
url = f"{base_url}{str(validate_next_path(next_path)) if next_path else ''}"
return HttpResponseRedirect(url)
+3 -2
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,7 +458,8 @@ def model_activity(
json.loads(current_instance) if current_instance is not None else None
)
# Loop through all keys in requested data and check the current value and requested value
# Loop through all keys in requested data and check the current value and
# requested value
for key in requested_data:
# Check if key is present in current instance or not
if key in current_instance:
@@ -76,7 +76,8 @@ 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
@@ -235,7 +236,8 @@ 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}' missing in issue seed"
f"Task: workspace_seed_task -> Required field '{field}' "
f"missing in issue seed"
)
continue
@@ -318,6 +320,7 @@ def workspace_seed(workspace_id: uuid.UUID) -> None:
return
except Exception as e:
logger.error(
f"Task: workspace_seed_task -> Failed to seed workspace {workspace_id}: {str(e)}"
f"Task: workspace_seed_task -> Failed to seed workspace "
f"{workspace_id}: {str(e)}"
)
raise e
@@ -62,7 +62,8 @@ class Command(BaseCommand):
# Access to the bucket is forbidden
self.stdout.write(
self.style.ERROR(
f"Access to the bucket '{bucket_name}' is forbidden. Check permissions."
f"Access to the bucket '{bucket_name}' is forbidden. "
f"Check permissions."
)
)
else:
@@ -61,7 +61,8 @@ 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,7 +37,8 @@ class Command(BaseCommand):
if workspace.deleted_at is None:
self.stdout.write(
self.style.WARNING(
f"Workspace '{workspace.name}' (slug: {workspace.slug}) is not deleted."
f"Workspace '{workspace.name}' (slug: {workspace.slug}) "
f"is not deleted."
)
)
return
@@ -46,7 +47,8 @@ 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}) already has a timestamp appended."
f"Workspace '{workspace.name}' (slug: {workspace.slug}) "
f"already has a timestamp appended."
)
)
return
@@ -59,7 +61,8 @@ class Command(BaseCommand):
if dry_run:
self.stdout.write(
f"Would update workspace '{workspace.name}' slug from '{workspace.slug}' to '{new_slug}'"
f"Would update workspace '{workspace.name}' slug from "
f"'{workspace.slug}' to '{new_slug}'"
)
else:
try:
@@ -68,7 +71,8 @@ class Command(BaseCommand):
workspace.save(update_fields=["slug"])
self.stdout.write(
self.style.SUCCESS(
f"Updated workspace '{workspace.name}' slug from '{workspace.slug}' to '{new_slug}'"
f"Updated workspace '{workspace.name}' slug from "
f"'{workspace.slug}' to '{new_slug}'"
)
)
except Exception as e:
+8 -2
View File
@@ -100,7 +100,10 @@ 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/{self.project_id}/issues/{self.issue_id}/attachments/{self.id}/"
return (
f"/api/assets/v2/workspaces/{self.workspace.slug}/projects/"
f"{self.project_id}/issues/{self.issue_id}/attachments/{self.id}/"
)
if self.entity_type in [
self.EntityTypeContext.ISSUE_DESCRIPTION,
@@ -108,6 +111,9 @@ class FileAsset(BaseModel):
self.EntityTypeContext.PAGE_DESCRIPTION,
self.EntityTypeContext.DRAFT_ISSUE_DESCRIPTION,
]:
return f"/api/assets/v2/workspaces/{self.workspace.slug}/projects/{self.project_id}/{self.id}/"
return (
f"/api/assets/v2/workspaces/{self.workspace.slug}/projects/"
f"{self.project_id}/{self.id}/"
)
return None
+4 -2
View File
@@ -210,7 +210,8 @@ 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:
@@ -553,7 +554,8 @@ 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)
+2 -1
View File
@@ -27,7 +27,8 @@ 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),
+2 -1
View File
@@ -155,7 +155,8 @@ 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.
+2 -1
View File
@@ -98,7 +98,8 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
from django.db import connection
print(
f"{request.method} - {request.get_full_path()} of Queries: {len(connection.queries)}"
f"{request.method} - {request.get_full_path()} of Queries: "
f"{len(connection.queries)}"
)
return response
@@ -167,7 +167,8 @@ 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,
)
+2 -1
View File
@@ -155,7 +155,8 @@ class ReadReplicaRoutingMiddleware:
# provides extra protection specifically for view exceptions
clear_read_replica_context()
logger.debug(
f"Cleaned up read replica context due to exception: {type(exception).__name__}"
f"Cleaned up read replica context due to exception: "
f"{type(exception).__name__}"
)
# Return None to let the exception continue propagating
+4 -2
View File
@@ -22,7 +22,8 @@ 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":
@@ -136,6 +137,7 @@ 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
+6 -2
View File
@@ -74,7 +74,9 @@ 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,
}
@@ -199,7 +201,9 @@ 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"},
+10 -5
View File
@@ -19,7 +19,8 @@ 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:
@@ -47,7 +48,8 @@ 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
@@ -59,7 +61,8 @@ class MongoConnection:
logger.info("MongoDB connection established successfully")
except Exception as e:
logger.warning(
f"Failed to initialize MongoDB connection: {str(e)}. MongoDB functionality will be disabled."
f"Failed to initialize MongoDB connection: {str(e)}. "
f"MongoDB functionality will be disabled."
)
return cls._instance
@@ -96,13 +99,15 @@ 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}': MongoDB not configured"
f"Cannot access collection '{collection_name}': "
f"MongoDB not configured"
)
return None
return db[collection_name]
+69 -42
View File
@@ -52,14 +52,16 @@ 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
@@ -67,14 +69,16 @@ 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
@@ -82,14 +86,16 @@ 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
@@ -97,14 +103,16 @@ 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
@@ -112,14 +120,16 @@ 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
@@ -127,14 +137,16 @@ 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
@@ -142,14 +154,16 @@ 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
@@ -157,14 +171,16 @@ 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
@@ -172,14 +188,15 @@ 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
@@ -187,70 +204,80 @@ 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."
),
},
],
+2 -1
View File
@@ -22,7 +22,8 @@ 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.
+3 -1
View File
@@ -304,7 +304,9 @@ class IssueCreateSerializer(BaseSerializer):
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": "Invalid binary data"}
)
return data
+2 -1
View File
@@ -96,7 +96,8 @@ 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,
+4 -2
View File
@@ -106,7 +106,8 @@ class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
from django.db import connection
print(
f"{request.method} - {request.get_full_path()} of Queries: {len(connection.queries)}"
f"{request.method} - {request.get_full_path()} of Queries: "
f"{len(connection.queries)}"
)
return response
@@ -191,7 +192,8 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
from django.db import connection
print(
f"{request.method} - {request.get_full_path()} of Queries: {len(connection.queries)}"
f"{request.method} - {request.get_full_path()} of Queries: "
f"{len(connection.queries)}"
)
return response
+2 -1
View File
@@ -148,7 +148,8 @@ class ProjectIssuesPublicEndpoint(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,
)
+2 -1
View File
@@ -62,7 +62,8 @@ def api_key_client(api_client, api_token):
@pytest.fixture
def session_client(api_client, create_user):
"""Return a session authenticated API client for app API testing, which is what plane.app uses"""
"""Return a session authenticated API client for app API testing, which
is what plane.app uses"""
api_client.force_authenticate(user=create_user)
return api_client
+2 -1
View File
@@ -52,7 +52,8 @@ def mock_mongodb():
"""
Mock MongoDB for testing without actual MongoDB connection.
This fixture patches PyMongo to return a MagicMock that behaves like a MongoDB client.
This fixture patches PyMongo to return a MagicMock that behaves like a
MongoDB client.
"""
# Create mock MongoDB clients and collections
mock_mongo_client = MagicMock()
@@ -1,6 +1,5 @@
import pytest
from rest_framework import status
from django.db import IntegrityError
from uuid import uuid4
from plane.db.models import Label, Project, ProjectMember
@@ -159,7 +158,10 @@ class TestLabelDetailAPIEndpoint:
def get_label_detail_url(self, workspace_slug, project_id, label_id):
"""Helper to get label detail endpoint URL"""
return f"/api/v1/workspaces/{workspace_slug}/projects/{project_id}/labels/{label_id}/"
return (
f"/api/v1/workspaces/{workspace_slug}/projects/{project_id}/"
f"labels/{label_id}/"
)
@pytest.mark.django_db
def test_get_label_success(self, api_key_client, workspace, project, create_label):
@@ -39,7 +39,10 @@ def setup_instance(db):
def django_client():
"""Return a Django test client with User-Agent header for handling redirects"""
client = Client(
HTTP_USER_AGENT="Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:15.0) Gecko/20100101 Firefox/15.0.1"
HTTP_USER_AGENT=(
"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:15.0) "
"Gecko/20100101 Firefox/15.0.1"
)
)
return client
@@ -216,7 +219,8 @@ class TestSignInEndpoint:
assert "error_code" not in response.url
# In a real browser, the next_path would be used to build the absolute URL
# Since we're just testing the authentication logic, we won't check for the exact URL structure
# Since we're just testing the authentication logic, we won't check for
# the exact URL structure
# Instead, just verify that we're authenticated
assert "_auth_user_id" in django_client.session
@@ -291,7 +295,8 @@ class TestMagicSignIn:
# Check that the token generation was successful
assert response.status_code == status.HTTP_200_OK
# Since we're mocking the magic_link task, we need to manually get the token from Redis
# Since we're mocking the magic_link task, we need to manually get
# the token from Redis
ri = redis_instance()
user_data = json.loads(ri.get("magic_user@plane.so"))
token = user_data["token"]
@@ -322,7 +327,8 @@ class TestMagicSignIn:
# Check that the token generation was successful
assert response.status_code == status.HTTP_200_OK
# Since we're mocking the magic_link task, we need to manually get the token from Redis
# Since we're mocking the magic_link task, we need to manually get
# the token from Redis
ri = redis_instance()
user_data = json.loads(ri.get("magic_user@plane.so"))
token = user_data["token"]
@@ -407,7 +413,8 @@ class TestMagicSignUp:
# Check that the token generation was successful
assert response.status_code == status.HTTP_200_OK
# Since we're mocking the magic_link task, we need to manually get the token from Redis
# Since we're mocking the magic_link task, we need to manually get
# the token from Redis
ri = redis_instance()
user_data = json.loads(ri.get(f"magic_{email}"))
token = user_data["token"]
@@ -443,7 +450,8 @@ class TestMagicSignUp:
# Check that the token generation was successful
assert response.status_code == status.HTTP_200_OK
# Since we're mocking the magic_link task, we need to manually get the token from Redis
# Since we're mocking the magic_link task, we need to manually get
# the token from Redis
ri = redis_instance()
user_data = json.loads(ri.get(f"magic_{email}"))
token = user_data["token"]
@@ -460,7 +468,8 @@ class TestMagicSignUp:
assert "error_code" not in response.url
# In a real browser, the next_path would be used to build the absolute URL
# Since we're just testing the authentication logic, we won't check for the exact URL structure
# Since we're just testing the authentication logic, we won't check for
# the exact URL structure
# Check if user was created
assert User.objects.filter(email=email).exists()
@@ -26,7 +26,8 @@ class TestWorkspaceAPI:
):
"""Test creating a workspace with valid data"""
url = reverse("workspace")
user = create_user # Use the create_user fixture directly as it returns a user object
user = create_user # Use the create_user fixture directly as it
# returns a user object
# Test with valid data - include all required fields
workspace_data = {
@@ -75,5 +76,6 @@ class TestWorkspaceAPI:
# The API returns 400 BAD REQUEST for duplicate slugs, not 409 CONFLICT
assert response.status_code == status.HTTP_400_BAD_REQUEST
# Optionally check the error message to confirm it's related to the duplicate slug
# Optionally check the error message to confirm it's related to the
# duplicate slug
assert "slug" in response.data
@@ -27,7 +27,8 @@ class TestAuthSmoke:
assert response.status_code != 404, "Authentication endpoint should exist"
if response.status_code == 200:
# If API returns 200 for failures, check the response body for error indication
# If API returns 200 for failures, check the response body for
# error indication
if hasattr(response, "json"):
try:
data = response.json()
@@ -57,7 +58,8 @@ class TestAuthSmoke:
# Successful auth should not be a client error or server error
assert response.status_code not in range(400, 600), (
f"Authentication with valid credentials failed with status {response.status_code}"
f"Authentication with valid credentials failed with status "
f"{response.status_code}"
)
# Specific validation based on response type
@@ -27,7 +27,11 @@ class TestCopyS3Objects:
name="Test Issue",
workspace=workspace,
project_id=project.id,
description_html=f'<div><image-component src="35e8b958-6ee5-43ce-ae56-fb0e776f421e"></image-component><image-component src="97988198-274f-4dfe-aa7a-4c0ffc684214"></image-component></div>',
description_html=(
'<div><image-component src="35e8b958-6ee5-43ce-ae56-'
'fb0e776f421e"></image-component><image-component src="'
'97988198-274f-4dfe-aa7a-4c0ffc684214"></image-component></div>'
),
)
@pytest.fixture
+4 -3
View File
@@ -2,7 +2,6 @@ import pytest
from plane.utils.url import (
contains_url,
is_valid_url,
get_url_components,
normalize_url_path,
)
@@ -103,7 +102,8 @@ class TestContainsURL:
def test_contains_url_line_length_scenarios(self):
"""Test contains_url with realistic line length scenarios"""
# Test with multiline input where total is under 1000 but we test line processing
# Test with multiline input where total is under 1000 but we test
# line processing
# Short lines with URL
multiline_short = "Line 1\nLine 2 with https://example.com\nLine 3"
assert contains_url(multiline_short) is True
@@ -118,7 +118,8 @@ class TestContainsURL:
def test_contains_url_total_length_vs_line_length(self):
"""Test the interaction between total length limit and line processing"""
# Test that total length limit takes precedence
# Even if individual lines would be processed, total > 1000 means immediate False
# Even if individual lines would be processed, total > 1000 means
# immediate False
over_limit_text = "a" * 1001 # No URL, but over total limit
assert contains_url(over_limit_text) is False
+2 -1
View File
@@ -58,7 +58,8 @@ def invalidate_cache_directly(
):
if url_params and path:
path_with_values = path
# Assuming `kwargs` could be passed directly if needed, otherwise, skip this part
# Assuming `kwargs` could be passed directly if needed, otherwise,
# skip this part
for key, value in request.resolver_match.kwargs.items():
path_with_values = path_with_values.replace(f":{key}", str(value))
custom_path = path_with_values
+2 -1
View File
@@ -147,7 +147,8 @@ def get_analytics_filters(
user: The current user
type: The type of filter ("analytics" or "chart")
date_filter: Optional date filter string
project_ids: Optional list of project IDs or comma-separated string of project IDs
project_ids: Optional list of project IDs or comma-separated string of
project IDs
Returns:
dict: A dictionary containing:
+4 -2
View File
@@ -10,7 +10,8 @@ from drf_spectacular.extensions import OpenApiAuthenticationExtension
class APIKeyAuthenticationExtension(OpenApiAuthenticationExtension):
"""
OpenAPI authentication extension for plane.api.middleware.api_authentication.APIKeyAuthentication
OpenAPI authentication extension for
plane.api.middleware.api_authentication.APIKeyAuthentication
"""
target_class = "plane.api.middleware.api_authentication.APIKeyAuthentication"
@@ -25,5 +26,6 @@ class APIKeyAuthenticationExtension(OpenApiAuthenticationExtension):
"type": "apiKey",
"in": "header",
"name": "X-API-Key",
"description": "API key authentication. Provide your API key in the X-API-Key header.",
"description": "API key authentication. Provide your API key in the "
"X-API-Key header.",
}
+14 -4
View File
@@ -499,7 +499,9 @@ ISSUE_COMMENT_EXAMPLE = OpenApiExample(
name="IssueComment",
value={
"id": "550e8400-e29b-41d4-a716-446655440000",
"comment_html": "<p>This issue has been resolved by implementing OAuth 2.0 flow.</p>",
"comment_html": (
"<p>This issue has been resolved by implementing OAuth 2.0 flow.</p>"
),
"comment_json": {
"type": "doc",
"content": [
@@ -508,7 +510,10 @@ ISSUE_COMMENT_EXAMPLE = OpenApiExample(
"content": [
{
"type": "text",
"text": "This issue has been resolved by implementing OAuth 2.0 flow.",
"text": (
"This issue has been resolved by implementing "
"OAuth 2.0 flow."
),
}
],
}
@@ -551,7 +556,9 @@ ISSUE_ATTACHMENT_NOT_UPLOADED_EXAMPLE = OpenApiExample(
"error": "The asset is not uploaded.",
"status": False,
},
description="Error when trying to download an attachment that hasn't been uploaded yet",
description=(
"Error when trying to download an attachment that hasn't been uploaded yet"
),
)
# Intake Issue Response Examples
@@ -733,7 +740,10 @@ SAMPLE_STATE = {
SAMPLE_COMMENT = {
"id": "550e8400-e29b-41d4-a716-446655440000",
"comment_html": "<p>This issue needs more investigation. I'll look into the database connection timeout.</p>",
"comment_html": (
"<p>This issue needs more investigation. I'll look into the "
"database connection timeout.</p>"
),
"created_at": "2024-01-15T14:20:00Z",
"actor": {"id": "550e8400-e29b-41d4-a716-446655440002", "display_name": "John Doe"},
}
+4 -1
View File
@@ -336,7 +336,10 @@ ORDER_BY_PARAMETER = OpenApiParameter(
OpenApiExample(
name="State group",
value="state__group",
description="Order by state group (backlog, unstarted, started, completed, cancelled)",
description=(
"Order by state group (backlog, unstarted, started, "
"completed, cancelled)"
),
),
OpenApiExample(
name="Assignee name",
+4 -1
View File
@@ -221,7 +221,10 @@ EXTERNAL_ID_EXISTS_RESPONSE = OpenApiResponse(
OpenApiExample(
name="External ID Exists",
value={
"error": "Resource with the same external id and external source already exists",
"error": (
"Resource with the same external id and external source "
"already exists"
),
"id": "550e8400-e29b-41d4-a716-446655440000",
},
)
+4 -2
View File
@@ -221,7 +221,8 @@ class GroupedOffsetPaginator(OffsetPaginator):
self.group_by_field_name = group_by_field_name
# Set the group by fields
self.group_by_fields = group_by_fields
# Set the count filter - this are extra filters that need to be passed to calculate the counts with the filters
# Set the count filter - this are extra filters that need to be
# passed to calculate the counts with the filters
self.count_filter = count_filter
def get_result(self, limit=50, cursor=None):
@@ -434,7 +435,8 @@ class SubGroupedOffsetPaginator(OffsetPaginator):
self.sub_group_by_field_name = sub_group_by_field_name
self.sub_group_by_fields = sub_group_by_fields
# Set the count filter - this are extra filters that need to be passed to calculate the counts with the filters
# Set the count filter - this are extra filters that need to be
# passed to calculate the counts with the filters
self.count_filter = count_filter
def get_result(self, limit=30, cursor=None):
+11 -6
View File
@@ -10,11 +10,13 @@ URL_PATTERN = re.compile(
r"(?:" # Non-capturing group for alternatives
r"https?://[^\s]+" # http:// or https:// followed by non-whitespace
r"|"
r"www\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*" # www.domain with proper length limits
r"www\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9]"
r"(?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*" # www.domain with proper length limits
r"|"
r"(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,6}" # domain.tld with length limits
r"|"
r"(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)" # IP address with proper validation
r"(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}"
r"(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)" # IP address with proper validation
r")"
)
@@ -85,7 +87,8 @@ def get_url_components(url: str) -> Optional[dict]:
Example:
>>> get_url_components("https://example.com/path?query=1")
{'scheme': 'https', 'netloc': 'example.com', 'path': '/path', 'params': '', 'query': 'query=1', 'fragment': ''}
{'scheme': 'https', 'netloc': 'example.com', 'path': '/path',
'params': '', 'query': 'query=1', 'fragment': ''}
"""
if not is_valid_url(url):
return None
@@ -102,10 +105,12 @@ def get_url_components(url: str) -> Optional[dict]:
def normalize_url_path(url: str) -> str:
"""
Normalize the path component of a URL by replacing multiple consecutive slashes with a single slash.
Normalize the path component of a URL by replacing multiple consecutive
slashes with a single slash.
This function preserves the protocol, domain, query parameters, and fragments of the URL,
only modifying the path portion to ensure there are no duplicate slashes.
This function preserves the protocol, domain, query parameters, and
fragments of the URL, only modifying the path portion to ensure there are
no duplicate slashes.
Args:
url (str): The input URL string to normalize.