Compare commits

...

3 Commits

Author SHA1 Message Date
pablohashescobar
bdffb58581 dev: update urls to use project identifier and issue sequence 2024-06-26 15:02:18 +05:30
pablohashescobar
6cd03cbb56 Merge branch 'preview' of github.com:makeplane/plane into chore-urls 2024-06-26 13:07:01 +05:30
pablohashescobar
e6ecfcbbc7 dev: update urls to use project identifiers and issue sequence 2024-06-25 21:18:01 +05:30
21 changed files with 487 additions and 260 deletions

View File

@@ -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):

View File

@@ -14,7 +14,7 @@ from plane.app.views import (
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/",
"workspaces/<str:slug>/projects/<str:project_id>/cycles/",
CycleViewSet.as_view(
{
"get": "list",
@@ -24,7 +24,7 @@ urlpatterns = [
name="project-cycle",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:pk>/",
"workspaces/<str:slug>/projects/<str:project_id>/cycles/<uuid:pk>/",
CycleViewSet.as_view(
{
"get": "retrieve",
@@ -36,7 +36,7 @@ urlpatterns = [
name="project-cycle",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/cycle-issues/",
"workspaces/<str:slug>/projects/<str:project_id>/cycles/<uuid:cycle_id>/cycle-issues/",
CycleIssueViewSet.as_view(
{
"get": "list",
@@ -46,7 +46,7 @@ urlpatterns = [
name="project-issue-cycle",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/cycle-issues/<uuid:issue_id>/",
"workspaces/<str:slug>/projects/<str:project_id>/cycles/<uuid:cycle_id>/cycle-issues/<uuid:issue_id>/",
CycleIssueViewSet.as_view(
{
"get": "retrieve",
@@ -58,12 +58,12 @@ urlpatterns = [
name="project-issue-cycle",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/date-check/",
"workspaces/<str:slug>/projects/<str:project_id>/cycles/date-check/",
CycleDateCheckEndpoint.as_view(),
name="project-cycle-date",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-cycles/",
"workspaces/<str:slug>/projects/<str:project_id>/user-favorite-cycles/",
CycleFavoriteViewSet.as_view(
{
"get": "list",
@@ -73,7 +73,7 @@ urlpatterns = [
name="user-favorite-cycle",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-cycles/<uuid:cycle_id>/",
"workspaces/<str:slug>/projects/<str:project_id>/user-favorite-cycles/<uuid:cycle_id>/",
CycleFavoriteViewSet.as_view(
{
"delete": "destroy",
@@ -82,27 +82,27 @@ urlpatterns = [
name="user-favorite-cycle",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/transfer-issues/",
"workspaces/<str:slug>/projects/<str:project_id>/cycles/<uuid:cycle_id>/transfer-issues/",
TransferCycleIssueEndpoint.as_view(),
name="transfer-issues",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/user-properties/",
"workspaces/<str:slug>/projects/<str:project_id>/cycles/<uuid:cycle_id>/user-properties/",
CycleUserPropertiesEndpoint.as_view(),
name="cycle-user-filters",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/archive/",
"workspaces/<str:slug>/projects/<str:project_id>/cycles/<uuid:cycle_id>/archive/",
CycleArchiveUnarchiveEndpoint.as_view(),
name="cycle-archive-unarchive",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-cycles/",
"workspaces/<str:slug>/projects/<str:project_id>/archived-cycles/",
CycleArchiveUnarchiveEndpoint.as_view(),
name="cycle-archive-unarchive",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-cycles/<uuid:pk>/",
"workspaces/<str:slug>/projects/<str:project_id>/archived-cycles/<uuid:pk>/",
CycleArchiveUnarchiveEndpoint.as_view(),
name="cycle-archive-unarchive",
),

View File

@@ -10,12 +10,12 @@ from plane.app.views import (
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/project-estimates/",
"workspaces/<str:slug>/projects/<str:project_id>/project-estimates/",
ProjectEstimatePointEndpoint.as_view(),
name="project-estimate-points",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/",
"workspaces/<str:slug>/projects/<str:project_id>/estimates/",
BulkEstimatePointEndpoint.as_view(
{
"get": "list",
@@ -25,7 +25,7 @@ urlpatterns = [
name="bulk-create-estimate-points",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/<uuid:estimate_id>/",
"workspaces/<str:slug>/projects/<str:project_id>/estimates/<uuid:estimate_id>/",
BulkEstimatePointEndpoint.as_view(
{
"get": "retrieve",
@@ -36,7 +36,7 @@ urlpatterns = [
name="bulk-create-estimate-points",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/<uuid:estimate_id>/estimate-points/",
"workspaces/<str:slug>/projects/<str:project_id>/estimates/<uuid:estimate_id>/estimate-points/",
EstimatePointEndpoint.as_view(
{
"post": "create",
@@ -45,7 +45,7 @@ urlpatterns = [
name="estimate-points",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/<uuid:estimate_id>/estimate-points/<estimate_point_id>/",
"workspaces/<str:slug>/projects/<str:project_id>/estimates/<uuid:estimate_id>/estimate-points/<estimate_point_id>/",
EstimatePointEndpoint.as_view(
{
"patch": "partial_update",

View File

@@ -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/<str:slug>/projects/<uuid:project_id>/ai-assistant/",
"workspaces/<str:slug>/projects/<str:project_id>/ai-assistant/",
GPTIntegrationEndpoint.as_view(),
name="importer",
),
path(
path(
"workspaces/<str:slug>/ai-assistant/",
WorkspaceGPTIntegrationEndpoint.as_view(),
name="importer",

View File

@@ -9,7 +9,7 @@ from plane.app.views import (
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/",
"workspaces/<str:slug>/projects/<str:project_id>/inboxes/",
InboxViewSet.as_view(
{
"get": "list",
@@ -19,7 +19,7 @@ urlpatterns = [
name="inbox",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/<uuid:pk>/",
"workspaces/<str:slug>/projects/<str:project_id>/inboxes/<uuid:pk>/",
InboxViewSet.as_view(
{
"get": "retrieve",
@@ -30,7 +30,7 @@ urlpatterns = [
name="inbox",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/",
"workspaces/<str:slug>/projects/<str:project_id>/inbox-issues/",
InboxIssueViewSet.as_view(
{
"get": "list",
@@ -40,7 +40,7 @@ urlpatterns = [
name="inbox-issue",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/<uuid:issue_id>/",
"workspaces/<str:slug>/projects/<str:project_id>/inbox-issues/<uuid:issue_id>/",
InboxIssueViewSet.as_view(
{
"get": "retrieve",

View File

@@ -25,12 +25,12 @@ from plane.app.views import (
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/list/",
"workspaces/<str:slug>/projects/<str:project_id>/issues/list/",
IssueListEndpoint.as_view(),
name="project-issue",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/",
"workspaces/<str:slug>/projects/<str:project_id>/issues/",
IssueViewSet.as_view(
{
"get": "list",
@@ -40,7 +40,7 @@ urlpatterns = [
name="project-issue",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:pk>/",
"workspaces/<str:slug>/projects/<str:project_id>/issues/<str:pk>/",
IssueViewSet.as_view(
{
"get": "retrieve",
@@ -52,7 +52,7 @@ urlpatterns = [
name="project-issue",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-labels/",
"workspaces/<str:slug>/projects/<str:project_id>/issue-labels/",
LabelViewSet.as_view(
{
"get": "list",
@@ -62,7 +62,7 @@ urlpatterns = [
name="project-issue-labels",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-labels/<uuid:pk>/",
"workspaces/<str:slug>/projects/<str:project_id>/issue-labels/<uuid:pk>/",
LabelViewSet.as_view(
{
"get": "retrieve",
@@ -74,28 +74,28 @@ urlpatterns = [
name="project-issue-labels",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/bulk-create-labels/",
"workspaces/<str:slug>/projects/<str:project_id>/bulk-create-labels/",
BulkCreateIssueLabelsEndpoint.as_view(),
name="project-bulk-labels",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/bulk-delete-issues/",
"workspaces/<str:slug>/projects/<str:project_id>/bulk-delete-issues/",
BulkDeleteIssuesEndpoint.as_view(),
name="project-issues-bulk",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/bulk-archive-issues/",
"workspaces/<str:slug>/projects/<str:project_id>/bulk-archive-issues/",
BulkArchiveIssuesEndpoint.as_view(),
name="bulk-archive-issues",
),
##
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/sub-issues/",
"workspaces/<str:slug>/projects/<str:project_id>/issues/<str:issue_id>/sub-issues/",
SubIssuesEndpoint.as_view(),
name="sub-issues",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-links/",
"workspaces/<str:slug>/projects/<str:project_id>/issues/<str:issue_id>/issue-links/",
IssueLinkViewSet.as_view(
{
"get": "list",
@@ -105,7 +105,7 @@ urlpatterns = [
name="project-issue-links",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-links/<uuid:pk>/",
"workspaces/<str:slug>/projects/<str:project_id>/issues/<str:issue_id>/issue-links/<uuid:pk>/",
IssueLinkViewSet.as_view(
{
"get": "retrieve",
@@ -117,12 +117,12 @@ urlpatterns = [
name="project-issue-links",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-attachments/",
"workspaces/<str:slug>/projects/<str:project_id>/issues/<str:issue_id>/issue-attachments/",
IssueAttachmentEndpoint.as_view(),
name="project-issue-attachments",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-attachments/<uuid:pk>/",
"workspaces/<str:slug>/projects/<str:project_id>/issues/<str:issue_id>/issue-attachments/<uuid:pk>/",
IssueAttachmentEndpoint.as_view(),
name="project-issue-attachments",
),
@@ -134,14 +134,14 @@ urlpatterns = [
## End Issues
## Issue Activity
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/history/",
"workspaces/<str:slug>/projects/<str:project_id>/issues/<str:issue_id>/history/",
IssueActivityEndpoint.as_view(),
name="project-issue-history",
),
## Issue Activity
## IssueComments
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/comments/",
"workspaces/<str:slug>/projects/<str:project_id>/issues/<str:issue_id>/comments/",
IssueCommentViewSet.as_view(
{
"get": "list",
@@ -151,7 +151,7 @@ urlpatterns = [
name="project-issue-comment",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/comments/<uuid:pk>/",
"workspaces/<str:slug>/projects/<str:project_id>/issues/<str:issue_id>/comments/<uuid:pk>/",
IssueCommentViewSet.as_view(
{
"get": "retrieve",
@@ -165,7 +165,7 @@ urlpatterns = [
## End IssueComments
# Issue Subscribers
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-subscribers/",
"workspaces/<str:slug>/projects/<str:project_id>/issues/<str:issue_id>/issue-subscribers/",
IssueSubscriberViewSet.as_view(
{
"get": "list",
@@ -175,12 +175,12 @@ urlpatterns = [
name="project-issue-subscribers",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-subscribers/<uuid:subscriber_id>/",
"workspaces/<str:slug>/projects/<str:project_id>/issues/<str:issue_id>/issue-subscribers/<uuid:subscriber_id>/",
IssueSubscriberViewSet.as_view({"delete": "destroy"}),
name="project-issue-subscribers",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/subscribe/",
"workspaces/<str:slug>/projects/<str:project_id>/issues/<str:issue_id>/subscribe/",
IssueSubscriberViewSet.as_view(
{
"get": "subscription_status",
@@ -193,7 +193,7 @@ urlpatterns = [
## End Issue Subscribers
# Issue Reactions
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/reactions/",
"workspaces/<str:slug>/projects/<str:project_id>/issues/<str:issue_id>/reactions/",
IssueReactionViewSet.as_view(
{
"get": "list",
@@ -203,7 +203,7 @@ urlpatterns = [
name="project-issue-reactions",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/reactions/<str:reaction_code>/",
"workspaces/<str:slug>/projects/<str:project_id>/issues/<str:issue_id>/reactions/<str:reaction_code>/",
IssueReactionViewSet.as_view(
{
"delete": "destroy",
@@ -214,7 +214,7 @@ urlpatterns = [
## End Issue Reactions
# Comment Reactions
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/comments/<uuid:comment_id>/reactions/",
"workspaces/<str:slug>/projects/<str:project_id>/comments/<uuid:comment_id>/reactions/",
CommentReactionViewSet.as_view(
{
"get": "list",
@@ -224,7 +224,7 @@ urlpatterns = [
name="project-issue-comment-reactions",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/comments/<uuid:comment_id>/reactions/<str:reaction_code>/",
"workspaces/<str:slug>/projects/<str:project_id>/comments/<uuid:comment_id>/reactions/<str:reaction_code>/",
CommentReactionViewSet.as_view(
{
"delete": "destroy",
@@ -235,14 +235,14 @@ urlpatterns = [
## End Comment Reactions
## IssueProperty
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-properties/",
"workspaces/<str:slug>/projects/<str:project_id>/user-properties/",
IssueUserDisplayPropertyEndpoint.as_view(),
name="project-issue-display-properties",
),
## IssueProperty End
## Issue Archives
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-issues/",
"workspaces/<str:slug>/projects/<str:project_id>/archived-issues/",
IssueArchiveViewSet.as_view(
{
"get": "list",
@@ -251,7 +251,7 @@ urlpatterns = [
name="project-issue-archive",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:pk>/archive/",
"workspaces/<str:slug>/projects/<str:project_id>/issues/<uuid:pk>/archive/",
IssueArchiveViewSet.as_view(
{
"get": "retrieve",
@@ -264,7 +264,7 @@ urlpatterns = [
## End Issue Archives
## Issue Relation
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-relation/",
"workspaces/<str:slug>/projects/<str:project_id>/issues/<str:issue_id>/issue-relation/",
IssueRelationViewSet.as_view(
{
"get": "list",
@@ -274,7 +274,7 @@ urlpatterns = [
name="issue-relation",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/remove-relation/",
"workspaces/<str:slug>/projects/<str:project_id>/issues/<str:issue_id>/remove-relation/",
IssueRelationViewSet.as_view(
{
"post": "remove_relation",
@@ -285,7 +285,7 @@ urlpatterns = [
## End Issue Relation
## Issue Drafts
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-drafts/",
"workspaces/<str:slug>/projects/<str:project_id>/issue-drafts/",
IssueDraftViewSet.as_view(
{
"get": "list",
@@ -295,7 +295,7 @@ urlpatterns = [
name="project-issue-draft",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-drafts/<uuid:pk>/",
"workspaces/<str:slug>/projects/<str:project_id>/issue-drafts/<uuid:pk>/",
IssueDraftViewSet.as_view(
{
"get": "retrieve",
@@ -306,7 +306,7 @@ urlpatterns = [
name="project-issue-draft",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/bulk-operation-issues/",
"workspaces/<str:slug>/projects/<str:project_id>/bulk-operation-issues/",
BulkIssueOperationsEndpoint.as_view(),
name="bulk-operations-issues",
),

View File

@@ -13,7 +13,7 @@ from plane.app.views import (
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/",
"workspaces/<str:slug>/projects/<str:project_id>/modules/",
ModuleViewSet.as_view(
{
"get": "list",
@@ -23,7 +23,7 @@ urlpatterns = [
name="project-modules",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:pk>/",
"workspaces/<str:slug>/projects/<str:project_id>/modules/<uuid:pk>/",
ModuleViewSet.as_view(
{
"get": "retrieve",
@@ -35,7 +35,7 @@ urlpatterns = [
name="project-modules",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/modules/",
"workspaces/<str:slug>/projects/<str:project_id>/issues/<uuid:issue_id>/modules/",
ModuleIssueViewSet.as_view(
{
"post": "create_issue_modules",
@@ -44,7 +44,7 @@ urlpatterns = [
name="issue-module",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/issues/",
"workspaces/<str:slug>/projects/<str:project_id>/modules/<uuid:module_id>/issues/",
ModuleIssueViewSet.as_view(
{
"post": "create_module_issues",
@@ -54,7 +54,7 @@ urlpatterns = [
name="project-module-issues",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/issues/<uuid:issue_id>/",
"workspaces/<str:slug>/projects/<str:project_id>/modules/<uuid:module_id>/issues/<uuid:issue_id>/",
ModuleIssueViewSet.as_view(
{
"get": "retrieve",
@@ -66,7 +66,7 @@ urlpatterns = [
name="project-module-issues",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/module-links/",
"workspaces/<str:slug>/projects/<str:project_id>/modules/<uuid:module_id>/module-links/",
ModuleLinkViewSet.as_view(
{
"get": "list",
@@ -76,7 +76,7 @@ urlpatterns = [
name="project-issue-module-links",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/module-links/<uuid:pk>/",
"workspaces/<str:slug>/projects/<str:project_id>/modules/<uuid:module_id>/module-links/<uuid:pk>/",
ModuleLinkViewSet.as_view(
{
"get": "retrieve",
@@ -88,7 +88,7 @@ urlpatterns = [
name="project-issue-module-links",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-modules/",
"workspaces/<str:slug>/projects/<str:project_id>/user-favorite-modules/",
ModuleFavoriteViewSet.as_view(
{
"get": "list",
@@ -98,7 +98,7 @@ urlpatterns = [
name="user-favorite-module",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-modules/<uuid:module_id>/",
"workspaces/<str:slug>/projects/<str:project_id>/user-favorite-modules/<uuid:module_id>/",
ModuleFavoriteViewSet.as_view(
{
"delete": "destroy",
@@ -107,22 +107,22 @@ urlpatterns = [
name="user-favorite-module",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/user-properties/",
"workspaces/<str:slug>/projects/<str:project_id>/modules/<uuid:module_id>/user-properties/",
ModuleUserPropertiesEndpoint.as_view(),
name="cycle-user-filters",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/archive/",
"workspaces/<str:slug>/projects/<str:project_id>/modules/<uuid:module_id>/archive/",
ModuleArchiveUnarchiveEndpoint.as_view(),
name="module-archive-unarchive",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-modules/",
"workspaces/<str:slug>/projects/<str:project_id>/archived-modules/",
ModuleArchiveUnarchiveEndpoint.as_view(),
name="module-archive-unarchive",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-modules/<uuid:pk>/",
"workspaces/<str:slug>/projects/<str:project_id>/archived-modules/<uuid:pk>/",
ModuleArchiveUnarchiveEndpoint.as_view(),
name="module-archive-unarchive",
),

View File

@@ -12,7 +12,7 @@ from plane.app.views import (
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/",
"workspaces/<str:slug>/projects/<str:project_id>/pages/",
PageViewSet.as_view(
{
"get": "list",
@@ -22,7 +22,7 @@ urlpatterns = [
name="project-pages",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/",
"workspaces/<str:slug>/projects/<str:project_id>/pages/<uuid:pk>/",
PageViewSet.as_view(
{
"get": "retrieve",
@@ -34,7 +34,7 @@ urlpatterns = [
),
# favorite pages
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/favorite-pages/<uuid:pk>/",
"workspaces/<str:slug>/projects/<str:project_id>/favorite-pages/<uuid:pk>/",
PageFavoriteViewSet.as_view(
{
"post": "create",
@@ -45,7 +45,7 @@ urlpatterns = [
),
# archived pages
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/archive/",
"workspaces/<str:slug>/projects/<str:project_id>/pages/<uuid:pk>/archive/",
PageViewSet.as_view(
{
"post": "archive",
@@ -56,7 +56,7 @@ urlpatterns = [
),
# lock and unlock
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/lock/",
"workspaces/<str:slug>/projects/<str:project_id>/pages/<uuid:pk>/lock/",
PageViewSet.as_view(
{
"post": "lock",
@@ -66,22 +66,22 @@ urlpatterns = [
name="project-pages-lock-unlock",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/transactions/",
"workspaces/<str:slug>/projects/<str:project_id>/pages/<uuid:pk>/transactions/",
PageLogEndpoint.as_view(),
name="page-transactions",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/transactions/<uuid:transaction>/",
"workspaces/<str:slug>/projects/<str:project_id>/pages/<uuid:pk>/transactions/<uuid:transaction>/",
PageLogEndpoint.as_view(),
name="page-transactions",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/sub-pages/",
"workspaces/<str:slug>/projects/<str:project_id>/pages/<uuid:pk>/sub-pages/",
SubPagesEndpoint.as_view(),
name="sub-page",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/description/",
"workspaces/<str:slug>/projects/<str:project_id>/pages/<uuid:pk>/description/",
PagesDescriptionViewSet.as_view(
{
"get": "retrieve",

View File

@@ -30,7 +30,7 @@ urlpatterns = [
name="project",
),
path(
"workspaces/<str:slug>/projects/<uuid:pk>/",
"workspaces/<str:slug>/projects/<str:pk>/",
ProjectViewSet.as_view(
{
"get": "retrieve",
@@ -47,7 +47,7 @@ urlpatterns = [
name="project-identifiers",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/invitations/",
"workspaces/<str:slug>/projects/<str:project_id>/invitations/",
ProjectInvitationsViewset.as_view(
{
"get": "list",
@@ -57,7 +57,7 @@ urlpatterns = [
name="project-member-invite",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/invitations/<uuid:pk>/",
"workspaces/<str:slug>/projects/<str:project_id>/invitations/<uuid:pk>/",
ProjectInvitationsViewset.as_view(
{
"get": "retrieve",
@@ -82,12 +82,12 @@ urlpatterns = [
name="user-project-roles",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/join/<uuid:pk>/",
"workspaces/<str:slug>/projects/<str:project_id>/join/<uuid:pk>/",
ProjectJoinEndpoint.as_view(),
name="project-join",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/members/",
"workspaces/<str:slug>/projects/<str:project_id>/members/",
ProjectMemberViewSet.as_view(
{
"get": "list",
@@ -97,7 +97,7 @@ urlpatterns = [
name="project-member",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/members/<uuid:pk>/",
"workspaces/<str:slug>/projects/<str:project_id>/members/<uuid:pk>/",
ProjectMemberViewSet.as_view(
{
"get": "retrieve",
@@ -108,7 +108,7 @@ urlpatterns = [
name="project-member",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/members/leave/",
"workspaces/<str:slug>/projects/<str:project_id>/members/leave/",
ProjectMemberViewSet.as_view(
{
"post": "leave",
@@ -117,17 +117,17 @@ urlpatterns = [
name="project-member",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/team-invite/",
"workspaces/<str:slug>/projects/<str:project_id>/team-invite/",
AddTeamToProjectEndpoint.as_view(),
name="projects",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/project-views/",
"workspaces/<str:slug>/projects/<str:project_id>/project-views/",
ProjectUserViewsEndpoint.as_view(),
name="project-view",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/project-members/me/",
"workspaces/<str:slug>/projects/<str:project_id>/project-members/me/",
ProjectMemberUserEndpoint.as_view(),
name="project-member-view",
),
@@ -142,7 +142,7 @@ urlpatterns = [
name="project-favorite",
),
path(
"workspaces/<str:slug>/user-favorite-projects/<uuid:project_id>/",
"workspaces/<str:slug>/user-favorite-projects/<str:project_id>/",
ProjectFavoritesViewSet.as_view(
{
"delete": "destroy",
@@ -156,7 +156,7 @@ urlpatterns = [
name="project-covers",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/project-deploy-boards/",
"workspaces/<str:slug>/projects/<str:project_id>/project-deploy-boards/",
DeployBoardViewSet.as_view(
{
"get": "list",
@@ -166,7 +166,7 @@ urlpatterns = [
name="project-deploy-board",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/project-deploy-boards/<uuid:pk>/",
"workspaces/<str:slug>/projects/<str:project_id>/project-deploy-boards/<uuid:pk>/",
DeployBoardViewSet.as_view(
{
"get": "retrieve",
@@ -177,7 +177,7 @@ urlpatterns = [
name="project-deploy-board",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/archive/",
"workspaces/<str:slug>/projects/<str:project_id>/archive/",
ProjectArchiveUnarchiveEndpoint.as_view(),
name="project-archive-unarchive",
),

View File

@@ -14,7 +14,7 @@ urlpatterns = [
name="global-search",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/search-issues/",
"workspaces/<str:slug>/projects/<str:project_id>/search-issues/",
IssueSearchEndpoint.as_view(),
name="project-issue-search",
),

View File

@@ -6,7 +6,7 @@ from plane.app.views import StateViewSet
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/states/",
"workspaces/<str:slug>/projects/<str:project_id>/states/",
StateViewSet.as_view(
{
"get": "list",
@@ -16,7 +16,7 @@ urlpatterns = [
name="project-states",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/states/<uuid:pk>/",
"workspaces/<str:slug>/projects/<str:project_id>/states/<uuid:pk>/",
StateViewSet.as_view(
{
"get": "retrieve",
@@ -27,7 +27,7 @@ urlpatterns = [
name="project-state",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/states/<uuid:pk>/mark-default/",
"workspaces/<str:slug>/projects/<str:project_id>/states/<uuid:pk>/mark-default/",
StateViewSet.as_view(
{
"post": "mark_as_default",

View File

@@ -11,7 +11,7 @@ from plane.app.views import (
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/views/",
"workspaces/<str:slug>/projects/<str:project_id>/views/",
IssueViewViewSet.as_view(
{
"get": "list",
@@ -21,7 +21,7 @@ urlpatterns = [
name="project-view",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/views/<uuid:pk>/",
"workspaces/<str:slug>/projects/<str:project_id>/views/<uuid:pk>/",
IssueViewViewSet.as_view(
{
"get": "retrieve",
@@ -64,7 +64,7 @@ urlpatterns = [
name="global-view-issues",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-views/",
"workspaces/<str:slug>/projects/<str:project_id>/user-favorite-views/",
IssueViewFavoriteViewSet.as_view(
{
"get": "list",
@@ -74,7 +74,7 @@ urlpatterns = [
name="user-favorite-view",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-views/<uuid:view_id>/",
"workspaces/<str:slug>/projects/<str:project_id>/user-favorite-views/<uuid:view_id>/",
IssueViewFavoriteViewSet.as_view(
{
"delete": "destroy",

View File

@@ -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,8 @@ 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, IssueSequence
from plane.utils.valid_int_uuid import is_uuid, is_int_or_uuid
class TimezoneMixin:
@@ -60,6 +62,44 @@ class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
search_fields = []
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 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, project_id):
# If the issue_id is present and is not a UUID then get the issue_id from the issue_identifier
if issue_id:
type, value = is_int_or_uuid(issue_id)
if type == "int":
issue_identifier = (
IssueSequence.objects.filter(
project_id=project_id, sequence=value
)
.values("issue_id")
.first()
)
# If the issue_identifier is not present then raise ObjectDoesNotExist
if not issue_identifier:
raise ObjectDoesNotExist
# Update the issue_id in the kwargs
return issue_identifier.get("issue_id")
return issue_id
def get_queryset(self):
try:
return self.model.objects.all()
@@ -116,8 +156,22 @@ class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
def dispatch(self, request, *args, **kwargs):
try:
# Write identifier to the uuid
project_id = kwargs.get("project_id", None)
if project_id:
kwargs["project_id"] = self.get_project_id(project_id)
# Check issue_id is present in the kwargs
issue_id = kwargs.get("issue_id", None)
if issue_id:
kwargs["issue_id"] = self.get_issue_id(
issue_id, kwargs["project_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 +179,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)
@@ -138,7 +193,7 @@ class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
def project_id(self):
project_id = self.kwargs.get("project_id", None)
if project_id:
return project_id
return self.get_project_id(project_id)
if resolve(self.request.path_info).url_name == "project":
return self.kwargs.get("pk", None)

View File

@@ -44,6 +44,7 @@ from plane.db.models import (
IssueReaction,
IssueSubscriber,
Project,
IssueSequence,
)
from plane.utils.grouper import (
issue_group_values,
@@ -58,8 +59,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):
@@ -197,6 +197,23 @@ class IssueViewSet(BaseViewSet):
"workspace__id",
]
def process_issue_id(self, issue_id, project_id):
# Check if the pk is an integer or a UUID
type, value = is_int_or_uuid(issue_id)
if type == "int":
# Get the issue_id from the sequence_id
sequence = (
IssueSequence.objects.filter(
sequence=value,
project_id=project_id,
)
.values("issue_id")
.first()
)
# Call the parent dispatch method
return sequence.get("issue_id")
return issue_id
def get_queryset(self):
return (
Issue.issue_objects.filter(
@@ -410,67 +427,76 @@ class IssueViewSet(BaseViewSet):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def retrieve(self, request, slug, project_id, pk=None):
pk = self.process_issue_id(pk, project_id)
issue = (
self.get_queryset()
.filter(pk=pk)
.annotate(
label_ids=Coalesce(
ArrayAgg(
"labels__id",
distinct=True,
filter=~Q(labels__id__isnull=True),
(
self.get_queryset()
.annotate(
label_ids=Coalesce(
ArrayAgg(
"labels__id",
distinct=True,
filter=~Q(labels__id__isnull=True),
),
Value([], output_field=ArrayField(UUIDField())),
),
Value([], output_field=ArrayField(UUIDField())),
),
assignee_ids=Coalesce(
ArrayAgg(
"assignees__id",
distinct=True,
filter=~Q(assignees__id__isnull=True)
& Q(assignees__member_project__is_active=True),
assignee_ids=Coalesce(
ArrayAgg(
"assignees__id",
distinct=True,
filter=~Q(assignees__id__isnull=True)
& Q(assignees__member_project__is_active=True),
),
Value([], output_field=ArrayField(UUIDField())),
),
Value([], output_field=ArrayField(UUIDField())),
),
module_ids=Coalesce(
ArrayAgg(
"issue_module__module_id",
distinct=True,
filter=~Q(issue_module__module_id__isnull=True),
),
Value([], output_field=ArrayField(UUIDField())),
),
)
.prefetch_related(
Prefetch(
"issue_reactions",
queryset=IssueReaction.objects.select_related(
"issue", "actor"
module_ids=Coalesce(
ArrayAgg(
"issue_module__module_id",
distinct=True,
filter=~Q(issue_module__module_id__isnull=True),
),
Value([], output_field=ArrayField(UUIDField())),
),
)
)
.prefetch_related(
Prefetch(
"issue_attachment",
queryset=IssueAttachment.objects.select_related("issue"),
.prefetch_related(
Prefetch(
"issue_reactions",
queryset=IssueReaction.objects.select_related(
"issue", "actor"
),
)
)
)
.prefetch_related(
Prefetch(
"issue_link",
queryset=IssueLink.objects.select_related("created_by"),
.prefetch_related(
Prefetch(
"issue_attachment",
queryset=IssueAttachment.objects.select_related(
"issue"
),
)
)
)
.annotate(
is_subscribed=Exists(
IssueSubscriber.objects.filter(
workspace__slug=slug,
project_id=project_id,
issue_id=OuterRef("pk"),
subscriber=request.user,
.prefetch_related(
Prefetch(
"issue_link",
queryset=IssueLink.objects.select_related(
"created_by"
),
)
)
.annotate(
is_subscribed=Exists(
IssueSubscriber.objects.filter(
workspace__slug=slug,
project_id=project_id,
issue_id=OuterRef("pk"),
subscriber=request.user,
)
)
)
)
).first()
.filter(pk=pk)
.first()
)
if not issue:
return Response(
{"error": "The required object does not exist."},
@@ -481,6 +507,7 @@ class IssueViewSet(BaseViewSet):
return Response(serializer.data, status=status.HTTP_200_OK)
def partial_update(self, request, slug, project_id, pk=None):
pk = self.process_issue_id(pk, project_id)
issue = self.get_queryset().filter(pk=pk).first()
if not issue:
@@ -515,8 +542,11 @@ class IssueViewSet(BaseViewSet):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def destroy(self, request, slug, project_id, pk=None):
pk = self.process_issue_id(pk, project_id)
issue = Issue.objects.get(
workspace__slug=slug, project_id=project_id, pk=pk
workspace__slug=slug,
project_id=project_id,
pk=pk,
)
issue.delete()
issue_activity.delay(

View File

@@ -50,6 +50,7 @@ from plane.db.models import (
)
from plane.utils.cache import cache_response
from plane.bgtasks.webhook_task import model_activity
from plane.utils.valid_int_uuid import is_uuid
class ProjectViewSet(BaseViewSet):
@@ -61,6 +62,25 @@ class ProjectViewSet(BaseViewSet):
ProjectBasePermission,
]
def dispatch(self, request, *args, **kwargs):
pk = kwargs.get("pk")
# Check if the project identifier is a UUID
if pk and not is_uuid(pk):
# Check if the project identifier is a name
project = (
ProjectIdentifier.objects.filter(
name=pk, workspace__slug=kwargs.get("slug")
)
.values("project")
.first()
)
if project:
# Update the project identifier with the UUID
kwargs["pk"] = project["project"]
# Call the parent dispatch method
return super().dispatch(request, *args, **kwargs)
def get_queryset(self):
sort_order = ProjectMember.objects.filter(
member=self.request.user,
@@ -484,32 +504,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 +547,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,
)

View File

@@ -442,47 +442,53 @@ def model_activity(
slug,
origin=None,
):
"""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,
verb="created",
field=None,
old_value=None,
new_value=None,
actor_id=actor_id,
slug=slug,
current_site=origin,
event_id=model_id,
old_identifier=None,
new_identifier=None,
try:
"""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,
verb="created",
field=None,
old_value=None,
new_value=None,
actor_id=actor_id,
slug=slug,
current_site=origin,
event_id=model_id,
old_identifier=None,
new_identifier=None,
)
return
# Load the current instance
current_instance = (
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
for key in requested_data:
# Check if key is present in current instance or not
if key in current_instance:
current_value = current_instance.get(key, None)
requested_value = requested_data.get(key, None)
if current_value != requested_value:
webhook_activity.delay(
event=model_name,
verb="updated",
field=key,
old_value=current_value,
new_value=requested_value,
actor_id=actor_id,
slug=slug,
current_site=origin,
event_id=model_id,
old_identifier=None,
new_identifier=None,
)
return
except Exception as e:
log_exception(e)
return
# Load the current instance
current_instance = (
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
for key in requested_data:
# Check if key is present in current instance or not
if key in current_instance:
current_value = current_instance.get(key, None)
requested_value = requested_data.get(key, None)
if current_value != requested_value:
webhook_activity.delay(
event=model_name,
verb="updated",
field=key,
old_value=current_value,
new_value=requested_value,
actor_id=actor_id,
slug=slug,
current_site=origin,
event_id=model_id,
old_identifier=None,
new_identifier=None,
)
return

View File

@@ -0,0 +1,58 @@
# 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),
migrations.AlterField(
model_name="issue",
name="sequence_id",
field=models.IntegerField(
db_index=True, default=1, verbose_name="Issue Sequence ID"
),
),
migrations.AlterField(
model_name="issuesequence",
name="issue",
field=models.OneToOneField(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="issue_sequence",
to="db.issue",
),
),
]

View File

@@ -7,9 +7,8 @@ from django.contrib.postgres.fields import ArrayField
from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.utils import timezone
from django.db import transaction
# Module imports
from plane.utils.html_processor import strip_tags
@@ -174,6 +173,8 @@ class Issue(ProjectBaseModel):
ordering = ("-created_at",)
def save(self, *args, **kwargs):
# is the model being saved for the first time?
is_new = self._state.adding
# This means that the model isn't saved to the database yet
if self.state is None:
try:
@@ -205,32 +206,43 @@ class Issue(ProjectBaseModel):
self.completed_at = None
except ImportError:
pass
# Atomic transaction to ensure that the sequence is created only if the issue is created
with transaction.atomic():
if is_new:
# Get the maximum display_id value from the database
last_id = IssueSequence.objects.filter(
project=self.project
).aggregate(largest=models.Max("sequence"))["largest"]
# aggregate can return None! Check it first.
# If it isn't none, just use the last ID specified (which should be the greatest) and add one to it
if last_id:
self.sequence_id = last_id + 1
else:
self.sequence_id = 1
if self._state.adding:
# Get the maximum display_id value from the database
last_id = IssueSequence.objects.filter(
project=self.project
).aggregate(largest=models.Max("sequence"))["largest"]
# aggregate can return None! Check it first.
# If it isn't none, just use the last ID specified (which should be the greatest) and add one to it
if last_id:
self.sequence_id = last_id + 1
else:
self.sequence_id = 1
largest_sort_order = Issue.objects.filter(
project=self.project, state=self.state
).aggregate(largest=models.Max("sort_order"))["largest"]
if largest_sort_order is not None:
self.sort_order = largest_sort_order + 10000
largest_sort_order = Issue.objects.filter(
project=self.project, state=self.state
).aggregate(largest=models.Max("sort_order"))["largest"]
if largest_sort_order is not None:
self.sort_order = largest_sort_order + 10000
# Strip the html tags using html parser
self.description_stripped = (
None
if (self.description_html == "" or self.description_html is None)
else strip_tags(self.description_html)
)
super(Issue, self).save(*args, **kwargs)
# Strip the html tags using html parser
self.description_stripped = (
None
if (
self.description_html == ""
or self.description_html is None
)
else strip_tags(self.description_html)
)
super(Issue, self).save(*args, **kwargs)
# Create a sequence for the issue
if is_new:
IssueSequence.objects.create(
issue=self,
sequence=self.sequence_id,
project=self.project,
)
def __str__(self):
"""Return name of the issue"""
@@ -667,14 +679,3 @@ class IssueVote(ProjectBaseModel):
def __str__(self):
return f"{self.issue.name} {self.actor.email}"
# TODO: Find a better method to save the model
@receiver(post_save, sender=Issue)
def create_issue_sequence(sender, instance, created, **kwargs):
if created:
IssueSequence.objects.create(
issue=instance,
sequence=instance.sequence_id,
project=instance.project,
)

View File

@@ -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"]

View File

@@ -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

View File

@@ -0,0 +1,29 @@
# Python imports
import uuid
def is_uuid(value):
try:
# Check if the value is a valid UUID
uuid.UUID(str(value))
return True
except ValueError:
return False
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