Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d2110d3318 |
@@ -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.
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,4 +1,3 @@
|
||||
from lxml import html
|
||||
|
||||
# Django imports
|
||||
from django.utils import timezone
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from lxml import html
|
||||
|
||||
# Django imports
|
||||
from django.utils import timezone
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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("/"),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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"])
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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/",
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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."
|
||||
),
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.",
|
||||
}
|
||||
|
||||
@@ -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"},
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user