From e6ecfcbbc7e4e40603e7f2bc6f629207980411e1 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Tue, 25 Jun 2024 21:18:01 +0530 Subject: [PATCH] dev: update urls to use project identifiers and issue sequence --- apiserver/plane/app/serializers/project.py | 53 ++++++++++------ apiserver/plane/app/urls/cycle.py | 24 +++---- apiserver/plane/app/urls/estimate.py | 10 +-- apiserver/plane/app/urls/external.py | 9 ++- apiserver/plane/app/urls/inbox.py | 8 +-- apiserver/plane/app/urls/issue.py | 62 +++++++++---------- apiserver/plane/app/urls/module.py | 26 ++++---- apiserver/plane/app/urls/page.py | 18 +++--- apiserver/plane/app/urls/project.py | 28 ++++----- apiserver/plane/app/urls/search.py | 2 +- apiserver/plane/app/urls/state.py | 6 +- apiserver/plane/app/urls/views.py | 8 +-- apiserver/plane/app/views/base.py | 50 ++++++++++++++- apiserver/plane/app/views/issue/base.py | 19 ++++-- apiserver/plane/app/views/project/base.py | 24 +++++++ ...ojectidentifier_current_active_and_more.py | 41 ++++++++++++ apiserver/plane/db/models/project.py | 6 +- apiserver/plane/settings/local.py | 5 +- apiserver/plane/utils/valid_int_uuid.py | 20 ++++++ 19 files changed, 289 insertions(+), 130 deletions(-) create mode 100644 apiserver/plane/db/migrations/0070_projectidentifier_current_active_and_more.py create mode 100644 apiserver/plane/utils/valid_int_uuid.py diff --git a/apiserver/plane/app/serializers/project.py b/apiserver/plane/app/serializers/project.py index 1bbc580c11..8024bc27fa 100644 --- a/apiserver/plane/app/serializers/project.py +++ b/apiserver/plane/app/serializers/project.py @@ -44,12 +44,16 @@ class ProjectSerializer(BaseSerializer): detail="Project Identifier is taken" ) project = Project.objects.create( - **validated_data, workspace_id=self.context["workspace_id"] + **validated_data, + workspace_id=self.context["workspace_id"], ) _ = ProjectIdentifier.objects.create( name=project.identifier, project=project, workspace_id=self.context["workspace_id"], + created_by_id=project.created_by_id, + updated_by_id=project.updated_by_id, + current_active=project.identifier, ) return project @@ -61,29 +65,38 @@ class ProjectSerializer(BaseSerializer): project = super().update(instance, validated_data) return project - # If no Project Identifier is found create it - project_identifier = ProjectIdentifier.objects.filter( - name=identifier, workspace_id=instance.workspace_id - ).first() - if project_identifier is None: - project = super().update(instance, validated_data) - project_identifier = ProjectIdentifier.objects.filter( - project=project - ).first() - if project_identifier is not None: - project_identifier.name = identifier - project_identifier.save() - return project - # If found check if the project_id to be updated and identifier project id is same - if project_identifier.project_id == instance.id: - # If same pass update + # When updating check if the project identifier is different + if instance.identifier == identifier: project = super().update(instance, validated_data) return project - # If not same fail update - raise serializers.ValidationError( - detail="Project Identifier is already taken" + # Check if this project identifier is already taken in the current workspace + if ProjectIdentifier.objects.filter( + name=identifier, + workspace_id=instance.workspace_id, + ).exists(): + raise serializers.ValidationError( + detail="Project Identifier is already taken" + ) + + # Update the project identifier + project = super().update(instance, validated_data) + # If no Project Identifier is found create it and deactivate the old ones + ProjectIdentifier.objects.filter( + project_id=instance.id, + workspace_id=instance.workspace_id, + ).update(current_active=identifier) + # Create new project identifier + ProjectIdentifier.objects.create( + name=identifier, + project=instance, + workspace_id=instance.workspace_id, + created_by_id=project.created_by_id, + updated_by_id=project.updated_by_id, + current_active=identifier, ) + # Return the updated project + return project class ProjectLiteSerializer(BaseSerializer): diff --git a/apiserver/plane/app/urls/cycle.py b/apiserver/plane/app/urls/cycle.py index ce2e0f6dcc..88b8a2caba 100644 --- a/apiserver/plane/app/urls/cycle.py +++ b/apiserver/plane/app/urls/cycle.py @@ -14,7 +14,7 @@ from plane.app.views import ( urlpatterns = [ path( - "workspaces//projects//cycles/", + "workspaces//projects//cycles/", CycleViewSet.as_view( { "get": "list", @@ -24,7 +24,7 @@ urlpatterns = [ name="project-cycle", ), path( - "workspaces//projects//cycles//", + "workspaces//projects//cycles//", CycleViewSet.as_view( { "get": "retrieve", @@ -36,7 +36,7 @@ urlpatterns = [ name="project-cycle", ), path( - "workspaces//projects//cycles//cycle-issues/", + "workspaces//projects//cycles//cycle-issues/", CycleIssueViewSet.as_view( { "get": "list", @@ -46,7 +46,7 @@ urlpatterns = [ name="project-issue-cycle", ), path( - "workspaces//projects//cycles//cycle-issues//", + "workspaces//projects//cycles//cycle-issues//", CycleIssueViewSet.as_view( { "get": "retrieve", @@ -58,12 +58,12 @@ urlpatterns = [ name="project-issue-cycle", ), path( - "workspaces//projects//cycles/date-check/", + "workspaces//projects//cycles/date-check/", CycleDateCheckEndpoint.as_view(), name="project-cycle-date", ), path( - "workspaces//projects//user-favorite-cycles/", + "workspaces//projects//user-favorite-cycles/", CycleFavoriteViewSet.as_view( { "get": "list", @@ -73,7 +73,7 @@ urlpatterns = [ name="user-favorite-cycle", ), path( - "workspaces//projects//user-favorite-cycles//", + "workspaces//projects//user-favorite-cycles//", CycleFavoriteViewSet.as_view( { "delete": "destroy", @@ -82,27 +82,27 @@ urlpatterns = [ name="user-favorite-cycle", ), path( - "workspaces//projects//cycles//transfer-issues/", + "workspaces//projects//cycles//transfer-issues/", TransferCycleIssueEndpoint.as_view(), name="transfer-issues", ), path( - "workspaces//projects//cycles//user-properties/", + "workspaces//projects//cycles//user-properties/", CycleUserPropertiesEndpoint.as_view(), name="cycle-user-filters", ), path( - "workspaces//projects//cycles//archive/", + "workspaces//projects//cycles//archive/", CycleArchiveUnarchiveEndpoint.as_view(), name="cycle-archive-unarchive", ), path( - "workspaces//projects//archived-cycles/", + "workspaces//projects//archived-cycles/", CycleArchiveUnarchiveEndpoint.as_view(), name="cycle-archive-unarchive", ), path( - "workspaces//projects//archived-cycles//", + "workspaces//projects//archived-cycles//", CycleArchiveUnarchiveEndpoint.as_view(), name="cycle-archive-unarchive", ), diff --git a/apiserver/plane/app/urls/estimate.py b/apiserver/plane/app/urls/estimate.py index 7db94aa46c..8e4d4dc0c0 100644 --- a/apiserver/plane/app/urls/estimate.py +++ b/apiserver/plane/app/urls/estimate.py @@ -10,12 +10,12 @@ from plane.app.views import ( urlpatterns = [ path( - "workspaces//projects//project-estimates/", + "workspaces//projects//project-estimates/", ProjectEstimatePointEndpoint.as_view(), name="project-estimate-points", ), path( - "workspaces//projects//estimates/", + "workspaces//projects//estimates/", BulkEstimatePointEndpoint.as_view( { "get": "list", @@ -25,7 +25,7 @@ urlpatterns = [ name="bulk-create-estimate-points", ), path( - "workspaces//projects//estimates//", + "workspaces//projects//estimates//", BulkEstimatePointEndpoint.as_view( { "get": "retrieve", @@ -36,7 +36,7 @@ urlpatterns = [ name="bulk-create-estimate-points", ), path( - "workspaces//projects//estimates//estimate-points/", + "workspaces//projects//estimates//estimate-points/", EstimatePointEndpoint.as_view( { "post": "create", @@ -45,7 +45,7 @@ urlpatterns = [ name="estimate-points", ), path( - "workspaces//projects//estimates//estimate-points//", + "workspaces//projects//estimates//estimate-points//", EstimatePointEndpoint.as_view( { "patch": "partial_update", diff --git a/apiserver/plane/app/urls/external.py b/apiserver/plane/app/urls/external.py index 744c646ca4..b7c4d28cf8 100644 --- a/apiserver/plane/app/urls/external.py +++ b/apiserver/plane/app/urls/external.py @@ -2,7 +2,10 @@ from django.urls import path from plane.app.views import UnsplashEndpoint -from plane.app.views import GPTIntegrationEndpoint, WorkspaceGPTIntegrationEndpoint +from plane.app.views import ( + GPTIntegrationEndpoint, + WorkspaceGPTIntegrationEndpoint, +) urlpatterns = [ @@ -12,11 +15,11 @@ urlpatterns = [ name="unsplash", ), path( - "workspaces//projects//ai-assistant/", + "workspaces//projects//ai-assistant/", GPTIntegrationEndpoint.as_view(), name="importer", ), - path( + path( "workspaces//ai-assistant/", WorkspaceGPTIntegrationEndpoint.as_view(), name="importer", diff --git a/apiserver/plane/app/urls/inbox.py b/apiserver/plane/app/urls/inbox.py index b6848244ba..92a26d35ab 100644 --- a/apiserver/plane/app/urls/inbox.py +++ b/apiserver/plane/app/urls/inbox.py @@ -9,7 +9,7 @@ from plane.app.views import ( urlpatterns = [ path( - "workspaces//projects//inboxes/", + "workspaces//projects//inboxes/", InboxViewSet.as_view( { "get": "list", @@ -19,7 +19,7 @@ urlpatterns = [ name="inbox", ), path( - "workspaces//projects//inboxes//", + "workspaces//projects//inboxes//", InboxViewSet.as_view( { "get": "retrieve", @@ -30,7 +30,7 @@ urlpatterns = [ name="inbox", ), path( - "workspaces//projects//inbox-issues/", + "workspaces//projects//inbox-issues/", InboxIssueViewSet.as_view( { "get": "list", @@ -40,7 +40,7 @@ urlpatterns = [ name="inbox-issue", ), path( - "workspaces//projects//inbox-issues//", + "workspaces//projects//inbox-issues//", InboxIssueViewSet.as_view( { "get": "retrieve", diff --git a/apiserver/plane/app/urls/issue.py b/apiserver/plane/app/urls/issue.py index b7a4eaa480..e5c2d127d2 100644 --- a/apiserver/plane/app/urls/issue.py +++ b/apiserver/plane/app/urls/issue.py @@ -25,12 +25,12 @@ from plane.app.views import ( urlpatterns = [ path( - "workspaces//projects//issues/list/", + "workspaces//projects//issues/list/", IssueListEndpoint.as_view(), name="project-issue", ), path( - "workspaces//projects//issues/", + "workspaces//projects//issues/", IssueViewSet.as_view( { "get": "list", @@ -40,7 +40,7 @@ urlpatterns = [ name="project-issue", ), path( - "workspaces//projects//issues//", + "workspaces//projects//issues//", IssueViewSet.as_view( { "get": "retrieve", @@ -52,7 +52,7 @@ urlpatterns = [ name="project-issue", ), path( - "workspaces//projects//issue-labels/", + "workspaces//projects//issue-labels/", LabelViewSet.as_view( { "get": "list", @@ -62,7 +62,7 @@ urlpatterns = [ name="project-issue-labels", ), path( - "workspaces//projects//issue-labels//", + "workspaces//projects//issue-labels//", LabelViewSet.as_view( { "get": "retrieve", @@ -74,28 +74,28 @@ urlpatterns = [ name="project-issue-labels", ), path( - "workspaces//projects//bulk-create-labels/", + "workspaces//projects//bulk-create-labels/", BulkCreateIssueLabelsEndpoint.as_view(), name="project-bulk-labels", ), path( - "workspaces//projects//bulk-delete-issues/", + "workspaces//projects//bulk-delete-issues/", BulkDeleteIssuesEndpoint.as_view(), name="project-issues-bulk", ), path( - "workspaces//projects//bulk-archive-issues/", + "workspaces//projects//bulk-archive-issues/", BulkArchiveIssuesEndpoint.as_view(), name="bulk-archive-issues", ), ## path( - "workspaces//projects//issues//sub-issues/", + "workspaces//projects//issues//sub-issues/", SubIssuesEndpoint.as_view(), name="sub-issues", ), path( - "workspaces//projects//issues//issue-links/", + "workspaces//projects//issues//issue-links/", IssueLinkViewSet.as_view( { "get": "list", @@ -105,7 +105,7 @@ urlpatterns = [ name="project-issue-links", ), path( - "workspaces//projects//issues//issue-links//", + "workspaces//projects//issues//issue-links//", IssueLinkViewSet.as_view( { "get": "retrieve", @@ -117,12 +117,12 @@ urlpatterns = [ name="project-issue-links", ), path( - "workspaces//projects//issues//issue-attachments/", + "workspaces//projects//issues//issue-attachments/", IssueAttachmentEndpoint.as_view(), name="project-issue-attachments", ), path( - "workspaces//projects//issues//issue-attachments//", + "workspaces//projects//issues//issue-attachments//", IssueAttachmentEndpoint.as_view(), name="project-issue-attachments", ), @@ -134,14 +134,14 @@ urlpatterns = [ ## End Issues ## Issue Activity path( - "workspaces//projects//issues//history/", + "workspaces//projects//issues//history/", IssueActivityEndpoint.as_view(), name="project-issue-history", ), ## Issue Activity ## IssueComments path( - "workspaces//projects//issues//comments/", + "workspaces//projects//issues//comments/", IssueCommentViewSet.as_view( { "get": "list", @@ -151,7 +151,7 @@ urlpatterns = [ name="project-issue-comment", ), path( - "workspaces//projects//issues//comments//", + "workspaces//projects//issues//comments//", IssueCommentViewSet.as_view( { "get": "retrieve", @@ -165,7 +165,7 @@ urlpatterns = [ ## End IssueComments # Issue Subscribers path( - "workspaces//projects//issues//issue-subscribers/", + "workspaces//projects//issues//issue-subscribers/", IssueSubscriberViewSet.as_view( { "get": "list", @@ -175,12 +175,12 @@ urlpatterns = [ name="project-issue-subscribers", ), path( - "workspaces//projects//issues//issue-subscribers//", + "workspaces//projects//issues//issue-subscribers//", IssueSubscriberViewSet.as_view({"delete": "destroy"}), name="project-issue-subscribers", ), path( - "workspaces//projects//issues//subscribe/", + "workspaces//projects//issues//subscribe/", IssueSubscriberViewSet.as_view( { "get": "subscription_status", @@ -193,7 +193,7 @@ urlpatterns = [ ## End Issue Subscribers # Issue Reactions path( - "workspaces//projects//issues//reactions/", + "workspaces//projects//issues//reactions/", IssueReactionViewSet.as_view( { "get": "list", @@ -203,7 +203,7 @@ urlpatterns = [ name="project-issue-reactions", ), path( - "workspaces//projects//issues//reactions//", + "workspaces//projects//issues//reactions//", IssueReactionViewSet.as_view( { "delete": "destroy", @@ -214,7 +214,7 @@ urlpatterns = [ ## End Issue Reactions # Comment Reactions path( - "workspaces//projects//comments//reactions/", + "workspaces//projects//comments//reactions/", CommentReactionViewSet.as_view( { "get": "list", @@ -224,7 +224,7 @@ urlpatterns = [ name="project-issue-comment-reactions", ), path( - "workspaces//projects//comments//reactions//", + "workspaces//projects//comments//reactions//", CommentReactionViewSet.as_view( { "delete": "destroy", @@ -235,14 +235,14 @@ urlpatterns = [ ## End Comment Reactions ## IssueProperty path( - "workspaces//projects//user-properties/", + "workspaces//projects//user-properties/", IssueUserDisplayPropertyEndpoint.as_view(), name="project-issue-display-properties", ), ## IssueProperty End ## Issue Archives path( - "workspaces//projects//archived-issues/", + "workspaces//projects//archived-issues/", IssueArchiveViewSet.as_view( { "get": "list", @@ -251,7 +251,7 @@ urlpatterns = [ name="project-issue-archive", ), path( - "workspaces//projects//issues//archive/", + "workspaces//projects//issues//archive/", IssueArchiveViewSet.as_view( { "get": "retrieve", @@ -264,7 +264,7 @@ urlpatterns = [ ## End Issue Archives ## Issue Relation path( - "workspaces//projects//issues//issue-relation/", + "workspaces//projects//issues//issue-relation/", IssueRelationViewSet.as_view( { "get": "list", @@ -274,7 +274,7 @@ urlpatterns = [ name="issue-relation", ), path( - "workspaces//projects//issues//remove-relation/", + "workspaces//projects//issues//remove-relation/", IssueRelationViewSet.as_view( { "post": "remove_relation", @@ -285,7 +285,7 @@ urlpatterns = [ ## End Issue Relation ## Issue Drafts path( - "workspaces//projects//issue-drafts/", + "workspaces//projects//issue-drafts/", IssueDraftViewSet.as_view( { "get": "list", @@ -295,7 +295,7 @@ urlpatterns = [ name="project-issue-draft", ), path( - "workspaces//projects//issue-drafts//", + "workspaces//projects//issue-drafts//", IssueDraftViewSet.as_view( { "get": "retrieve", @@ -306,7 +306,7 @@ urlpatterns = [ name="project-issue-draft", ), path( - "workspaces//projects//bulk-operation-issues/", + "workspaces//projects//bulk-operation-issues/", BulkIssueOperationsEndpoint.as_view(), name="bulk-operations-issues", ), diff --git a/apiserver/plane/app/urls/module.py b/apiserver/plane/app/urls/module.py index bf6c84b2f2..d1de7edd3f 100644 --- a/apiserver/plane/app/urls/module.py +++ b/apiserver/plane/app/urls/module.py @@ -13,7 +13,7 @@ from plane.app.views import ( urlpatterns = [ path( - "workspaces//projects//modules/", + "workspaces//projects//modules/", ModuleViewSet.as_view( { "get": "list", @@ -23,7 +23,7 @@ urlpatterns = [ name="project-modules", ), path( - "workspaces//projects//modules//", + "workspaces//projects//modules//", ModuleViewSet.as_view( { "get": "retrieve", @@ -35,7 +35,7 @@ urlpatterns = [ name="project-modules", ), path( - "workspaces//projects//issues//modules/", + "workspaces//projects//issues//modules/", ModuleIssueViewSet.as_view( { "post": "create_issue_modules", @@ -44,7 +44,7 @@ urlpatterns = [ name="issue-module", ), path( - "workspaces//projects//modules//issues/", + "workspaces//projects//modules//issues/", ModuleIssueViewSet.as_view( { "post": "create_module_issues", @@ -54,7 +54,7 @@ urlpatterns = [ name="project-module-issues", ), path( - "workspaces//projects//modules//issues//", + "workspaces//projects//modules//issues//", ModuleIssueViewSet.as_view( { "get": "retrieve", @@ -66,7 +66,7 @@ urlpatterns = [ name="project-module-issues", ), path( - "workspaces//projects//modules//module-links/", + "workspaces//projects//modules//module-links/", ModuleLinkViewSet.as_view( { "get": "list", @@ -76,7 +76,7 @@ urlpatterns = [ name="project-issue-module-links", ), path( - "workspaces//projects//modules//module-links//", + "workspaces//projects//modules//module-links//", ModuleLinkViewSet.as_view( { "get": "retrieve", @@ -88,7 +88,7 @@ urlpatterns = [ name="project-issue-module-links", ), path( - "workspaces//projects//user-favorite-modules/", + "workspaces//projects//user-favorite-modules/", ModuleFavoriteViewSet.as_view( { "get": "list", @@ -98,7 +98,7 @@ urlpatterns = [ name="user-favorite-module", ), path( - "workspaces//projects//user-favorite-modules//", + "workspaces//projects//user-favorite-modules//", ModuleFavoriteViewSet.as_view( { "delete": "destroy", @@ -107,22 +107,22 @@ urlpatterns = [ name="user-favorite-module", ), path( - "workspaces//projects//modules//user-properties/", + "workspaces//projects//modules//user-properties/", ModuleUserPropertiesEndpoint.as_view(), name="cycle-user-filters", ), path( - "workspaces//projects//modules//archive/", + "workspaces//projects//modules//archive/", ModuleArchiveUnarchiveEndpoint.as_view(), name="module-archive-unarchive", ), path( - "workspaces//projects//archived-modules/", + "workspaces//projects//archived-modules/", ModuleArchiveUnarchiveEndpoint.as_view(), name="module-archive-unarchive", ), path( - "workspaces//projects//archived-modules//", + "workspaces//projects//archived-modules//", ModuleArchiveUnarchiveEndpoint.as_view(), name="module-archive-unarchive", ), diff --git a/apiserver/plane/app/urls/page.py b/apiserver/plane/app/urls/page.py index a6d43600f1..385ee66bc8 100644 --- a/apiserver/plane/app/urls/page.py +++ b/apiserver/plane/app/urls/page.py @@ -12,7 +12,7 @@ from plane.app.views import ( urlpatterns = [ path( - "workspaces//projects//pages/", + "workspaces//projects//pages/", PageViewSet.as_view( { "get": "list", @@ -22,7 +22,7 @@ urlpatterns = [ name="project-pages", ), path( - "workspaces//projects//pages//", + "workspaces//projects//pages//", PageViewSet.as_view( { "get": "retrieve", @@ -34,7 +34,7 @@ urlpatterns = [ ), # favorite pages path( - "workspaces//projects//favorite-pages//", + "workspaces//projects//favorite-pages//", PageFavoriteViewSet.as_view( { "post": "create", @@ -45,7 +45,7 @@ urlpatterns = [ ), # archived pages path( - "workspaces//projects//pages//archive/", + "workspaces//projects//pages//archive/", PageViewSet.as_view( { "post": "archive", @@ -56,7 +56,7 @@ urlpatterns = [ ), # lock and unlock path( - "workspaces//projects//pages//lock/", + "workspaces//projects//pages//lock/", PageViewSet.as_view( { "post": "lock", @@ -66,22 +66,22 @@ urlpatterns = [ name="project-pages-lock-unlock", ), path( - "workspaces//projects//pages//transactions/", + "workspaces//projects//pages//transactions/", PageLogEndpoint.as_view(), name="page-transactions", ), path( - "workspaces//projects//pages//transactions//", + "workspaces//projects//pages//transactions//", PageLogEndpoint.as_view(), name="page-transactions", ), path( - "workspaces//projects//pages//sub-pages/", + "workspaces//projects//pages//sub-pages/", SubPagesEndpoint.as_view(), name="sub-page", ), path( - "workspaces//projects//pages//description/", + "workspaces//projects//pages//description/", PagesDescriptionViewSet.as_view( { "get": "retrieve", diff --git a/apiserver/plane/app/urls/project.py b/apiserver/plane/app/urls/project.py index 0807c7616a..e5a1906643 100644 --- a/apiserver/plane/app/urls/project.py +++ b/apiserver/plane/app/urls/project.py @@ -30,7 +30,7 @@ urlpatterns = [ name="project", ), path( - "workspaces//projects//", + "workspaces//projects//", ProjectViewSet.as_view( { "get": "retrieve", @@ -47,7 +47,7 @@ urlpatterns = [ name="project-identifiers", ), path( - "workspaces//projects//invitations/", + "workspaces//projects//invitations/", ProjectInvitationsViewset.as_view( { "get": "list", @@ -57,7 +57,7 @@ urlpatterns = [ name="project-member-invite", ), path( - "workspaces//projects//invitations//", + "workspaces//projects//invitations//", ProjectInvitationsViewset.as_view( { "get": "retrieve", @@ -82,12 +82,12 @@ urlpatterns = [ name="user-project-roles", ), path( - "workspaces//projects//join//", + "workspaces//projects//join//", ProjectJoinEndpoint.as_view(), name="project-join", ), path( - "workspaces//projects//members/", + "workspaces//projects//members/", ProjectMemberViewSet.as_view( { "get": "list", @@ -97,7 +97,7 @@ urlpatterns = [ name="project-member", ), path( - "workspaces//projects//members//", + "workspaces//projects//members//", ProjectMemberViewSet.as_view( { "get": "retrieve", @@ -108,7 +108,7 @@ urlpatterns = [ name="project-member", ), path( - "workspaces//projects//members/leave/", + "workspaces//projects//members/leave/", ProjectMemberViewSet.as_view( { "post": "leave", @@ -117,17 +117,17 @@ urlpatterns = [ name="project-member", ), path( - "workspaces//projects//team-invite/", + "workspaces//projects//team-invite/", AddTeamToProjectEndpoint.as_view(), name="projects", ), path( - "workspaces//projects//project-views/", + "workspaces//projects//project-views/", ProjectUserViewsEndpoint.as_view(), name="project-view", ), path( - "workspaces//projects//project-members/me/", + "workspaces//projects//project-members/me/", ProjectMemberUserEndpoint.as_view(), name="project-member-view", ), @@ -142,7 +142,7 @@ urlpatterns = [ name="project-favorite", ), path( - "workspaces//user-favorite-projects//", + "workspaces//user-favorite-projects//", ProjectFavoritesViewSet.as_view( { "delete": "destroy", @@ -156,7 +156,7 @@ urlpatterns = [ name="project-covers", ), path( - "workspaces//projects//project-deploy-boards/", + "workspaces//projects//project-deploy-boards/", DeployBoardViewSet.as_view( { "get": "list", @@ -166,7 +166,7 @@ urlpatterns = [ name="project-deploy-board", ), path( - "workspaces//projects//project-deploy-boards//", + "workspaces//projects//project-deploy-boards//", DeployBoardViewSet.as_view( { "get": "retrieve", @@ -177,7 +177,7 @@ urlpatterns = [ name="project-deploy-board", ), path( - "workspaces//projects//archive/", + "workspaces//projects//archive/", ProjectArchiveUnarchiveEndpoint.as_view(), name="project-archive-unarchive", ), diff --git a/apiserver/plane/app/urls/search.py b/apiserver/plane/app/urls/search.py index 05a79994e0..75a376d267 100644 --- a/apiserver/plane/app/urls/search.py +++ b/apiserver/plane/app/urls/search.py @@ -14,7 +14,7 @@ urlpatterns = [ name="global-search", ), path( - "workspaces//projects//search-issues/", + "workspaces//projects//search-issues/", IssueSearchEndpoint.as_view(), name="project-issue-search", ), diff --git a/apiserver/plane/app/urls/state.py b/apiserver/plane/app/urls/state.py index 9fec70ea1a..eeb07e0ac0 100644 --- a/apiserver/plane/app/urls/state.py +++ b/apiserver/plane/app/urls/state.py @@ -6,7 +6,7 @@ from plane.app.views import StateViewSet urlpatterns = [ path( - "workspaces//projects//states/", + "workspaces//projects//states/", StateViewSet.as_view( { "get": "list", @@ -16,7 +16,7 @@ urlpatterns = [ name="project-states", ), path( - "workspaces//projects//states//", + "workspaces//projects//states//", StateViewSet.as_view( { "get": "retrieve", @@ -27,7 +27,7 @@ urlpatterns = [ name="project-state", ), path( - "workspaces//projects//states//mark-default/", + "workspaces//projects//states//mark-default/", StateViewSet.as_view( { "post": "mark_as_default", diff --git a/apiserver/plane/app/urls/views.py b/apiserver/plane/app/urls/views.py index a2f8e2ac88..ce0ff22619 100644 --- a/apiserver/plane/app/urls/views.py +++ b/apiserver/plane/app/urls/views.py @@ -11,7 +11,7 @@ from plane.app.views import ( urlpatterns = [ path( - "workspaces//projects//views/", + "workspaces//projects//views/", IssueViewViewSet.as_view( { "get": "list", @@ -21,7 +21,7 @@ urlpatterns = [ name="project-view", ), path( - "workspaces//projects//views//", + "workspaces//projects//views//", IssueViewViewSet.as_view( { "get": "retrieve", @@ -64,7 +64,7 @@ urlpatterns = [ name="global-view-issues", ), path( - "workspaces//projects//user-favorite-views/", + "workspaces//projects//user-favorite-views/", IssueViewFavoriteViewSet.as_view( { "get": "list", @@ -74,7 +74,7 @@ urlpatterns = [ name="user-favorite-view", ), path( - "workspaces//projects//user-favorite-views//", + "workspaces//projects//user-favorite-views//", IssueViewFavoriteViewSet.as_view( { "delete": "destroy", diff --git a/apiserver/plane/app/views/base.py b/apiserver/plane/app/views/base.py index 45488b64e9..6d129043ff 100644 --- a/apiserver/plane/app/views/base.py +++ b/apiserver/plane/app/views/base.py @@ -1,12 +1,12 @@ # Python imports import traceback - +import uuid import zoneinfo + +# Django imports from django.conf import settings from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.db import IntegrityError - -# Django imports from django.urls import resolve from django.utils import timezone from django_filters.rest_framework import DjangoFilterBackend @@ -24,6 +24,7 @@ from rest_framework.viewsets import ModelViewSet from plane.authentication.session import BaseSessionAuthentication from plane.utils.exception_logger import log_exception from plane.utils.paginator import BasePaginator +from plane.db.models import ProjectIdentifier class TimezoneMixin: @@ -60,6 +61,38 @@ class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator): search_fields = [] + def is_uuid(self, value): + try: + # Check if the value is a valid UUID + uuid.UUID(str(value)) + return True + except ValueError: + return False + + def get_project_id(self, project_id): + # If the project_id is present and is not a UUID then get the project_id from the project_identifier + if project_id and not self.is_uuid(project_id): + project_identifier = ( + ProjectIdentifier.objects.filter( + workspace__slug=self.workspace_slug, name=project_id + ) + .values("project_id") + .first() + ) + # If the project_identifier is not present then raise ObjectDoesNotExist + if not project_identifier: + raise ObjectDoesNotExist + + # Update the project_id in the kwargs + return project_identifier.get("project_id") + return project_id + + def get_issue_id(self, issue_id): + # If the issue_id is present and is not a UUID then get the issue_id from the issue_identifier + if issue_id and not self.is_uuid(issue_id): + pass + return issue_id + def get_queryset(self): try: return self.model.objects.all() @@ -116,8 +149,18 @@ class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator): def dispatch(self, request, *args, **kwargs): try: + # Write identifier to the uuid + project_id = self.kwargs.get("project_id", None) + if project_id: + self.kwargs["project_id"] = self.get_project_id(project_id) + # Check issue_id is present in the kwargs + issue_id = self.kwargs.get("issue_id", None) + if issue_id: + self.kwargs["issue_id"] = self.get_issue_id(issue_id) + # If the project_id is not present then get the project_id from the project_identifier response = super().dispatch(request, *args, **kwargs) + # Print the number of queries if the debug is enabled if settings.DEBUG: from django.db import connection @@ -125,6 +168,7 @@ class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator): f"{request.method} - {request.get_full_path()} of Queries: {len(connection.queries)}" ) + # Return the response return response except Exception as exc: response = self.handle_exception(exc) diff --git a/apiserver/plane/app/views/issue/base.py b/apiserver/plane/app/views/issue/base.py index e7b12a528d..acaeeb06de 100644 --- a/apiserver/plane/app/views/issue/base.py +++ b/apiserver/plane/app/views/issue/base.py @@ -58,8 +58,7 @@ from plane.utils.paginator import ( ) from .. import BaseAPIView, BaseViewSet from plane.utils.user_timezone_converter import user_timezone_converter - -# Module imports +from plane.utils.valid_int_uuid import is_int_or_uuid class IssueListEndpoint(BaseAPIView): @@ -410,9 +409,10 @@ class IssueViewSet(BaseViewSet): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) def retrieve(self, request, slug, project_id, pk=None): + type, value = is_int_or_uuid(pk) + issue = ( self.get_queryset() - .filter(pk=pk) .annotate( label_ids=Coalesce( ArrayAgg( @@ -470,7 +470,18 @@ class IssueViewSet(BaseViewSet): ) ) ) - ).first() + ) + if type == "int": + issue = issue.filter(sequence_id=pk) + elif type == "uuid": + issue = issue.filter(pk=pk) + else: + return Response( + {"error": "Invalid issue id"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + issue = issue.first() if not issue: return Response( {"error": "The required object does not exist."}, diff --git a/apiserver/plane/app/views/project/base.py b/apiserver/plane/app/views/project/base.py index 55471e6743..bd8f4e471d 100644 --- a/apiserver/plane/app/views/project/base.py +++ b/apiserver/plane/app/views/project/base.py @@ -61,6 +61,21 @@ class ProjectViewSet(BaseViewSet): ProjectBasePermission, ] + def dispatch(self, request, *args, **kwargs): + pk = kwargs.get("pk") + if pk and not self.is_uuid(pk): + project = ( + ProjectIdentifier.objects.filter( + name=pk, workspace__slug=kwargs.get("slug") + ) + .values("project") + .first() + ) + if project: + kwargs["pk"] = project["project"] + + return super().dispatch(request, *args, **kwargs) + def get_queryset(self): sort_order = ProjectMember.objects.filter( member=self.request.user, @@ -484,32 +499,39 @@ class ProjectIdentifierEndpoint(BaseAPIView): ] def get(self, request, slug): + # Get the identifier name name = request.GET.get("name", "").strip().upper() + # Check if the name is passed if name == "": return Response( {"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST, ) + # Check if the identifier exists exists = ProjectIdentifier.objects.filter( name=name, workspace__slug=slug ).values("id", "name", "project") + # Return the response return Response( {"exists": len(exists), "identifiers": exists}, status=status.HTTP_200_OK, ) def delete(self, request, slug): + # Get the identifier name name = request.data.get("name", "").strip().upper() + # Check if the name is passed if name == "": return Response( {"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST, ) + # Check if the identifier is used in any project if Project.objects.filter( identifier=name, workspace__slug=slug ).exists(): @@ -520,10 +542,12 @@ class ProjectIdentifierEndpoint(BaseAPIView): status=status.HTTP_400_BAD_REQUEST, ) + # Delete the identifier ProjectIdentifier.objects.filter( name=name, workspace__slug=slug ).delete() + # Return success return Response( status=status.HTTP_204_NO_CONTENT, ) diff --git a/apiserver/plane/db/migrations/0070_projectidentifier_current_active_and_more.py b/apiserver/plane/db/migrations/0070_projectidentifier_current_active_and_more.py new file mode 100644 index 0000000000..a2814ebf13 --- /dev/null +++ b/apiserver/plane/db/migrations/0070_projectidentifier_current_active_and_more.py @@ -0,0 +1,41 @@ +# Generated by Django 4.2.11 on 2024-06-25 07:32 + +from django.db import migrations, models +import django.db.models.deletion + + +def migrate_current_active(apps, schema_editor): + ProjectIdentifier = apps.get_model("db", "ProjectIdentifier") + + # Update current_active field with name field + ProjectIdentifier.objects.update(current_active=models.F("name")) + + +class Migration(migrations.Migration): + + dependencies = [ + ("db", "0069_alter_account_provider_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="projectidentifier", + name="current_active", + field=models.CharField(db_index=True, max_length=12, null=True), + ), + migrations.AlterField( + model_name="projectidentifier", + name="name", + field=models.CharField(db_index=True, max_length=12), + ), + migrations.AlterField( + model_name="projectidentifier", + name="project", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_identifier", + to="db.project", + ), + ), + migrations.RunPython(migrate_current_active), + ] diff --git a/apiserver/plane/db/models/project.py b/apiserver/plane/db/models/project.py index ba8dbf580f..6e7b0e76f3 100644 --- a/apiserver/plane/db/models/project.py +++ b/apiserver/plane/db/models/project.py @@ -206,7 +206,6 @@ class ProjectMember(ProjectBaseModel): return f"{self.member.email} <{self.project.name}>" -# TODO: Remove workspace relation later class ProjectIdentifier(AuditModel): workspace = models.ForeignKey( "db.Workspace", @@ -214,10 +213,11 @@ class ProjectIdentifier(AuditModel): related_name="project_identifiers", null=True, ) - project = models.OneToOneField( + project = models.ForeignKey( Project, on_delete=models.CASCADE, related_name="project_identifier" ) - name = models.CharField(max_length=12) + name = models.CharField(max_length=12, db_index=True) + current_active = models.CharField(max_length=12, db_index=True, null=True) class Meta: unique_together = ["name", "workspace"] diff --git a/apiserver/plane/settings/local.py b/apiserver/plane/settings/local.py index b175e4c835..14f226f420 100644 --- a/apiserver/plane/settings/local.py +++ b/apiserver/plane/settings/local.py @@ -8,7 +8,10 @@ DEBUG = True # Debug Toolbar settings INSTALLED_APPS += ("debug_toolbar",) # noqa -MIDDLEWARE += ("debug_toolbar.middleware.DebugToolbarMiddleware",) # noqa +MIDDLEWARE += ( # noqa + "debug_toolbar.middleware.DebugToolbarMiddleware", + "kolo.middleware.KoloMiddleware", +) DEBUG_TOOLBAR_PATCH_SETTINGS = False diff --git a/apiserver/plane/utils/valid_int_uuid.py b/apiserver/plane/utils/valid_int_uuid.py new file mode 100644 index 0000000000..48f483b566 --- /dev/null +++ b/apiserver/plane/utils/valid_int_uuid.py @@ -0,0 +1,20 @@ +# Python imports +import uuid + + +def is_int_or_uuid(value): + # Check if the value can be converted to an integer + try: + int_value = int(value) + return "int", int_value + except ValueError: + pass + + # Check if the value can be converted to a UUID + try: + uuid_value = uuid.UUID(str(value)) + return "uuid", uuid_value + except ValueError: + pass + + return "", None